diff --git a/.github/actions/load-toolchain-pins/action.yml b/.github/actions/load-toolchain-pins/action.yml new file mode 100644 index 0000000..e929f2a --- /dev/null +++ b/.github/actions/load-toolchain-pins/action.yml @@ -0,0 +1,9 @@ +name: "Load toolchain pins" +description: "Export toolchain-pins.env into the job environment." + +runs: + using: composite + steps: + - name: Load toolchain-pins.env + shell: bash + run: bash scripts/load-toolchain-pins.sh "${GITHUB_ENV}" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 65d9ecc..3483253 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,15 +4,6 @@ on: push: pull_request: -env: - RUST_TOOLCHAIN: "1.95.0" - XGENEXT2FS_VERSION: v1.5.6 - XGENEXT2FS_SHA256_AMD64: 996e4e68a638b5dc5967d3410f92ecb8d2f41e32218bbe0f8b4c4474d7eebc59 - XGENEXT2FS_SHA256_ARM64: e5aca81164b762bbe5447bacef41e4fa9e357fd9c8f44e519c5206227d43144d - CARTESI_MACHINE_VERSION: v0.20.0-test2 - CARTESI_MACHINE_SHA256_AMD64: 39bbfc96a6cc6606307294b719df65f4f2725e8d200d062bcbd8c22355b99b56 - CARTESI_MACHINE_SHA256_ARM64: 787d823756000cdecd72da8a3494b4c08613087379035959e561bbaef7a220ba - jobs: rust: runs-on: ubuntu-latest @@ -22,6 +13,12 @@ jobs: - name: Checkout uses: actions/checkout@v5 + - name: Load toolchain pins + uses: ./.github/actions/load-toolchain-pins + + - name: Verify toolchain pin alignment + run: bash scripts/verify-toolchain-pins.sh + - name: Install system dependencies run: | sudo apt-get update @@ -55,6 +52,12 @@ jobs: - name: Clippy run: cargo clippy --workspace --all-targets --all-features --locked -- -D warnings + - name: Watchdog Lua tests + run: lua watchdog/tests/run.lua + + - name: Watchdog divergence drill + run: bash scripts/test-watchdog-divergence-drill.sh + - name: Test timeout-minutes: 15 run: cargo test --workspace --all-targets --all-features --locked @@ -68,6 +71,9 @@ jobs: - name: Checkout uses: actions/checkout@v5 + - name: Load toolchain pins + uses: ./.github/actions/load-toolchain-pins + - name: Setup guest toolchain uses: ./.github/actions/setup-guest-toolchain with: @@ -94,6 +100,9 @@ jobs: - name: Checkout uses: actions/checkout@v5 + - name: Load toolchain pins + uses: ./.github/actions/load-toolchain-pins + - name: Setup guest toolchain uses: ./.github/actions/setup-guest-toolchain with: @@ -111,5 +120,35 @@ jobs: sudo apt-get update sudo apt-get install -y faketime libfaketime + - name: Build watchdog Lua deps + run: | + sudo apt-get install -y libcurl4-openssl-dev build-essential pkg-config + just watchdog-lua-deps + - name: Run rollups E2E tests run: just test-rollups-e2e + + # Runs after the e2e step so the canonical machine image is already built; + # exercises the in-process machine_cartesi binding incl. store -> reload -> advance, + # which the Rust harness never loads (its compare passes only load the genesis image). + - name: Watchdog Lua CM e2e + run: just test-watchdog-e2e + + watchdog-docker: + name: Watchdog Docker image smoke + runs-on: ubuntu-latest + needs: rust + timeout-minutes: 30 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Load toolchain pins + uses: ./.github/actions/load-toolchain-pins + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build and smoke-test watchdog image + run: bash scripts/ci-watchdog-docker-smoke.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7d23ac3..8509458 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,15 +19,6 @@ on: permissions: contents: write -env: - RUST_TOOLCHAIN: "1.95.0" - XGENEXT2FS_VERSION: v1.5.6 - XGENEXT2FS_SHA256_AMD64: 996e4e68a638b5dc5967d3410f92ecb8d2f41e32218bbe0f8b4c4474d7eebc59 - XGENEXT2FS_SHA256_ARM64: e5aca81164b762bbe5447bacef41e4fa9e357fd9c8f44e519c5206227d43144d - CARTESI_MACHINE_VERSION: v0.20.0-test2 - CARTESI_MACHINE_SHA256_AMD64: 39bbfc96a6cc6606307294b719df65f4f2725e8d200d062bcbd8c22355b99b56 - CARTESI_MACHINE_SHA256_ARM64: 787d823756000cdecd72da8a3494b4c08613087379035959e561bbaef7a220ba - jobs: build-sequencer: name: Build sequencer (${{ matrix.arch }}) @@ -45,6 +36,9 @@ jobs: - name: Checkout uses: actions/checkout@v5 + - name: Load toolchain pins + uses: ./.github/actions/load-toolchain-pins + - name: Install system dependencies run: | sudo apt-get update @@ -85,6 +79,10 @@ jobs: mkdir -p "package/sequencer-${TAG}-linux-${ARCH}" cp "target/${TARGET}/release/sequencer" "package/sequencer-${TAG}-linux-${ARCH}/sequencer" + bash scripts/generate-release-manifest.sh \ + --tag "${TAG}" \ + --git-sha "${GITHUB_SHA}" \ + --output "package/sequencer-${TAG}-linux-${ARCH}/RELEASE.json" cat > "package/sequencer-${TAG}-linux-${ARCH}/RUNNING.md" <<'EOF' ## Running @@ -122,6 +120,9 @@ jobs: - name: Checkout uses: actions/checkout@v5 + - name: Load toolchain pins + uses: ./.github/actions/load-toolchain-pins + - name: Setup guest toolchain uses: ./.github/actions/setup-guest-toolchain with: @@ -159,36 +160,100 @@ jobs: name: canonical-machine-images path: dist/canonical-machine-image-*.tar.gz + build-watchdog-image: + name: Build watchdog image (${{ matrix.arch }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + platform: linux/amd64 + deb_sha_env: CARTESI_MACHINE_SHA256_AMD64 + - arch: arm64 + platform: linux/arm64 + deb_sha_env: CARTESI_MACHINE_SHA256_ARM64 + + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Load toolchain pins + uses: ./.github/actions/load-toolchain-pins + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Build and export watchdog image + env: + TAG: ${{ inputs.tag || github.ref_name }} + DEB_SHA_ENV: ${{ matrix.deb_sha_env }} + run: | + set -euo pipefail + DEB_SHA="${!DEB_SHA_ENV}" + image="sequencer-watchdog:${TAG}" + docker build \ + --platform "${{ matrix.platform }}" \ + --build-arg "RELEASE_TAG=${TAG}" \ + --build-arg "GIT_COMMIT=${GITHUB_SHA}" \ + --build-arg "CARTESI_MACHINE_VERSION=${CARTESI_MACHINE_VERSION}" \ + --build-arg "CARTESI_MACHINE_DEB_SHA256=${DEB_SHA}" \ + -f watchdog/Dockerfile \ + -t "${image}" \ + . + mkdir -p dist + docker save "${image}" | gzip -9 > "dist/sequencer-watchdog-${TAG}-linux-${{ matrix.arch }}.tar.gz" + + - name: Upload artifact + uses: actions/upload-artifact@v6 + with: + name: watchdog-image-linux-${{ matrix.arch }} + path: dist/sequencer-watchdog-*.tar.gz + publish: name: Publish GitHub Release runs-on: ubuntu-latest needs: - build-sequencer - build-canonical-machine-image + - build-watchdog-image steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Load toolchain pins + uses: ./.github/actions/load-toolchain-pins + - name: Download build artifacts uses: actions/download-artifact@v6 with: path: dist - name: Flatten artifacts + env: + TAG: ${{ inputs.tag || github.ref_name }} run: | set -euo pipefail mkdir -p out find dist -type f -name '*.tar.gz' -exec cp -v '{}' out/ \; + bash scripts/generate-release-manifest.sh \ + --tag "${TAG}" \ + --git-sha "${GITHUB_SHA}" \ + --output "out/release-manifest-${TAG}.json" - name: Generate checksums working-directory: out run: | set -euo pipefail - sha256sum *.tar.gz > SHA256SUMS + sha256sum *.tar.gz *.json > SHA256SUMS - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: tag_name: ${{ inputs.tag || github.ref_name }} - prerelease: ${{ inputs.prerelease || true }} + prerelease: ${{ github.event_name == 'workflow_dispatch' && inputs.prerelease }} fail_on_unmatched_files: true files: | out/*.tar.gz + out/*.json out/SHA256SUMS diff --git a/.gitignore b/.gitignore index 0359111..d58ae5a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,24 @@ /target +.deps/ +watchdog-e2e-*/ .env .env.fish sequencer.db sequencer.db-shm sequencer.db-wal /out/ +examples/canonical-app/out/ /.DS_Store +.vscode/ soljson-latest.js **/states/ +__pycache__/ +/benchmarks/ +/.watchdog-cm-work/ + +# Local CM / demo scratch (not part of the repo) +/input-*-output-*.bin +docs/live-demo.md +tests/scripts/sepolia-demo.txt +tests/scripts/sepolia_accounts.txt +tests/scripts/tests/ diff --git a/AGENTS.md b/AGENTS.md index 41583d3..4be52e4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -343,4 +343,7 @@ Before finishing a change, ensure: - [`docs/threat-model/README.md`](docs/threat-model/README.md) — trust boundaries, in-scope and out-of-scope threats. - [`docs/recovery/README.md`](docs/recovery/README.md) — recovery design, TLA+ formal verification, design history. - [`docs/snapshots/`](docs/snapshots/) — app snapshots: [`format.md`](docs/snapshots/format.md) (dump trait + wire format) and [`lifecycle.md`](docs/snapshots/lifecycle.md) (take/promote/GC/lease design + crash-safety). +- [`docs/watchdog/operator-deployment.md`](docs/watchdog/operator-deployment.md) — production-like watchdog (Sepolia / mainnet; internal snapshot API). +- [`docs/watchdog/getting-started.md`](docs/watchdog/getting-started.md) — local dev: watchdog + `sequencer-devnet` on Anvil. +- [`docs/watchdog/README.md`](docs/watchdog/README.md) — watchdog architecture, compare vs advance modes, test commands. - [`sequencer-core/`](sequencer-core/) — shared domain types and protocol contracts. diff --git a/CLAUDE.md b/CLAUDE.md index 79065b2..aa9131e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -56,3 +56,5 @@ Rust edition 2024 / Axum API / SQLite (rusqlite, WAL) / EIP-712 signing / SSZ en - **[`docs/threat-model/README.md`](docs/threat-model/README.md)** — trust boundaries and in-scope threats. - **[`docs/recovery/README.md`](docs/recovery/README.md)** — preemptive recovery design + TLA+ proofs. - **[`docs/snapshots/lifecycle.md`](docs/snapshots/lifecycle.md)** — snapshot lifecycle design + invariants (take/promote/GC, crash-safety). Read before touching the inclusion lane's safe-frontier/snapshot path. +- **[`docs/watchdog/operator-deployment.md`](docs/watchdog/operator-deployment.md)** — watchdog on live L1 (Sepolia / mainnet, production-like). +- **[`docs/watchdog/getting-started.md`](docs/watchdog/getting-started.md)** — local dev: watchdog + `sequencer-devnet` on Anvil. diff --git a/Cargo.lock b/Cargo.lock index a524514..fad2b8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3568,7 +3568,10 @@ dependencies = [ "rollups-harness", "sequencer-core", "sequencer-rust-client", + "serde_json", + "tempfile", "tokio", + "tracing-subscriber", ] [[package]] diff --git a/README.md b/README.md index 78b1686..b45e736 100644 --- a/README.md +++ b/README.md @@ -189,6 +189,7 @@ released even on client disconnect. Related docs: - App snapshots (format + lifecycle): `docs/snapshots/` +- Watchdog — local dev: [`docs/watchdog/getting-started.md`](docs/watchdog/getting-started.md); Sepolia/mainnet: [`docs/watchdog/operator-deployment.md`](docs/watchdog/operator-deployment.md) ## Prototype Limits @@ -217,6 +218,9 @@ Some tests require [Foundry](https://getfoundry.sh) (`anvil` on PATH). They run - [`CLAUDE.md`](CLAUDE.md) — quick reference for shell setup and commands. - [`docs/threat-model/README.md`](docs/threat-model/README.md) — trust boundaries, in-scope and out-of-scope threats. - [`docs/recovery/README.md`](docs/recovery/README.md) — recovery design, TLA+ formal verification, design history. +- [`docs/watchdog/getting-started.md`](docs/watchdog/getting-started.md) — step-by-step: run the watchdog with a local sequencer. +- [`docs/watchdog/operator-deployment.md`](docs/watchdog/operator-deployment.md) — watchdog on live L1 (Sepolia staging, mainnet production). +- [`docs/watchdog/README.md`](docs/watchdog/README.md) — watchdog architecture, modules, and test commands. - [`sequencer-core/`](sequencer-core/) — shared domain types (`Application`, `SignedUserOp`, `Batch`, `Frame`). - [`examples/app-core/`](examples/app-core/) — placeholder wallet app implementing the `Application` trait. diff --git a/docs/watchdog/README.md b/docs/watchdog/README.md new file mode 100644 index 0000000..9559f75 --- /dev/null +++ b/docs/watchdog/README.md @@ -0,0 +1,306 @@ +# Watchdog + +The watchdog is an off-chain safety process that compares the sequencer's +**finalized SSZ state dump** against state produced by the canonical Cartesi +Machine at the same L1 inclusion block. + +## Documentation + +| Doc | Audience | +|-----|----------| +| **[`operator-deployment.md`](operator-deployment.md)** | **Production-like** — Sepolia and mainnet: internal snapshot API, live L1, checkpoints (Sepolia = mainnet dress rehearsal) | +| **[`getting-started.md`](getting-started.md)** | **Local dev only** — Anvil + `sequencer-devnet`, harness smoke, two-terminal flow | +| This file | Architecture, modules, runtime contract, checkpoints, test commands | +| [`staging-drills.md`](staging-drills.md) | Webhook smoke, synthetic alarms, staging compare daemon | +| [`sepolia.md`](sepolia.md) | Redirect → [`operator-deployment.md`](operator-deployment.md) | + +### Quick start (pick your environment) + +**Sepolia / mainnet (operator):** [`operator-deployment.md`](operator-deployment.md) — shared checklist, internal URL, Sepolia CM image, mainnet notes. + +**Local devnet:** + +One-time setup, then either a single automated check or an interactive run: + +```bash +just setup && just canonical-build-machine-image && just watchdog-lua-deps + +# Path A — full smoke (Anvil + sequencer + CM + compare), one command: +just test-watchdog-compare-harness + +# Path B — two terminals: stack prints WATCHDOG_* exports, then init + tick: +just devnet-for-watchdog # terminal 1 — leave running +# terminal 2: paste exports, then: +export WATCHDOG_LUA_ROOT="$(pwd)" +export WATCHDOG_LUA_BIN=lua +export WATCHDOG_LUA_DEPS=.deps/lua +./watchdog/sequencer-watchdog init +./watchdog/sequencer-watchdog tick +``` + +The `sequencer-watchdog` wrapper wraps `init`/`tick` with an advisory `flock` +on `$WATCHDOG_STATE_DIR/run.lock`. Production schedulers must also prevent +overlapping ticks +(`flock`, systemd, or Kubernetes `concurrencyPolicy: Forbid`). + +Details: **[`getting-started.md`](getting-started.md)**. + +## Host dependencies (`watchdog-lua-deps`) + +The watchdog cycle and any test that hits HTTP need a native **`lcurl.so`** built into `.deps/lua/`. JSON is pure Lua (no compile step). + +```bash +just watchdog-lua-deps # idempotent; writes .deps/lua/lcurl.so +export WATCHDOG_LUA_DEPS="$(pwd)/.deps/lua" +``` + +You also need **`cartesi-machine`** on `PATH` (in-process `cartesi` +Lua module), **`lua`** (5.4 recommended), and a scheduler non-overlap +guard. The release Docker image uses Linux `flock`; Nix also provides the +same CLI on macOS/Linux via `nixpkgs#util-linux`: + +```bash +nix shell nixpkgs#util-linux +``` + +### System packages + +| OS | Packages | +|----|----------| +| Debian / Ubuntu / WSL | `libcurl4-openssl-dev` `liblua5.4-dev` `lua5.4` `build-essential` `util-linux` | +| Fedora | `libcurl-devel` `lua-devel` `util-linux` | +| Arch | `curl` `lua` `util-linux` | + +Verify before building: + +```bash +pkg-config --exists libcurl && echo "libcurl ok" +test -f /usr/include/lua5.4/lua.h && echo "lua headers ok" +``` + +On Debian/Ubuntu, Lua headers live under **`/usr/include/lua5.4/`**, not `/usr/include/`. lua-cURLv3 is **vendored in-tree** under `watchdog/third_party/lua-curl/src`; `scripts/watchdog-lua-deps.sh` compiles it locally (no build-time download), discovering the Lua headers via `pkg-config` (override with `LUA_INC`). + +### Troubleshooting `just watchdog-lua-deps` + +| Message / error | Fix | +|-----------------|-----| +| `install libcurl dev package` | `sudo apt-get install -y libcurl4-openssl-dev` (or distro equivalent), then rerun `just watchdog-lua-deps` | +| `install Lua headers` | `sudo apt-get install -y liblua5.4-dev` | +| `fatal error: lua.h: No such file or directory` | Install `liblua5.4-dev`. If headers are present but build still fails, ensure you are on a tree where `scripts/watchdog-lua-deps.sh` passes **`LUA_INC`** (not `LUA_INCLUDE_DIR`) to make — see script in repo | +| `built lcurl.so but lua cannot load it` | Lua version mismatch: build with the same `lua` you run (`lua -v` vs headers under `lua5.4`) | + +CI runs **`just test-watchdog`** (mocked HTTP), the divergence drill script, and watchdog rollups-e2e trials (`watchdog_genesis_compare_test`, non-genesis compare inside `deposit_transfer_withdrawal_test`, `watchdog_non_genesis_divergence_test`) plus a **`watchdog-docker`** image smoke job. Run **`just doctor`** locally before CM-backed work. Full local smoke: `just test-watchdog-compare-harness`. + +## V1 Shape + +The implementation lives in `watchdog/` and is intentionally split into small +Lua modules: + +- `http.lua`: HTTP adapter via **lua-cURLv3** / `lcurl`, vendored in-tree and compiled by `just watchdog-lua-deps` (no build-time download). +- `json.lua` / `third_party/json.lua`: pure-Lua JSON (RPC + structured watchdog events). +- `jsonrpc.lua`: JSON-RPC request/response validation. +- `l1_reader.lua`: partitioned `eth_getLogs` scanning, strict L1 log ordering, + and chunk callbacks so each successful provider response can be consumed and + discarded. +- `abi.lua`: decoding for the `InputAdded` / `EvmAdvance` envelope. +- `machine_runner.lua`: CM driver (`load`, `advance`, `inspect`, `dump`). +- `machine_cartesi.lua`: in-process `cartesi` Lua module binding (production path). +- `sequencer_reader.lua`: sequencer HTTP client (`GET /finalized_state/inclusion_block`, `GET /finalized_state`). +- `compare.lua`: raw byte comparison. +- `checkpoint.lua`: manifest-backed checkpoint persistence (`head.json` pointer). +- `state.lua`: persisted `config.json` and single-run state lock. +- `retry.lua`: bounded retry helper used by the runtime. +- `runner.lua`: one compare cycle — cheap `/finalized_state/inclusion_block` + poll, then (when finalized advanced) L1 fetch, CM replay, SSZ compare, + checkpoint write. +- `main.lua`: dispatches `init` and `tick`; `tick` exits `0`/`1`/`2`. + +The L1 reader follows the Rust partition strategy from +`sequencer/src/l1/partition.rs`: if an RPC provider rejects a large range, the +range is split recursively and retried. Lua decodes and validates input +envelopes, but it does not classify payload tags. Direct input vs batch +submission remains scheduler logic inside the canonical machine. + +`l1_reader.lua` has the `InputAdded(address,uint256,bytes)` event topic baked in and +filters logs by `topic0 = InputAdded` and `topic1 = app address`, matching the +Rust reader's app-filtered InputBox scan. + +## Runtime Contract + +The sequencer exposes operator-internal snapshot routes (see `sequencer/src/egress/api/snapshot.rs`): + +- `GET /finalized_state/inclusion_block` — cheap JSON `{ inclusion_block, l2_tx_index }` polled every compare tick. +- `GET /finalized_state` — streams the finalized SSZ state file (`application/octet-stream`) with `X-Inclusion-Block` and `X-L2-Tx-Index` headers. + +**Idle optimization:** when `inclusion_block` has not advanced past the watchdog +checkpoint's `safe_block`, the tick returns +immediately — no `/finalized_state` download, no L1 `eth_getLogs`, no CM load/advance/inspect. + +The watchdog compares the finalized SSZ bytes with the bytes returned by CM +inspect. It must not canonicalize either side before deciding pass/fail. + +For the toy wallet app, SSZ encoding lives in `examples/app-core/src/wallet_snapshot.rs` +and is shared by `WalletApp::create_dump`, `Application::canonical_snapshot_bytes`, +and the canonical scheduler's `Inspect` handler (`examples/canonical-app`). + +## Checkpoints + +V1 persists only the resulting Cartesi Machine checkpoint, not the fetched L1 +inputs. + +```text +state_dir/ + config.json + head.json + run.lock # advisory lock handle; file existence is not lock state + checkpoints/ + 00000000000001234567/ + snapshot/ + manifest.json +``` + +`manifest.json` records `safe_block` (the L1 reference block the CM snapshot +covers — the finalized `inclusion_block`), timestamp, +and optionally the CM image hash. A new checkpoint directory is written first, +then `head.json` is atomically replaced to point at it. + +`init` stores the operator-provided bootstrap CM snapshot into this layout. `tick` +requires both `config.json` and `head.json`; it never bootstraps from env. +`WATCHDOG_L1_RPC_URL` is intentionally read at tick time, not persisted in +`config.json`, so operators can rotate RPC endpoints without rewriting watchdog +state. + +- `WATCHDOG_CM_SNAPSHOT_DIR` +- `WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK` + +## How it runs + +The watchdog has two subcommands: + +```bash +sequencer-watchdog init # one-time setup: writes config.json + head.json +sequencer-watchdog tick # one compare cycle; schedule this +``` + +`tick` does one cycle per process, then exits — infra schedules re-runs +(systemd timer / k8s CronJob) and reacts to the exit code. There is no daemon +loop. `sequencer-watchdog` takes a non-blocking `flock` for `init`/`tick`; +host scheduling should provide the same non-overlap guarantee. Each tick: + +1. Loads the watchdog checkpoint from `head.json`. +2. Polls `/finalized_state/inclusion_block`. If it has not advanced past a + watchdog checkpoint, exits `0` (idle). Otherwise: +3. Streams and decodes `InputAdded` logs for the new block range. +4. Replays each successful L1 partition into the in-process Cartesi Machine, + then inspects with query `state`. +5. Byte-compares the SSZ report against `GET /finalized_state`; on match writes a + new checkpoint, on mismatch emits a `watchdog_event` and exits `2`. + +Runtime knobs: + +- `WATCHDOG_L1_RPC_URL`: current L1 JSON-RPC endpoint for tick. +- `WATCHDOG_RETRY_ATTEMPTS`: bounded retry attempts per run, default `3`. +- `WATCHDOG_RETRY_DELAY_SEC`: delay between retry attempts, default `5`. + +## Local Tests + +| Command | What it exercises | +|---------|-------------------| +| `just test-watchdog` | Lua unit tests (fake HTTP/RPC/CM; no live chain) | +| `just test-watchdog-e2e` | Real CM: advance, inspect; optional live compare if `WATCHDOG_E2E_SEQUENCER_URL` set | +| `just test-watchdog-compare-harness` | **Full E2E**: Anvil + devnet sequencer + `/finalized_state` + CM inspect + Lua `init`/`tick` | +| `just test-rollups-e2e` | All rollups e2e scenarios; includes watchdog genesis/non-genesis compare plus `watchdog_non_genesis_divergence_test` (needs Sepolia CM image) | +| `just test-watchdog-divergence-drill` | Synthetic divergence signal drill (`watchdog_event` + exit `2`) | +| `just doctor` | Toolchain sanity: lua, cartesi-machine, lcurl, devnet CM image loadable via `machine_cartesi` | + +Prerequisites for CM-backed tests: see **[Host dependencies](#host-dependencies-watchdog-lua-deps)** above, then: + +```bash +just doctor # fail fast before long harness runs +just canonical-build-machine-image # once, if out/ image is missing +just canonical-build-machine-image-sepolia # rollups-e2e divergence trial (auto-built by test-rollups-e2e) +just watchdog-lua-deps +export WATCHDOG_LUA_DEPS="$(pwd)/.deps/lua" +``` + +### Lua unit tests + +```bash +just test-watchdog +``` + +Covers raw comparison, golden InputAdded ABI decoding, L1 ordering, recursive +range partitioning, streamed L1 chunks, config, checkpoints, the compare runner +(fakes), and retry behavior. + +### Lua CM end-to-end + +```bash +just test-watchdog-e2e +``` + +Scenarios (verbose `step NN/NN` logging): + +- `prerequisites` — `cartesi-machine` on PATH and machine image present. +- `cm-inspect-state-query` — real `--cmio-inspect-state` with query `state`. +- `machine-cartesi-store-reload-advance` — store checkpoint snapshot, reload, advance again (in-process binding). +- `compare-runner-with-sequencer` — skipped unless `WATCHDOG_E2E_SEQUENCER_URL` is set. + +Rebuild the machine image after changing the canonical scheduler/dapp. A stale +image makes `cm-inspect-state-query` skip with `inspect endpoint not implemented`. + +### Rust compare harness (most complete integration test) + +```bash +just test-watchdog-compare-harness +``` + +Spawns Anvil + rollups devnet + `sequencer-devnet`, proves CM inspect SSZ at +genesis matches `wallet_snapshot::encode(WalletConfig::devnet())` (same as +`tests/fixtures/wallet_snapshot_v1_empty.hex` only for Sepolia `default()`), then runs +`sequencer-watchdog init` and `sequencer-watchdog tick`. +When `inclusion_block` is unchanged at genesis, the runner skips L1/CM work (idle-cheap); +`deposit_transfer_withdrawal_test` drives a gold batch first so compare replays real L1 inputs. +**Before first run (or after changing scheduler / SSZ / inspect code):** + +```bash +just watchdog-lua-deps +just canonical-build-machine-image # not only ensure-machine-image — rebuild when the guest changed +just test-watchdog-compare-harness +``` + +`ensure-machine-image` only checks that `examples/canonical-app/out/canonical-machine-image` +exists; it does **not** detect a stale guest. If you pulled SSZ/inspect changes, rebuild the image. + +### Troubleshooting `just test-watchdog-compare-harness` + +| Symptom | Likely cause | Fix | +|---------|----------------|-----| +| `install libcurl dev package` / `lua.h: No such file` | Missing host deps for `lcurl.so` | [Host dependencies](#host-dependencies-watchdog-lua-deps) | +| `could not determine which binary to run` | `rollups-e2e` crate has two bins | Use the just recipe, or `cargo run -p rollups-e2e --bin rollups-e2e -- …` | +| `invalid utf-8` / timeout on step 1 (older trees) | Harness treated SSZ body as UTF-8 | Update `tests/e2e/src/watchdog_compare.rs` (current tree decodes binary + chunked bodies) | +| `finalized_state bytes mismatch (len 87 vs expected 76)` | Wrong golden (Sepolia fixture vs devnet sequencer) and/or raw HTTP chunked framing | Harness expects **devnet** SSZ; `lcurl` decodes chunked responses automatically | +| `CM inspect bytes mismatch (len 27 vs expected 76)` | **Stale CM image** still returns JSON `{"balances":{},"nonces":{}}` from pre-SSZ inspect | `just canonical-build-machine-image` then rerun harness | +| `inspect endpoint not implemented` | Older guest without inspect handler | Same rebuild as above | +| Harness passes step 1–2 but Lua compare fails | `WATCHDOG_LUA_DEPS` or checkpoint/bootstrap | Set `export WATCHDOG_LUA_DEPS="$(pwd)/.deps/lua"`; see [`getting-started.md`](getting-started.md) env table | + +Manual equivalent of the recipe: + +```bash +cargo run -p rollups-e2e --bin rollups-e2e -- \ + watchdog_genesis_compare_test --exact --nocapture +``` + +### Staging / operator drills + +See [`staging-drills.md`](staging-drills.md) for divergence signal and watchdog tick drills. + +## Related sequencer tests + +```bash +cargo test -p sequencer snapshot_endpoints -- --test-threads=1 +cargo test -p app-core wallet_snapshot -- --test-threads=1 +``` + +HTTP integration for snapshot routes lives in `sequencer/tests/snapshot_endpoints.rs`. +SSZ golden bytes for the toy wallet live in `tests/fixtures/wallet_snapshot_v1_empty.{hex,bin}`. diff --git a/docs/watchdog/design-notes.md b/docs/watchdog/design-notes.md new file mode 100644 index 0000000..a85ffc9 --- /dev/null +++ b/docs/watchdog/design-notes.md @@ -0,0 +1,114 @@ +# Watchdog Design Notes + +The watchdog is an independent off-chain safety monitor. It advances a +canonical Cartesi Machine from L1 inputs, inspects the resulting SSZ snapshot, +and byte-compares it with the sequencer's `GET /finalized_state` response at +the same finalized `inclusion_block`. + +Its value is independence: the sequencer serves state derived from its own +safe-acceptance simulation, while the watchdog re-derives the canonical +scheduler result from L1. + +## Current Shape + +The watchdog has one executable with two subcommands: + +```bash +sequencer-watchdog init +sequencer-watchdog tick +``` + +`init` records the watchdog's canonical starting state. `tick` runs one compare +cycle and exits; infra schedules `tick` with a timer or CronJob. Runtime +non-overlap is enforced by `sequencer-watchdog` with a kernel `flock`, and +Kubernetes/systemd deployments should use their native non-overlap guard. + +Each tick: + +1. Loads the watchdog checkpoint selected by `head.json`. +2. Reads `GET /finalized_state/inclusion_block`. +3. Exits cheaply if the finalized block is unchanged. +4. Fetches L1 `InputAdded` logs for the open block range. +5. Advances the CM, inspects state, fetches `GET /finalized_state`, and compares + raw SSZ bytes. +6. Writes a new checkpoint only after a successful compare. + +There is no advance-only mode. Advancing the CM is just an implementation step +inside a compare cycle. + +## Watchdog State + +The watchdog state is canonical from the watchdog's point of view. The +sequencer is what gets verified. + +`init` stores the operator-provided bootstrap CM snapshot into the normal +checkpoint layout. `tick` never bootstraps from env; missing `head.json` is an +operator error. + +`config.json` stores stable deployment identity (`sequencer_url`, +`input_box_address`, `app_address`, retry knobs). `WATCHDOG_L1_RPC_URL` is read +at tick time rather than persisted, because provider URLs and credentials are +operational inputs that may rotate. + +Tradeoff accepted: if the watchdog is initialized while the sequencer is already +serving an incorrect finalized state at the exact same block, and the block does +not advance before the next tick, the unchanged-block skip can delay detection +until a future finalized block. This keeps the runtime model simple. + +Unreadable, malformed, or incomplete watchdog state means stop and let the +operator repair the state directory. + +## Checkpoint Crash Model + +Checkpoint writes use a pointer-swap model: + +1. Store the CM snapshot under `checkpoints//snapshot`. +2. Write `manifest.json` next to it. +3. Write `head.json.tmp`. +4. Rename `head.json.tmp` over `head.json`. +5. Best-effort delete the superseded checkpoint. + +Crash before the rename leaves the previous checkpoint selected. Crash after +the rename leaves the new checkpoint selected and may leave an old directory to +clean up later. The code checks write and close errors for the JSON files, but +it does not currently fsync files or directories; if production needs +power-loss-grade durability, use a small SQLite state store or add explicit +fsync support. + +## State Layout + +```text +state/ + config.json + head.json + run.lock # advisory lock handle in the production container + checkpoints/ + 00000000000000000042/ + manifest.json + snapshot/ +``` + +`config.json` is written by `init` and read by every `tick`. `head.json` is the +small mutable pointer. Checkpoints are block-named directories; after a +successful pointer flip, the superseded checkpoint is pruned best-effort. + +## Memory Notes + +The L1 fetch path consumes successful provider partitions immediately: + +- JSON-RPC still reads one `eth_getLogs` response body at a time; +- each successful partition response is sorted locally; +- logs are decoded into an input chunk; +- the runner advances the CM for that partition and then discards the chunk. + +This preserves the operational assumption that one provider response fits in +memory, while avoiding whole-range `logs` plus whole-range decoded `inputs`. +The Cartesi binding may still queue one partition internally while feeding it to +the machine. + +## Open Questions Before Merge + +- Is the current crash model sufficient for a watchdog sidecar, or do operators + need fsync/SQLite durability? +- If provider responses themselves become too large, add provider pagination or + a smaller fixed scan window in a separate change. diff --git a/docs/watchdog/getting-started.md b/docs/watchdog/getting-started.md new file mode 100644 index 0000000..c297bd3 --- /dev/null +++ b/docs/watchdog/getting-started.md @@ -0,0 +1,208 @@ +# Watchdog + sequencer: local development + +Step-by-step guide for running the watchdog alongside a **local** `sequencer-devnet` stack (Anvil + ephemeral ports). Use this for CI smoke tests and debugging the watchdog itself. + +**Running on Sepolia or mainnet?** That follows the same operator model as production — internal snapshot URL, live L1, persistent checkpoints, chain-specific CM image. See **[`operator-deployment.md`](operator-deployment.md)** (Sepolia is the usual dress rehearsal before mainnet). + +- Architecture and module map: [`README.md`](README.md) +- **Sepolia / mainnet (production-like):** [`operator-deployment.md`](operator-deployment.md) +- Staging drills: [`staging-drills.md`](staging-drills.md) +- Implementation: [`watchdog/`](../../watchdog/) (Lua) + +## Contents + +1. [What you are running](#what-you-are-running) +2. [Prerequisites](#prerequisites) +3. [Path A — Full automated smoke](#path-a--full-automated-smoke-recommended-first) +4. [Path B — Interactive (two terminals)](#path-b--interactive-sequencer--watchdog-two-terminals) +5. [Production-like deployments](#production-like-deployments-sepolia--mainnet) +6. [Environment reference](#environment-reference) +7. [Troubleshooting](#troubleshooting) +8. [Related commands](#related-commands) + +--- + +## What you are running + +| Process | Role | +|---------|------| +| **Anvil** | Local L1 with Cartesi rollups contracts pre-deployed (`just setup`) | +| **sequencer-devnet** | Off-chain sequencer (wallet app, batches, snapshot promotion) | +| **watchdog** | Polls `/finalized_state/inclusion_block`, replays L1 inputs in CM, compares SSZ to `/finalized_state` | + +The sequencer exposes (operator-internal, same HTTP listener today): + +- `GET /finalized_state/inclusion_block` — cheap cursor poll +- `GET /finalized_state` — SSZ state file when compare runs + +--- + +## Prerequisites + +From the repo root: + +1. **Rust** — `cargo` (edition 2024 workspace). + +2. **Nix / direnv (recommended)** — Foundry `anvil`, Cartesi tools, and consistent Lua headers: + + ```bash + eval "$(direnv export bash 2>/dev/null)" + ``` + + Without direnv you need on `PATH`: `anvil`, `lua`, `cartesi-machine`, and a C compiler for `lcurl`. + +3. **System packages for watchdog HTTP + scheduling** — see [`README.md` — Host dependencies](README.md#host-dependencies-watchdog-lua-deps) (Debian/WSL: `libcurl4-openssl-dev`, `liblua5.4-dev`, `lua5.4`, `util-linux`, then `just watchdog-lua-deps`; Nix: `nixpkgs#util-linux` provides `flock`). + +4. **Cartesi Machine** — `cartesi-machine` on `PATH` so the in-process `cartesi` Lua module loads (ships with Cartesi Machine install / nix shell). + +5. **One-time repo setup**: + + ```bash + just setup # Anvil state + contract artifacts + just canonical-build-machine-image # CM image (~minutes, needs cross toolchain) + just watchdog-lua-deps # builds .deps/lua/lcurl.so + just doctor # lua + cartesi + lcurl + machine_cartesi load probe + ``` + +6. **Unit smoke (optional)**: + + ```bash + just test-watchdog + ``` + +--- + +## Path A — Full automated smoke (recommended first) + +Proves Anvil + devnet sequencer + CM inspect + Lua compare in one command: + +```bash +just test-watchdog-compare-harness +``` + +This builds `sequencer-devnet`, spawns the stack, waits for `GET /finalized_state`, compares genesis **devnet** SSZ to the CM inspect bytes, and runs one Lua compare pass. Expect exit code 0. + +**First time or after scheduler/SSZ changes:** run `just watchdog-lua-deps` and `just canonical-build-machine-image` before the harness (see [compare harness troubleshooting](README.md#troubleshooting-just-test-watchdog-compare-harness)). + +--- + +## Path B — Interactive: sequencer + watchdog (two terminals) + +### Terminal 1 — Devnet stack (Anvil + sequencer) + +```bash +just devnet-for-watchdog +``` + +This starts Anvil and `sequencer-devnet` on **ephemeral local ports** (not fixed 8545/3000) and prints a block of `export WATCHDOG_*=...` lines. **Copy those exports** into Terminal 2. + +Leave Terminal 1 running until you are done; Ctrl+C stops Anvil and the sequencer. + +### Wait for finalized snapshot + +The watchdog needs a **finalized** SSZ dump. Right after boot, the cheap endpoint may return **404** until the sequencer has promoted a snapshot. + +In another shell (use the printed `WATCHDOG_SEQUENCER_URL`): + +```bash +curl -s "$WATCHDOG_SEQUENCER_URL/finalized_state/inclusion_block" +``` + +When you see JSON like `{"inclusion_block":0,"l2_tx_index":0}` (numbers may differ), the watchdog can compare. If it stays 404 for a long time, check sequencer logs in `tests/e2e/results/` and that L1 is mining (devnet Anvil auto-mines by default). + +Optional — inspect SSZ size: + +```bash +curl -s -D - "$WATCHDOG_SEQUENCER_URL/finalized_state" -o /tmp/finalized-state.bin +head -c 32 /tmp/finalized-state.bin | xxd +``` + +### Terminal 2 — Watchdog + +From repo root, after `just watchdog-lua-deps`: + +```bash +# Paste exports from Terminal 1, then initialize once and run one tick: +export WATCHDOG_LUA_ROOT="$(pwd)" +export WATCHDOG_LUA_BIN=lua +export WATCHDOG_LUA_DEPS=.deps/lua +./watchdog/sequencer-watchdog init +./watchdog/sequencer-watchdog tick +``` + +Success: exit **0**. If finalized has advanced, stderr ends in `compare pass complete`; if it has not, the tick exits idle after the cheap poll. + +Exit codes from `sequencer-watchdog tick`: **0** clean (or idle — finalized unchanged), **1** transient failure (RPC/CM/network after retries), **2** deterministic divergence (`watchdog_event` emitted on stderr before exit). + +The watchdog tick runs **one cycle per process and exits** — re-run it on a timer/cron for continuous monitoring. When `inclusion_block` has not advanced since the watchdog checkpoint, the cycle **skips** L1/CM work (idle-cheap) and exits 0. +`sequencer-watchdog` takes a non-blocking `flock`; production schedulers should +also prevent overlapping ticks with systemd or Kubernetes CronJob +`concurrencyPolicy: Forbid`. + +--- + +## Production-like deployments (Sepolia / mainnet) + +Local paths A–B do **not** apply to public L1. There is no `just devnet-for-watchdog` on Sepolia or mainnet. + +| Local devnet | Sepolia / mainnet | +|--------------|-------------------| +| You spawn Anvil + `sequencer-devnet` | Sequencer already run by ops | +| `canonical-machine-image` (devnet guest) | `canonical-machine-image-sepolia` (today); mainnet guest when released | +| Snapshot HTTP on localhost | **Internal** operator network only | +| Genesis bootstrap (`safe_block=0`) usual | Bootstrap must match **current** finalized `inclusion_block` | + +**Sepolia is the dress rehearsal for mainnet** — same checklist, alarms, checkpoint volume, and firewall rules; only chain IDs, RPC URLs, and contract addresses change. + +Full operator runbook: **[`operator-deployment.md`](operator-deployment.md)**. + +--- + +## Environment reference + +| Variable | Required | Description | +|----------|----------|-------------| +| `WATCHDOG_SEQUENCER_URL` | yes | e.g. `http://127.0.0.1:54321` | +| `WATCHDOG_L1_RPC_URL` | tick | Current L1 JSON-RPC; not persisted by `init` | +| `WATCHDOG_INPUTBOX_ADDRESS` | yes | InputBox contract | +| `WATCHDOG_APP_ADDRESS` | yes | Rollup application contract | +| `WATCHDOG_STATE_DIR` | yes | Persistent watchdog state (`config.json`, `head.json`, checkpoints) | +| `WATCHDOG_CM_SNAPSHOT_DIR` | init | Genesis CM image dir | +| `WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK` | with above | Usually `0` on fresh devnet | +| `WATCHDOG_LUA_DEPS` | yes | `.deps/lua` after `just watchdog-lua-deps` | + +See `watchdog/config.lua` for the full list. + +--- + +## Troubleshooting + +| Symptom | What to check | +|---------|----------------| +| `install libcurl dev package` | Install `libcurl4-openssl-dev` (or distro equivalent); see [Host dependencies](README.md#host-dependencies-watchdog-lua-deps) | +| `lua.h: No such file or directory` when building lcurl | Install `liblua5.4-dev` (Debian/WSL), or set `LUA_INC` to your Lua headers directory (Homebrew/nix) before `just watchdog-lua-deps` | +| `lcurl` / `cURL` not found at runtime | Run `just watchdog-lua-deps`, set `WATCHDOG_LUA_DEPS=.deps/lua` | +| `cartesi Lua module is required` | Install Cartesi Machine; use nix/direnv shell; ensure `cartesi-machine` on `PATH` | +| `inspect endpoint not implemented` | Rebuild CM image: `just canonical-build-machine-image` | +| CM inspect ~27 bytes / JSON in error | Stale image (old JSON inspect); rebuild: `just canonical-build-machine-image` | +| HTTP 404 on `/finalized_state/inclusion_block` | Sequencer not promoted yet; wait or drive L1 + batches | +| `state_mismatch` at genesis | Wrong `WATCHDOG_CM_SNAPSHOT_*` or stale CM image vs sequencer build | +| `inclusion_block_regressed` | Watchdog state ahead of sequencer (reset state dir or fix bootstrap block) | +| `flock` lock conflict | Another tick is still running or the scheduler allows overlap. With the container `flock`, a leftover `run.lock` path alone is harmless. | +| `could not determine which binary to run` | Use `just test-watchdog-compare-harness` (not bare `cargo run -p rollups-e2e`) | +| Harness `87 vs 76` or `27 vs 76` byte mismatch | Stale CM image and/or wrong fixture; see [harness troubleshooting](README.md#troubleshooting-just-test-watchdog-compare-harness) | + +Full harness failure table: **[`README.md` — Troubleshooting compare harness](README.md#troubleshooting-just-test-watchdog-compare-harness)**. + +--- + +## Related commands + +```bash +just doctor # toolchain sanity before CM-backed tests +just test-watchdog # Lua unit tests (no live chain) +just test-watchdog-e2e # CM advance/inspect (optional live sequencer URL) +just test-watchdog-compare-harness # Full stack smoke +cargo test -p sequencer --test snapshot_endpoints +cargo test -p app-core wallet_snapshot +``` diff --git a/docs/watchdog/operator-deployment.md b/docs/watchdog/operator-deployment.md new file mode 100644 index 0000000..6116eed --- /dev/null +++ b/docs/watchdog/operator-deployment.md @@ -0,0 +1,273 @@ +# Watchdog — operator deployment (Sepolia and mainnet) + +This is the **production-like** runbook for running the watchdog next to a **live** sequencer on a public L1 (Sepolia today, mainnet when deployed). + +**Sepolia is the dress rehearsal for mainnet.** The watchdog process, compare algorithm, internal snapshot API, checkpoint model, and network boundaries are the same. What changes per chain is: L1 RPC URL, deployed contract addresses, CM machine image build, wallet portal/token constants, and poll cadence. + +For **local development only** (Anvil + `sequencer-devnet`, CI smoke tests), use [`getting-started.md`](getting-started.md) instead. + +## Two deployment tiers + +```text + ┌─────────────────────────────────────┐ + Internet / users │ Public ingress (POST /tx, WS) │ ← benchmarks, wallets + └─────────────────┬───────────────────┘ + │ + ┌─────────────────▼───────────────────┐ + Operator network │ Sequencer process │ + │ + internal snapshot HTTP │ ← watchdog ONLY here + │ /finalized_state* │ + └─────────┬───────────────┬─────────┘ + │ │ + ┌─────────▼───┐ ┌───────▼────────┐ + │ L1 (Sepolia │ │ Watchdog host │ + │ or mainnet)│ │ (compare) │ + └─────────────┘ └────────────────┘ +``` + +The watchdog never substitutes for the sequencer. It reads **finalized SSZ** the sequencer already committed and independently replays L1 through the canonical CM. + +--- + +## Shared operator checklist (Sepolia and mainnet) + +Use this checklist for any live deployment. Chain-specific values are in the tables below. + +### 1. Network access + +- [ ] Watchdog host can reach **internal** `WATCHDOG_SEQUENCER_URL` (not only the public `/tx` URL). +- [ ] Watchdog host can reach **L1 JSON-RPC** with `eth_getLogs` (archive recommended if replaying long history). +- [ ] `/finalized_state` is **not** exposed on the public internet. + +Verify snapshot API before CM bootstrap: + +```bash +curl -sS -o /dev/null -w "%{http_code}\n" "$WATCHDOG_SEQUENCER_URL/finalized_state/inclusion_block" +# expect 200 when a finalized snapshot exists (404 = not promoted yet or wrong host) +``` + +### 2. Watchdog runtime (release bundle or local build) + +**Production (recommended):** use the **release bundle** for tag `vX` — same +git tag as the sequencer binary and `canonical-machine-image-*-vX.tar.gz`. +Load `sequencer-watchdog-vX-linux-.tar.gz` (`docker load`) and verify +alignment via `release-manifest-vX.json` and `/opt/watchdog/RELEASE.json` +inside the image. Toolchain pins live in [`toolchain-pins.env`](../../toolchain-pins.env). + +`cartesi-machine` in the watchdog image **must** match +`CARTESI_MACHINE_VERSION` in [`toolchain-pins.env`](../../toolchain-pins.env) +(the emulator that built the CM image tarball). Mismatch causes load failures +or false `state_mismatch`. + +Release image quick run: + +```bash +docker load < sequencer-watchdog-vX-linux-amd64.tar.gz + +docker run --rm \ + -e WATCHDOG_SEQUENCER_URL="https://" \ + -e WATCHDOG_INPUTBOX_ADDRESS="0x..." \ + -e WATCHDOG_APP_ADDRESS="0x..." \ + -e WATCHDOG_STATE_DIR=/watchdog-state \ + -e WATCHDOG_CM_SNAPSHOT_DIR=/cm-bootstrap \ + -e WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK="" \ + -v /var/lib/watchdog/state:/watchdog-state \ + -v /var/lib/watchdog/cm-bootstrap:/cm-bootstrap:ro \ + sequencer-watchdog:vX init + +docker run --rm \ + -e WATCHDOG_L1_RPC_URL="https://" \ + -e WATCHDOG_STATE_DIR=/watchdog-state \ + -v /var/lib/watchdog/state:/watchdog-state \ + sequencer-watchdog:vX tick +``` + +**Local / dev build:** + +```bash +eval "$(direnv export bash 2>/dev/null)" +just watchdog-lua-deps # .deps/lua/lcurl.so — needs libcurl + Lua dev headers; see README +``` + +Host packages and build errors: [`README.md` — Host dependencies](README.md#host-dependencies-watchdog-lua-deps). + +Requires: `lua`, `cartesi-machine` (in-process `cartesi` Lua module), +libcurl + Lua headers, and a scheduler non-overlap guard. The release image +installs Linux `flock` from `util-linux`; for Nix shells, the package is +`nixpkgs#util-linux`. Pin `cartesi-machine` to the same version as your CM +bootstrap tarball. + +### 3. Build the CM image for **this chain** + +The RISC-V guest must use the same wallet/scheduler constants as the deployed app. + +| Chain | Command | Image directory | +|-------|---------|-----------------| +| **Sepolia** | `just canonical-build-machine-image-sepolia` | `examples/canonical-app/out/canonical-machine-image-sepolia` | +| **Local devnet** | `just canonical-build-machine-image` | `.../canonical-machine-image` (not for public L1) | +| **Mainnet** | *Ship a mainnet-targeted guest build when available* | Match production scheduler artifact | + +Today `WalletApp::default()` / `WalletConfig::sepolia()` align with Sepolia staging; mainnet production will need matching mainnet portal/token addresses in app-core before rebuilding the CM image. + +### 4. Collect deployment facts + +| Variable | Where it comes from | +|----------|---------------------| +| `WATCHDOG_SEQUENCER_URL` | Ops: internal HTTP base (see network diagram) | +| `WATCHDOG_L1_RPC_URL` | Ops: current chain RPC for `tick` (archive for historical `getLogs`; not persisted by `init`) | +| `WATCHDOG_APP_ADDRESS` | This rollup’s Cartesi **application** contract | +| `WATCHDOG_INPUTBOX_ADDRESS` | InputBox on that L1 ([Cartesi deployed contracts](https://docs.cartesi.io/cartesi-rollups/2.0/deployment/self-hosted.md)) | +| `WATCHDOG_STATE_DIR` | Persistent volume on watchdog host | +| `WATCHDOG_CM_SNAPSHOT_DIR` | Bootstrap CM snapshot (`init` only) | +| `WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK` | L1 block that bootstrap snapshot represents (= finalized `inclusion_block` at bootstrap) | +| `WATCHDOG_LUA_DEPS` | `.deps/lua` | + +The sequencer discovers and pins `input_box_address` at startup; use the same values as `SEQ_ETH_RPC_URL` / `SEQ_APP_ADDRESS` configuration. + +### 5. Initialize watchdog state (first run on a live chain) + +On a long-lived deployment, **`WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK=0` is usually wrong** unless finalized state is still at genesis. + +Pick one: + +1. **Ops hands off** a CM snapshot directory + block number matching current finalized `inclusion_block`, or +2. **Watchdog reuses** `WATCHDOG_STATE_DIR` from a prior run on this deployment, or +3. **Replay from genesis** (only for new rollups / low block height — slow). + +Run `init` once to store the bootstrap CM snapshot into the watchdog state +layout. `init` does not need `WATCHDOG_L1_RPC_URL`; the RPC URL is read by +each `tick` so it can rotate without editing state: + +```bash +sequencer-watchdog init +``` + +After init, schedule `tick`; tick will fail if `head.json` is missing. + +### 6. Run tick + +The watchdog runs **one tick per process, then exits** — there is no daemon +loop. Run it once as a smoke check, then schedule it (systemd timer / k8s +CronJob) and alert on the exit code: + +```bash +sequencer-watchdog tick # exit 0 = clean/idle, 1 = transient, 2 = divergence +``` + +`sequencer-watchdog` wraps `init` and `tick` with a non-blocking `flock` on +`$WATCHDOG_STATE_DIR/run.lock`, which is released by the kernel if the process +dies. Use the scheduler's non-overlap primitive as well (for example systemd or +Kubernetes CronJob `concurrencyPolicy: Forbid`). A leftover `run.lock` path is +only a lock handle; by itself it does not mean a lock is held. + +When `inclusion_block` ≤ the watchdog checkpoint, the runner only hits `/finalized_state/inclusion_block` and skips L1/CM work. + +--- + +## Sepolia (testnet staging) + +Use Sepolia to validate **the same procedure** you will run on mainnet: internal URLs, alarms, checkpoint persistence, RPC limits, CM image version pinning. + +### Sepolia-specific values + +| Item | Typical source | +|------|----------------| +| Chain ID | `11155111` | +| Public user ingress (tx demos only) | e.g. `https://eth-sepolia.rollups.cartesi.io/v2` — **may not** serve `/finalized_state` | +| Application instance | Per deployment (confirm with ops; demos have used `0x4CE633CA71071818cD73187765ee60F696dae083`) | +| InputBox (rollups v2.x on Sepolia) | Confirm on [deployed contracts](https://docs.cartesi.io/cartesi-rollups/2.0/deployment/self-hosted.md) (community examples use `0x58Df21fE097d4bE5dCf61e01d9ea3f6B81c2E1dB`) | +| CM image | `just canonical-build-machine-image-sepolia` | +| Tx / deposit demos | `tests/scripts/demo_sepolia.py` (copy `.env` locally; never commit secrets) | + +### Example env block (fill from ops) + +```bash +export WATCHDOG_SEQUENCER_URL="https://" +export WATCHDOG_L1_RPC_URL="https://" +export WATCHDOG_APP_ADDRESS="0x..." +export WATCHDOG_INPUTBOX_ADDRESS="0x..." +export WATCHDOG_STATE_DIR="/var/lib/watchdog/state-sepolia" +export WATCHDOG_CM_SNAPSHOT_DIR="/path/to/canonical-machine-image-sepolia" +export WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK="" +export WATCHDOG_LUA_DEPS="/path/to/sequencer/.deps/lua" +``` + +### Operating the Sepolia sequencer + +If your team runs the sequencer on Sepolia (not only the public endpoint): + +1. `sequencer` / release binary with Sepolia `SEQ_*` (chain id, app address, batch submitter key, L1 RPC). +2. Inclusion lane promotes finalized snapshots when L1 safe advances — required for `/finalized_state` 200. +3. Snapshot routes on an **internal** bind / port reachable by the watchdog host. +4. Sequencer binary built with **`WalletApp::new(WalletConfig::sepolia())`** (see `sequencer-devnet` vs production binary choice in your release pipeline). + +--- + +## Mainnet (production) + +When the rollup runs on Ethereum mainnet, **reuse the same operator checklist above**. Differences are operational scale, not watchdog logic: + +| Topic | Mainnet notes | +|-------|----------------| +| L1 RPC | Production-grade archive provider; rate limits matter for wide `getLogs` ranges | +| Contracts | Mainnet InputBox, application, portals from production deployment manifest | +| CM image | Build from production app/scheduler artifacts (mainnet wallet constants when defined in app-core) | +| Schedule cadence | A cron/timer interval of 300s+ is fine; finalized promotion follows mainnet safe head | +| Security | Stricter firewall between public ingress and internal snapshot tier; secrets management for RPC credentials | +| Bootstrap | Almost always ops-provided CM snapshot or continued state dir — not genesis replay | + +There is no `just devnet-for-watchdog` or automated harness on mainnet; treat Sepolia compare success as the gate before mainnet go-live. + +--- + +## Compare Cycle Behavior (All Live Chains) + +Same on Sepolia and mainnet: + +1. Load watchdog checkpoint from `head.json`. +2. `GET /finalized_state/inclusion_block` — if unchanged, **stop** (cheap). +3. If advanced: `eth_getLogs` on InputBox for `(last_block+1)..inclusion_block`. +4. Advance CM incrementally; `inspect` → SSZ bytes. +5. `GET /finalized_state` → SSZ bytes. +6. Raw compare; emit `watchdog_event` + non-zero exit on mismatch. +7. Write new CM checkpoint on success. + +Details: [`README.md`](README.md), [`docs/snapshots/lifecycle.md`](../snapshots/lifecycle.md). + +--- + +## Checkpoint disk usage and backups + +Each successful promotion stores a full CM snapshot under +`$WATCHDOG_STATE_DIR/checkpoints//`, and the watchdog **keeps only +the selected one** — after the atomic `head.json` flip it deletes the +checkpoint it superseded (crash-safe: `head.json` always names a complete +checkpoint). Local disk therefore stays bounded at a single snapshot; no +operator cleanup is required. + +For backups / rollback history, schedule the watchdog tick (it runs one cycle and +exits) and **after it exits** `aws s3 sync $WATCHDOG_STATE_DIR/checkpoints/ +s3://…` (without `--delete`). Because the process has exited there is no race +with its store or prune, and omitting `--delete` **accumulates a per-block +history in S3** while local disk stays at one snapshot. Restore feeds a chosen +snapshot back through the watchdog/sequencer recovery workflow. + +## Troubleshooting (live deployments) + +| Symptom | Likely cause | +|---------|----------------| +| `/finalized_state` missing on public URL | Wrong tier — use internal `WATCHDOG_SEQUENCER_URL` | +| `state_mismatch` | CM image / wallet constants ≠ sequencer build; or wrong bootstrap block | +| `inclusion_block_regressed` | Stale watchdog state vs sequencer finalized head | +| Slow or failing `getLogs` | RPC range limits — watchdog uses same partition strategy as sequencer | +| Transient `L1 RPC latest head lags target block` | Fallback RPC is behind the sequencer's finalized inclusion block; watchdog retries until the node has indexed through the target (avoids truncated `eth_getLogs` false mismatches) | +| `inspect endpoint not implemented` | Rebuild CM image for the correct chain target | +| Works on Sepolia, fails on mainnet | Different deployment addresses or different guest build — do not reuse Sepolia env verbatim | + +--- + +## Related + +- **Local dev / CI:** [`getting-started.md`](getting-started.md) +- **Architecture:** [`README.md`](README.md) +- **Webhooks:** [`staging-drills.md`](staging-drills.md) diff --git a/docs/watchdog/sepolia.md b/docs/watchdog/sepolia.md new file mode 100644 index 0000000..c3bf8fb --- /dev/null +++ b/docs/watchdog/sepolia.md @@ -0,0 +1,5 @@ +# Watchdog on Sepolia + +This page moved to **[`operator-deployment.md`](operator-deployment.md)**. + +Sepolia uses the same operator model as mainnet (internal snapshot API, live L1, per-deployment addresses, persistent checkpoints). That document is the production-like runbook; [`getting-started.md`](getting-started.md) remains for local Anvil + `sequencer-devnet` only. diff --git a/docs/watchdog/staging-drills.md b/docs/watchdog/staging-drills.md new file mode 100644 index 0000000..1db2288 --- /dev/null +++ b/docs/watchdog/staging-drills.md @@ -0,0 +1,98 @@ +# Watchdog Staging Drills + +Operator drills for divergence detection and watchdog tick verification. + +- **Sepolia / mainnet:** [`operator-deployment.md`](operator-deployment.md). **Local dev:** [`getting-started.md`](getting-started.md). +- Module map and local test recipes: [`README.md`](README.md). + +This document covers staging and manual verification beyond the devnet tutorial. + +## Prerequisites + +- **Release bundle (staging/production):** deploy `sequencer-watchdog-vX` and `canonical-machine-image-*-vX` from the same git tag; toolchain pins live in [`toolchain-pins.env`](../../toolchain-pins.env). +- Built canonical machine image: `just canonical-build-machine-image` +- `cartesi-machine`, `lua`, and `curl` on PATH +- `just watchdog-lua-deps` — builds `lcurl.so` into `.deps/lua` (libcurl + Lua headers on host) +- JSON is pure Lua (`watchdog/third_party/json.lua`); no cjson compile step +- Staging or local sequencer reachable at `WATCHDOG_SEQUENCER_URL` +- L1 RPC + InputBox + app addresses matching that deployment +- Log collection for `watchdog_event` lines and process exit codes + +## Drill 1 — Divergence signal (synthetic mismatch, no CM) + +Verifies the production `main.lua` divergence path (`watchdog_event` + exit code `2`) with +injected fake deps — no sequencer required. + +```bash +just watchdog-lua-deps +WATCHDOG_LUA_DEPS=.deps/lua lua watchdog/tests/drill_divergence.lua # exits 2 +# or: just test-watchdog-divergence-drill # wraps the drill; recipe exits 0 +``` + +Expected: `main.lua` emits a structured `watchdog_event` with `kind=state_mismatch` and +non-zero `mismatch_offset`, then the drill process exits with code `2`. + +Unit coverage: `just test-watchdog` (`runner returns state mismatch payload`). + +## Drill 2 — Happy compare (local Anvil harness) + +Full stack: Anvil + devnet rollups + sequencer + CM inspect + `GET /finalized_state`. + +```bash +just test-watchdog-compare-harness +# equivalent: +# just setup && just watchdog-lua-deps && just ensure-machine-image +# cargo build -p sequencer --bin sequencer-devnet -p rollups-e2e +# cargo run -p rollups-e2e --bin rollups-e2e -- watchdog_genesis_compare_test --exact +``` + +Or run the Lua compare pass manually after starting a devnet sequencer yourself: + +```bash +export WATCHDOG_SEQUENCER_URL=http://127.0.0.1: +export WATCHDOG_L1_RPC_URL=http://127.0.0.1:8545 +export WATCHDOG_INPUTBOX_ADDRESS= +export WATCHDOG_APP_ADDRESS= +export WATCHDOG_STATE_DIR=/tmp/watchdog-state +export WATCHDOG_CM_SNAPSHOT_DIR=examples/canonical-app/out/canonical-machine-image +export WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK=0 +export WATCHDOG_LUA_ROOT="$(pwd)" +export WATCHDOG_LUA_BIN=lua +export WATCHDOG_LUA_DEPS=.deps/lua +./watchdog/sequencer-watchdog init +./watchdog/sequencer-watchdog tick +``` + +Expected: exit **0**; the tick may exit idle if the finalized block is unchanged. +The harness path also proves byte-identical **devnet** genesis SSZ on sequencer `/finalized_state` and CM inspect +(same bytes as `wallet_snapshot::encode(WalletConfig::devnet())`; the `.hex` fixture +is for Sepolia `default()` — do not use it as the devnet golden). + +## Drill 3 — Production compare (scheduled) + +Run the watchdog against staging. Each tick runs one cycle and exits; schedule re-runs +with a systemd timer / cron and alert on the exit code. `sequencer-watchdog` +takes a non-blocking `flock`; production scheduling should also prevent +overlapping ticks with systemd, Kubernetes, or an equivalent scheduler guard: + +```bash +# ... all WATCHDOG_* vars from config.lua ... +sequencer-watchdog tick +``` + +Exit codes from `sequencer-watchdog tick`: + +| Code | Meaning | +|------|---------| +| `0` | Compare cycle completed — clean, or idle when finalized is unchanged | +| `1` | Transient error after retries (RPC, CM, network) | +| `2` | Deterministic divergence — `watchdog_event` on stderr with `{kind, previous_safe_block, sequencer_inclusion_block, mismatch_offset?}` | + +## Triage checklist + +| Symptom | Likely cause | +|---------|----------------| +| `inspect endpoint not implemented` | Stale CM image — rebuild | +| `state_mismatch` at genesis | Checkpoint not aligned with sequencer history | +| Compare skipped in Lua e2e | Set `WATCHDOG_E2E_SEQUENCER_URL` to a live sequencer | +| CM inspect 27 bytes / harness byte mismatch | Rebuild devnet image: `just canonical-build-machine-image` — see [`README.md`](README.md#troubleshooting-just-test-watchdog-compare-harness) | diff --git a/examples/app-core/src/application/mod.rs b/examples/app-core/src/application/mod.rs index 78d7cea..b1a9ee3 100644 --- a/examples/app-core/src/application/mod.rs +++ b/examples/app-core/src/application/mod.rs @@ -10,3 +10,7 @@ pub use anvil_accounts::default_private_keys; pub use method::{MAX_METHOD_PAYLOAD_BYTES, Method, Transfer, Withdrawal}; pub use notice::{DepositNotice, TransferNotice}; pub use wallet::{WalletApp, WalletConfig}; + +pub use crate::wallet_snapshot::{ + decode as decode_wallet_snapshot, encode as encode_wallet_snapshot, +}; diff --git a/examples/app-core/src/application/wallet.rs b/examples/app-core/src/application/wallet.rs index 845d9c7..90e1af5 100644 --- a/examples/app-core/src/application/wallet.rs +++ b/examples/app-core/src/application/wallet.rs @@ -6,8 +6,7 @@ use std::io::Write; use std::path::{Path, PathBuf}; use alloy_primitives::{Address, U256, address}; -use ssz::{Decode, Encode}; -use ssz_derive::{Decode as SszDecode, Encode as SszEncode}; +use ssz::Decode; use tracing::{error, warn}; use types::alloy_sol_types::SolCall; use types::{Erc20Deposit, Erc20Transfer}; @@ -51,7 +50,7 @@ impl Default for WalletConfig { } } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct WalletApp { config: WalletConfig, balances: HashMap, @@ -59,28 +58,6 @@ pub struct WalletApp { executed_input_count: u64, } -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode)] -struct SnapshotBalance { - address: [u8; 20], - balance_be: [u8; 32], -} - -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode)] -struct SnapshotNonce { - address: [u8; 20], - nonce: u32, -} - -#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode)] -struct WalletSnapshotV1 { - erc20_portal_address: [u8; 20], - supported_erc20_token: [u8; 20], - sequencer_address: [u8; 20], - balances: Vec, - nonces: Vec, - executed_input_count: u64, -} - pub const SEPOLIA_ERC20_PORTAL_ADDRESS: Address = address!("0xACA6586A0Cf05bD831f2501E7B4aea550dA6562D"); pub const SEPOLIA_USDC_ADDRESS: Address = address!("0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"); @@ -92,14 +69,54 @@ pub const DEVNET_SEQUENCER_ADDRESS: Address = address!("0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"); impl WalletApp { pub fn new(config: WalletConfig) -> Self { + Self::from_snapshot_parts(config, HashMap::new(), HashMap::new(), 0) + } + + pub(crate) fn from_snapshot_parts( + config: WalletConfig, + balances: HashMap, + nonces: HashMap, + executed_input_count: u64, + ) -> Self { Self { config, - balances: HashMap::new(), - nonces: HashMap::new(), - executed_input_count: 0, + balances, + nonces, + executed_input_count, } } + pub(crate) fn config(&self) -> &WalletConfig { + &self.config + } + + pub(crate) fn balances_iter(&self) -> impl Iterator { + self.balances.iter() + } + + pub(crate) fn nonces_iter(&self) -> impl Iterator { + self.nonces.iter() + } + + #[cfg(test)] + pub(crate) fn balances_mut(&mut self) -> &mut HashMap { + &mut self.balances + } + + #[cfg(test)] + pub(crate) fn nonces_mut(&mut self) -> &mut HashMap { + &mut self.nonces + } + + pub(crate) fn executed_input_count(&self) -> u64 { + self.executed_input_count + } + + #[cfg(test)] + pub(crate) fn set_executed_input_count(&mut self, count: u64) { + self.executed_input_count = count; + } + fn balance_of(&self, addr: &Address) -> U256 { *self.balances.get(addr).unwrap_or(&U256::ZERO) } @@ -138,94 +155,40 @@ impl WalletApp { Erc20Deposit::decode(&input.payload).map(Some) } - /// Serialize the wallet's logical state to deterministic SSZ bytes. - /// - /// Determinism is enforced by sorting `balances` and `nonces` by - /// address before encoding; `WalletApp` stores them in `HashMap`s - /// whose iteration order is non-deterministic. - fn snapshot_bytes(&self) -> Vec { + fn state_json(&self) -> String { let mut balances: Vec<_> = self .balances .iter() - .map(|(address, balance)| SnapshotBalance { - address: address.into_array(), - balance_be: balance.to_be_bytes(), - }) + .filter(|(_, balance)| **balance != U256::ZERO) .collect(); - balances.sort_unstable_by_key(|entry| entry.address); + balances.sort_by_key(|(address, _)| address.as_slice()); let mut nonces: Vec<_> = self .nonces .iter() - .map(|(address, nonce)| SnapshotNonce { - address: address.into_array(), - nonce: *nonce, - }) + .filter(|(_, nonce)| **nonce != 0) .collect(); - nonces.sort_unstable_by_key(|entry| entry.address); + nonces.sort_by_key(|(address, _)| address.as_slice()); - WalletSnapshotV1 { - erc20_portal_address: self.config.erc20_portal_address.into_array(), - supported_erc20_token: self.config.supported_erc20_token.into_array(), - sequencer_address: self.config.sequencer_address.into_array(), - balances, - nonces, - executed_input_count: self.executed_input_count, - } - .as_ssz_bytes() - } - - /// Decode SSZ-encoded snapshot bytes into a fresh `WalletApp`. - /// - /// Rejects snapshots containing duplicate addresses in `balances` or - /// `nonces`. Without this check, multiple distinct byte sequences - /// could decode to the same logical state (whichever entry came - /// last in the encoded list would win silently), breaking the - /// canonicality the watchdog relies on for byte-for-byte comparison. - fn from_snapshot_bytes(bytes: &[u8]) -> Result { - let decoded = WalletSnapshotV1::from_ssz_bytes(bytes).map_err(|e| AppError::Internal { - // ssz::DecodeError doesn't implement Display, so Debug is the - // only way to surface variant info as a string. If downstream - // needs to match on decode kinds programmatically, introduce a - // dedicated AppError variant that carries the typed error - // instead of fixing the format string. - reason: format!("snapshot decode failed: {e:?}"), - })?; - - let mut balances = HashMap::with_capacity(decoded.balances.len()); - for entry in decoded.balances { - let address = Address::from(entry.address); - let balance = U256::from_be_bytes(entry.balance_be); - if balances.insert(address, balance).is_some() { - return Err(AppError::Internal { - reason: format!("snapshot contains duplicate balance entry for {address}"), - }); - } - } - - let mut nonces = HashMap::with_capacity(decoded.nonces.len()); - for entry in decoded.nonces { - let address = Address::from(entry.address); - if nonces.insert(address, entry.nonce).is_some() { - return Err(AppError::Internal { - reason: format!("snapshot contains duplicate nonce entry for {address}"), - }); - } - } + let balance_entries = balances + .into_iter() + .map(|(address, balance)| format!("\"{}\":\"{balance}\"", json_address(address))) + .collect::>() + .join(","); + let nonce_entries = nonces + .into_iter() + .map(|(address, nonce)| format!("\"{}\":{nonce}", json_address(address))) + .collect::>() + .join(","); - Ok(Self { - config: WalletConfig { - erc20_portal_address: Address::from(decoded.erc20_portal_address), - supported_erc20_token: Address::from(decoded.supported_erc20_token), - sequencer_address: Address::from(decoded.sequencer_address), - }, - balances, - nonces, - executed_input_count: decoded.executed_input_count, - }) + format!("{{\"balances\":{{{balance_entries}}},\"nonces\":{{{nonce_entries}}}}}") } } +fn json_address(address: &Address) -> String { + format!("0x{}", alloy_primitives::hex::encode(address.as_slice())) +} + impl Default for WalletApp { fn default() -> Self { Self::new(WalletConfig::default()) @@ -369,7 +332,11 @@ impl Application for WalletApp { fn from_dump(prefix: &Path) -> Result { let state_path = Self::state_file_in_dump(prefix); let bytes = std::fs::read(&state_path)?; - Self::from_snapshot_bytes(&bytes) + crate::wallet_snapshot::decode(&bytes) + } + + fn canonical_snapshot_bytes(&self) -> Result, AppError> { + Ok(crate::wallet_snapshot::encode(self)) } fn create_dump(&self, prefix: &Path) -> Result<(), AppError> { @@ -378,7 +345,7 @@ impl Application for WalletApp { // unique per call; a collision means a lane bug worth surfacing // loudly rather than silently overwriting prior state. std::fs::create_dir(prefix)?; - let bytes = self.snapshot_bytes(); + let bytes = crate::wallet_snapshot::encode(self); let state_path = Self::state_file_in_dump(prefix); let mut file = std::fs::File::create(&state_path)?; @@ -407,6 +374,10 @@ impl Application for WalletApp { fn state_file_in_dump(prefix: &Path) -> PathBuf { prefix.join("state") } + + fn export_state(&self) -> Result { + Ok(self.state_json()) + } } #[cfg(test)] @@ -414,8 +385,10 @@ mod tests { use std::path::PathBuf; use std::time::{SystemTime, UNIX_EPOCH}; + use crate::wallet_snapshot::{SnapshotBalance, SnapshotNonce, WalletSnapshotV1}; use alloy_primitives::{Address, U256, address}; - use ssz_derive::{Decode, Encode}; + use ssz::Encode as SszEncodeTrait; + use ssz_derive::{Decode as SszDecode, Encode as SszEncode}; use types::ERC20_DEPOSIT_PREFIX_BYTES; use types::Erc20Transfer; use types::alloy_sol_types::SolCall; @@ -486,24 +459,24 @@ mod tests { assert_eq!(app.current_user_balance(recipient), U256::ZERO); } - #[derive(PartialEq, Debug, Encode, Decode, Clone)] + #[derive(PartialEq, Debug, SszEncode, SszDecode, Clone)] struct LegacyDeposit { amount: U256, to: Address, } - #[derive(PartialEq, Debug, Encode, Decode, Clone)] + #[derive(PartialEq, Debug, SszEncode, SszDecode, Clone)] struct LegacyWithdrawal { amount: U256, } - #[derive(PartialEq, Debug, Encode, Decode, Clone)] + #[derive(PartialEq, Debug, SszEncode, SszDecode, Clone)] struct LegacyTransfer { amount: U256, to: Address, } - #[derive(PartialEq, Debug, Encode, Decode, Clone)] + #[derive(PartialEq, Debug, SszEncode, SszDecode, Clone)] #[ssz(enum_behaviour = "union")] enum LegacyMethod { Withdrawal(LegacyWithdrawal), @@ -529,7 +502,7 @@ mod tests { let valid = ValidUserOp { sender, fee: 0, - data: ssz::Encode::as_ssz_bytes(&legacy), + data: SszEncodeTrait::as_ssz_bytes(&legacy), }; let outputs = app @@ -842,6 +815,16 @@ mod tests { assert_eq!(restored.executed_input_count, app.executed_input_count); } + #[test] + fn create_dump_state_file_matches_canonical_encode() { + let app = WalletApp::new(WalletConfig::default()); + let prefix = temp_dump_prefix(); + app.create_dump(&prefix).expect("create dump"); + let on_disk = std::fs::read(WalletApp::state_file_in_dump(&prefix)).expect("read state"); + WalletApp::delete_dump(&prefix).expect("cleanup"); + assert_eq!(on_disk, crate::wallet_snapshot::encode(&app)); + } + #[test] fn create_dump_produces_deterministic_bytes() { // Insert in scrambled order so the HashMap's non-deterministic @@ -909,16 +892,16 @@ mod tests { // The encoder never produces this, but the decoder must reject it // to keep snapshot bytes canonical: without this check, multiple // distinct byte sequences could decode to the same logical state. - let snapshot = super::WalletSnapshotV1 { + let snapshot = WalletSnapshotV1 { erc20_portal_address: [0; 20], supported_erc20_token: [0; 20], sequencer_address: [0; 20], balances: vec![ - super::SnapshotBalance { + SnapshotBalance { address: [1; 20], balance_be: [0; 32], }, - super::SnapshotBalance { + SnapshotBalance { address: [1; 20], balance_be: [0; 32], }, @@ -929,7 +912,7 @@ mod tests { let bytes = ssz::Encode::as_ssz_bytes(&snapshot); let err = - WalletApp::from_snapshot_bytes(&bytes).expect_err("duplicate balance should reject"); + crate::wallet_snapshot::decode(&bytes).expect_err("duplicate balance should reject"); match err { AppError::Internal { reason } => assert!( reason.contains("duplicate balance"), @@ -941,17 +924,17 @@ mod tests { #[test] fn from_snapshot_bytes_rejects_duplicate_nonce_addresses() { - let snapshot = super::WalletSnapshotV1 { + let snapshot = WalletSnapshotV1 { erc20_portal_address: [0; 20], supported_erc20_token: [0; 20], sequencer_address: [0; 20], balances: vec![], nonces: vec![ - super::SnapshotNonce { + SnapshotNonce { address: [1; 20], nonce: 0, }, - super::SnapshotNonce { + SnapshotNonce { address: [1; 20], nonce: 1, }, @@ -961,7 +944,7 @@ mod tests { let bytes = ssz::Encode::as_ssz_bytes(&snapshot); let err = - WalletApp::from_snapshot_bytes(&bytes).expect_err("duplicate nonce should reject"); + crate::wallet_snapshot::decode(&bytes).expect_err("duplicate nonce should reject"); match err { AppError::Internal { reason } => assert!( reason.contains("duplicate nonce"), @@ -994,4 +977,30 @@ mod tests { path.push(format!("wallet-dump-{}-{nanos}-{n}", std::process::id())); path } + + #[test] + fn export_state_is_deterministic_and_omits_defaults() { + let mut app = WalletApp::new(WalletConfig::default()); + let high = address!("0xffffffffffffffffffffffffffffffffffffffff"); + let low = address!("0x1111111111111111111111111111111111111111"); + app.balances.insert(high, U256::from(20_u64)); + app.balances.insert(low, U256::from(10_u64)); + app.balances.insert(Address::ZERO, U256::ZERO); + app.nonces.insert(high, 2); + app.nonces.insert(low, 1); + app.nonces.insert(Address::ZERO, 0); + + assert_eq!( + app.export_state().expect("export state"), + concat!( + "{\"balances\":{", + "\"0x1111111111111111111111111111111111111111\":\"10\",", + "\"0xffffffffffffffffffffffffffffffffffffffff\":\"20\"", + "},\"nonces\":{", + "\"0x1111111111111111111111111111111111111111\":1,", + "\"0xffffffffffffffffffffffffffffffffffffffff\":2", + "}}" + ) + ); + } } diff --git a/examples/app-core/src/lib.rs b/examples/app-core/src/lib.rs index 90eb57f..7082663 100644 --- a/examples/app-core/src/lib.rs +++ b/examples/app-core/src/lib.rs @@ -2,3 +2,4 @@ // SPDX-License-Identifier: Apache-2.0 (see LICENSE) pub mod application; +pub mod wallet_snapshot; diff --git a/examples/app-core/src/wallet_snapshot.rs b/examples/app-core/src/wallet_snapshot.rs new file mode 100644 index 0000000..4ec96f3 --- /dev/null +++ b/examples/app-core/src/wallet_snapshot.rs @@ -0,0 +1,160 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +//! Canonical SSZ snapshot encoding for the toy wallet app. +//! +//! [`encode`] and [`decode`] are the single source of truth used by +//! [`WalletApp::create_dump`](crate::application::wallet::WalletApp::create_dump), +//! CM `inspect_state`, and the watchdog's `/finalized_state` byte compare. +//! +//! Golden bytes: `tests/fixtures/wallet_snapshot_v1_empty.{hex,bin}` (shared with +//! Rust and Lua parity tests). + +use std::collections::HashMap; + +use alloy_primitives::{Address, U256}; +use ssz::{Decode, Encode}; +use ssz_derive::{Decode as SszDecode, Encode as SszEncode}; + +use crate::application::{WalletApp, WalletConfig}; +use sequencer_core::application::AppError; + +#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode)] +pub struct SnapshotBalance { + pub address: [u8; 20], + pub balance_be: [u8; 32], +} + +#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode)] +pub struct SnapshotNonce { + pub address: [u8; 20], + pub nonce: u32, +} + +#[derive(Debug, Clone, PartialEq, Eq, SszEncode, SszDecode)] +pub struct WalletSnapshotV1 { + pub erc20_portal_address: [u8; 20], + pub supported_erc20_token: [u8; 20], + pub sequencer_address: [u8; 20], + pub balances: Vec, + pub nonces: Vec, + pub executed_input_count: u64, +} + +/// Deterministic SSZ bytes for `app`'s logical state (sorted map entries). +pub fn encode(app: &WalletApp) -> Vec { + let mut balances: Vec<_> = app + .balances_iter() + .map(|(address, balance)| SnapshotBalance { + address: address.into_array(), + balance_be: balance.to_be_bytes(), + }) + .collect(); + balances.sort_unstable_by_key(|entry| entry.address); + + let mut nonces: Vec<_> = app + .nonces_iter() + .map(|(address, nonce)| SnapshotNonce { + address: address.into_array(), + nonce: *nonce, + }) + .collect(); + nonces.sort_unstable_by_key(|entry| entry.address); + + WalletSnapshotV1 { + erc20_portal_address: app.config().erc20_portal_address.into_array(), + supported_erc20_token: app.config().supported_erc20_token.into_array(), + sequencer_address: app.config().sequencer_address.into_array(), + balances, + nonces, + executed_input_count: app.executed_input_count(), + } + .as_ssz_bytes() +} + +/// Rehydrate a [`WalletApp`] from SSZ snapshot bytes. +pub fn decode(bytes: &[u8]) -> Result { + let decoded = WalletSnapshotV1::from_ssz_bytes(bytes).map_err(|e| AppError::Internal { + reason: format!("snapshot decode failed: {e:?}"), + })?; + + let mut balances = HashMap::with_capacity(decoded.balances.len()); + for entry in decoded.balances { + let address = Address::from(entry.address); + let balance = U256::from_be_bytes(entry.balance_be); + if balances.insert(address, balance).is_some() { + return Err(AppError::Internal { + reason: format!("snapshot contains duplicate balance entry for {address}"), + }); + } + } + + let mut nonces = HashMap::with_capacity(decoded.nonces.len()); + for entry in decoded.nonces { + let address = Address::from(entry.address); + if nonces.insert(address, entry.nonce).is_some() { + return Err(AppError::Internal { + reason: format!("snapshot contains duplicate nonce entry for {address}"), + }); + } + } + + Ok(WalletApp::from_snapshot_parts( + WalletConfig { + erc20_portal_address: Address::from(decoded.erc20_portal_address), + supported_erc20_token: Address::from(decoded.supported_erc20_token), + sequencer_address: Address::from(decoded.sequencer_address), + }, + balances, + nonces, + decoded.executed_input_count, + )) +} + +#[cfg(test)] +mod tests { + use alloy_primitives::address; + + use super::*; + + fn fixture_path(name: &str) -> std::path::PathBuf { + std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../tests/fixtures") + .join(name) + } + + fn read_fixture_hex(name: &str) -> Vec { + let hex = std::fs::read_to_string(fixture_path(name)) + .unwrap_or_else(|e| panic!("read fixture {name}: {e}")); + let hex = hex.trim().replace(['\n', ' '], ""); + (0..hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).expect("hex nibble")) + .collect() + } + + #[test] + fn encode_default_wallet_matches_golden_vector() { + let app = WalletApp::new(WalletConfig::default()); + assert_eq!( + encode(&app), + read_fixture_hex("wallet_snapshot_v1_empty.hex") + ); + } + + #[test] + fn encode_round_trips_through_decode() { + let mut app = WalletApp::new(WalletConfig::default()); + app.balances_mut().insert( + address!("0x1111111111111111111111111111111111111111"), + U256::from(10_u64), + ); + app.nonces_mut() + .insert(address!("0x1111111111111111111111111111111111111111"), 1); + app.set_executed_input_count(3); + + let bytes = encode(&app); + let restored = decode(&bytes).expect("decode snapshot"); + assert_eq!(encode(&restored), bytes); + } +} diff --git a/examples/canonical-app/justfile b/examples/canonical-app/justfile index f3eb915..5c5ecb6 100644 --- a/examples/canonical-app/justfile +++ b/examples/canonical-app/justfile @@ -35,13 +35,13 @@ build-dapp: build-dapp-devnet build-dapp-devnet: mkdir -p {{out_dir}} - SOURCE_DATE_EPOCH={{source_date_epoch}} CARGO_PROFILE_RELEASE_STRIP=symbols CROSS_CONFIG=Cross.toml DOCKER_DEFAULT_PLATFORM=linux/amd64 cross build --package canonical-app --bin canonical-app-devnet --target riscv64gc-unknown-linux-musl --release + SOURCE_DATE_EPOCH={{source_date_epoch}} CARGO_PROFILE_RELEASE_STRIP=symbols CARGO_TARGET_DIR=../../target CROSS_CONFIG=Cross.toml DOCKER_DEFAULT_PLATFORM=linux/amd64 cross build --package canonical-app --bin canonical-app-devnet --target riscv64gc-unknown-linux-musl --release cp ../../target/riscv64gc-unknown-linux-musl/release/canonical-app-devnet {{dapp_binary_devnet}} cp {{dapp_binary_devnet}} {{dapp_binary}} build-dapp-sepolia: mkdir -p {{out_dir}} - SOURCE_DATE_EPOCH={{source_date_epoch}} CARGO_PROFILE_RELEASE_STRIP=symbols CROSS_CONFIG=Cross.toml DOCKER_DEFAULT_PLATFORM=linux/amd64 cross build --package canonical-app --bin canonical-app-sepolia --target riscv64gc-unknown-linux-musl --release + SOURCE_DATE_EPOCH={{source_date_epoch}} CARGO_PROFILE_RELEASE_STRIP=symbols CARGO_TARGET_DIR=../../target CROSS_CONFIG=Cross.toml DOCKER_DEFAULT_PLATFORM=linux/amd64 cross build --package canonical-app --bin canonical-app-sepolia --target riscv64gc-unknown-linux-musl --release cp ../../target/riscv64gc-unknown-linux-musl/release/canonical-app-sepolia {{dapp_binary_sepolia}} cp {{dapp_binary_sepolia}} {{dapp_binary}} diff --git a/examples/canonical-app/src/scheduler/core.rs b/examples/canonical-app/src/scheduler/core.rs index 7ea6e21..07c641f 100644 --- a/examples/canonical-app/src/scheduler/core.rs +++ b/examples/canonical-app/src/scheduler/core.rs @@ -87,6 +87,15 @@ impl PartialEq for ProcessOutcome { } } +/// Inspect query accepted by the scheduler's state export endpoint. +pub const STATE_INSPECT_QUERY: &[u8] = b"state"; + +#[derive(Debug, PartialEq, Eq)] +pub(super) enum InspectError { + UnsupportedQuery, + Application(String), +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(super) enum BatchRejectReason { DecodeFailed, @@ -130,6 +139,16 @@ impl Scheduler { self.next_expected_batch_nonce } + pub(super) fn inspect_state(&self, query: &[u8]) -> Result, InspectError> { + if !query.is_empty() && query != STATE_INSPECT_QUERY { + return Err(InspectError::UnsupportedQuery); + } + + self.app + .canonical_snapshot_bytes() + .map_err(|err| InspectError::Application(err.to_string())) + } + pub(super) fn process_input(&mut self, input: SchedulerInput) -> ProcessResult { // Execute overdue directs before any input to keep backstop semantics explicit. let mut outputs = Vec::new(); @@ -483,6 +502,12 @@ mod tests { fn state_file_in_dump(_prefix: &std::path::Path) -> std::path::PathBuf { unimplemented!("RecordingApp does not participate in snapshot lifecycle") } + + fn canonical_snapshot_bytes( + &self, + ) -> Result, sequencer_core::application::AppError> { + Ok(format!("events:{}", self.executed.len()).into_bytes()) + } } const SEQUENCER: Address = address!("0x1111111111111111111111111111111111111111"); @@ -1006,6 +1031,60 @@ mod tests { assert_eq!(scheduler.app.events(), [RecordedTx::UserOp(9)]); } + #[test] + fn inspect_exports_application_state_for_state_query() { + let mut scheduler = Scheduler::new( + RecordingApp::default(), + SchedulerConfig { + sequencer_address: SEQUENCER, + max_wait_blocks: 100, + }, + ); + assert_eq!( + scheduler.process_input(direct_input(1, 7)), + ProcessOutcome::DirectEnqueued + ); + // Inspect reflects executed app state, not the direct-input queue. + let state = scheduler + .inspect_state(STATE_INSPECT_QUERY) + .expect("inspect state"); + assert_eq!(state, b"events:0"); + + let batch = Batch { + nonce: 0, + frames: vec![Frame { + user_ops: vec![], + safe_block: 1, + fee_price: 0, + }], + }; + assert_eq!( + scheduler.process_input(batch_input(2, batch)), + ProcessOutcome::BatchExecuted + ); + + let state = scheduler + .inspect_state(STATE_INSPECT_QUERY) + .expect("inspect state after drain"); + assert_eq!(state, b"events:1"); + } + + #[test] + fn inspect_rejects_unsupported_query() { + let scheduler = Scheduler::new( + RecordingApp::default(), + SchedulerConfig { + sequencer_address: SEQUENCER, + max_wait_blocks: 100, + }, + ); + + assert_eq!( + scheduler.inspect_state(b"balances"), + Err(InspectError::UnsupportedQuery) + ); + } + #[test] fn wrong_batch_nonce_is_rejected_without_consuming_nonce() { let mut scheduler = Scheduler::new( diff --git a/examples/canonical-app/src/scheduler/mod.rs b/examples/canonical-app/src/scheduler/mod.rs index 8cbc12d..cd12f41 100644 --- a/examples/canonical-app/src/scheduler/mod.rs +++ b/examples/canonical-app/src/scheduler/mod.rs @@ -3,7 +3,9 @@ mod core; -pub use core::{DEVNET_SEQUENCER_ADDRESS, SEPOLIA_SEQUENCER_ADDRESS, SchedulerConfig}; +pub use core::{ + DEVNET_SEQUENCER_ADDRESS, SEPOLIA_SEQUENCER_ADDRESS, STATE_INSPECT_QUERY, SchedulerConfig, +}; use sequencer_core::application::AppOutput; use sequencer_core::application::Application; @@ -46,9 +48,21 @@ pub fn run_scheduler_forever( }); } } - Ok(RollupRequest::Inspect { .. }) => { + Ok(RollupRequest::Inspect { payload }) => { + // Inspect is a public, read-only query endpoint: an unknown query + // (or a state-encode error) must not halt the guest. Emit a + // structured error report and keep serving, as it did before. + let report = match scheduler.inspect_state(&payload) { + Ok(bytes) => bytes, + Err(core::InspectError::UnsupportedQuery) => { + b"unsupported inspect query".to_vec() + } + Err(core::InspectError::Application(reason)) => { + format!("inspect failed: {reason}").into_bytes() + } + }; rollup - .emit_report(b"scheduler inspect endpoint not implemented") + .emit_report(&report) .unwrap_or_else(|err| panic!("scheduler failed to emit inspect report: {err}")); } Err(err) => panic!("scheduler failed while reading next input: {err}"), @@ -157,6 +171,74 @@ mod tests { } } + #[test] + fn run_scheduler_emits_exported_state_for_state_inspect() { + let inspect = RollupRequest::Inspect { + payload: STATE_INSPECT_QUERY.to_vec(), + }; + let terminal_err = Err(RollupError::CmtCallFailed { + operation: "next_input", + code: -22, + }); + let (rollup, reports) = MockRollup::with_inputs(vec![Ok(inspect), terminal_err]); + let expected = app_core::wallet_snapshot::encode(&WalletApp::new(WalletConfig::default())); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + run_scheduler_forever( + rollup, + WalletApp::new(WalletConfig::default()), + SchedulerConfig::default(), + ) + })); + + assert!( + result.is_err(), + "scheduler loop should panic on rollup error" + ); + let reports = reports.lock().expect("poisoned reports mutex"); + assert!( + reports + .iter() + .any(|report| report.as_slice() == expected.as_slice()), + "missing state inspect report, got: {reports:?}" + ); + } + + #[test] + fn run_scheduler_reports_unsupported_inspect_query_without_panicking() { + // A non-"state" inspect payload must produce a graceful report, not a + // guest panic. The loop should survive the inspect and only panic when + // it later hits the terminal rollup error. + let inspect = RollupRequest::Inspect { + payload: b"balances".to_vec(), + }; + let terminal_err = Err(RollupError::CmtCallFailed { + operation: "next_input", + code: -22, + }); + let (rollup, reports) = MockRollup::with_inputs(vec![Ok(inspect), terminal_err]); + + let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + run_scheduler_forever( + rollup, + WalletApp::new(WalletConfig::default()), + SchedulerConfig::default(), + ) + })); + + assert!( + result.is_err(), + "scheduler loop should panic only on the terminal rollup error" + ); + let reports = reports.lock().expect("poisoned reports mutex"); + assert!( + reports + .iter() + .any(|report| report.as_slice() == b"unsupported inspect query"), + "expected a graceful unsupported-query report, got: {reports:?}" + ); + } + #[test] fn run_scheduler_emits_report_for_invalid_batch_before_rollup_error() { let sequencer = SchedulerConfig::default().sequencer_address; diff --git a/justfile b/justfile index 61dfda5..db67d9a 100644 --- a/justfile +++ b/justfile @@ -12,6 +12,30 @@ check-all-targets: test: cargo test --workspace +test-watchdog: + just -f watchdog/justfile test + +test-watchdog-e2e: + just -f watchdog/justfile test-e2e + +# Verify divergence signal via main.lua (drill exits 2 like production). +test-watchdog-divergence-drill: watchdog-lua-deps + @just -f watchdog/justfile test-divergence-drill + +# Build lcurl (lua-cURLv3) into .deps/lua; JSON is pure Lua under watchdog/third_party/. +watchdog-lua-deps: + @just -f watchdog/justfile lua-deps + +# Anvil + rollups + sequencer-devnet; prints WATCHDOG_* exports until Ctrl+C. +devnet-for-watchdog: setup ensure-machine-image + cargo build -p sequencer --bin sequencer-devnet + cargo build -p rollups-e2e --bin devnet-stack + cargo run -p rollups-e2e --bin devnet-stack + +test-watchdog-compare-harness: setup watchdog-lua-deps ensure-machine-image + cargo build -p sequencer --bin sequencer-devnet -p rollups-e2e --bin rollups-e2e + cargo run -p rollups-e2e --bin rollups-e2e -- watchdog_genesis_compare_test --exact --nocapture + # Run sequencer tests sequentially so partition static config (init) is not shared across parallel tests. test-sequencer: cargo test -p sequencer --lib -- --test-threads=1 @@ -19,19 +43,27 @@ test-sequencer: cargo test -p sequencer --test ws_broadcaster -- --test-threads=1 cargo test -p sequencer --test batch_submitter_integration -- --test-threads=1 -test-rollups-e2e: setup ensure-machine-image - cargo build -p sequencer --bin sequencer-devnet -p rollups-e2e - cargo run -p rollups-e2e +test-rollups-e2e: setup ensure-machine-image ensure-sepolia-machine-image + just watchdog-lua-deps + cargo build -p sequencer --bin sequencer-devnet -p rollups-e2e --bin rollups-e2e + cargo run -p rollups-e2e --bin rollups-e2e ensure-machine-image: @test -d examples/canonical-app/out/canonical-machine-image || just canonical-build-machine-image +ensure-sepolia-machine-image: + @test -d examples/canonical-app/out/canonical-machine-image-sepolia || just canonical-build-machine-image-sepolia + bench target="all": just -f tests/benchmarks/justfile {{target}} setup: just -f examples/canonical-app/justfile download-deps just -f tests/benchmarks/justfile setup + just watchdog-lua-deps + +doctor: + just -f watchdog/justfile doctor canonical-build-machine-image: just -f examples/canonical-app/justfile build-machine-image diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..6360c18 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "1.95.0" +components = ["rustfmt", "clippy"] diff --git a/scripts/ci-watchdog-docker-smoke.sh b/scripts/ci-watchdog-docker-smoke.sh new file mode 100755 index 0000000..49ddacf --- /dev/null +++ b/scripts/ci-watchdog-docker-smoke.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# Build the watchdog OCI image and smoke-test cartesi-machine + cartesi Lua module. +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "${root}" + +# shellcheck disable=SC1091 +source toolchain-pins.env + +image="sequencer-watchdog:ci-smoke" +if command -v dpkg >/dev/null 2>&1; then + arch="$(dpkg --print-architecture)" +else + case "$(uname -m)" in + x86_64) arch=amd64 ;; + aarch64 | arm64) arch=arm64 ;; + *) + echo "unsupported arch for watchdog docker smoke: $(uname -m)" >&2 + exit 1 + ;; + esac +fi +case "${arch}" in + amd64) deb_sha="${CARTESI_MACHINE_SHA256_AMD64}" ;; + arm64) deb_sha="${CARTESI_MACHINE_SHA256_ARM64}" ;; + *) + echo "unsupported arch for watchdog docker smoke: ${arch}" >&2 + exit 1 + ;; +esac + +docker build \ + --build-arg "RELEASE_TAG=ci-smoke" \ + --build-arg "GIT_COMMIT=local" \ + --build-arg "CARTESI_MACHINE_VERSION=${CARTESI_MACHINE_VERSION}" \ + --build-arg "CARTESI_MACHINE_DEB_SHA256=${deb_sha}" \ + -f watchdog/Dockerfile \ + -t "${image}" \ + . + +docker run --rm -e WATCHDOG_PRINT_RELEASE_INFO=1 "${image}" >/dev/null +docker run --rm --entrypoint cartesi-machine "${image}" --version >/dev/null +docker run --rm --entrypoint flock "${image}" --version >/dev/null +docker run --rm --entrypoint lua5.4 "${image}" -e "require('cartesi'); print('cartesi ok')" +# Validate the vendored lcurl build loads in the runtime image. +docker run --rm --entrypoint lua5.4 "${image}" -e "require('lcurl'); print('lcurl ok')" + +state_dir="$(mktemp -d)" +lock_container="" +cleanup() { + if [[ -n "${lock_container}" ]]; then + docker rm -f "${lock_container}" >/dev/null 2>&1 || true + fi + rm -rf "${state_dir}" +} +trap cleanup EXIT + +set +e +tick_output="$( + docker run --rm \ + -e WATCHDOG_STATE_DIR=/state \ + -v "${state_dir}:/state" \ + "${image}" tick 2>&1 +)" +tick_status=$? +set -e +if [[ "${tick_status}" -ne 1 || "${tick_output}" != *"failed to load config.json"* ]]; then + echo "expected unlocked tick to reach Lua and fail on missing config.json" >&2 + echo "status=${tick_status}" >&2 + echo "${tick_output}" >&2 + exit 1 +fi + +lock_container="sequencer-watchdog-lock-smoke-$$" +docker run -d \ + --name "${lock_container}" \ + -v "${state_dir}:/state" \ + --entrypoint sh \ + "${image}" \ + -c 'flock /state/run.lock sleep 30' >/dev/null + +locked="" +for _ in $(seq 1 30); do + set +e + lock_output="$( + docker run --rm \ + -e WATCHDOG_STATE_DIR=/state \ + -v "${state_dir}:/state" \ + "${image}" tick 2>&1 + )" + lock_status=$? + set -e + if [[ "${lock_status}" -eq 1 && "${lock_output}" == *"watchdog state is already locked"* ]]; then + locked=1 + break + fi + sleep 0.2 +done +if [[ -z "${locked}" ]]; then + echo "expected tick to fail on held watchdog lock" >&2 + echo "last status=${lock_status}" >&2 + echo "${lock_output}" >&2 + exit 1 +fi + +echo "watchdog docker smoke ok" diff --git a/scripts/generate-release-manifest.sh b/scripts/generate-release-manifest.sh new file mode 100755 index 0000000..5bfb75f --- /dev/null +++ b/scripts/generate-release-manifest.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Emit release-manifest.json: ties sequencer, watchdog, CM images, and toolchain pins. +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +pins="${root}/toolchain-pins.env" + +usage() { + echo "usage: $0 --tag TAG [--git-sha SHA] [--output PATH]" >&2 + exit 1 +} + +tag="" +git_sha="" +output="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --tag) + tag="${2:-}" + shift 2 + ;; + --git-sha) + git_sha="${2:-}" + shift 2 + ;; + --output) + output="${2:-}" + shift 2 + ;; + *) + usage + ;; + esac +done + +if [[ -z "${tag}" ]]; then + usage +fi + +if [[ -z "${git_sha}" ]]; then + git_sha="$(git -C "${root}" rev-parse HEAD 2>/dev/null || echo unknown)" +fi + +if [[ ! -f "${pins}" ]]; then + echo "missing ${pins}" >&2 + exit 1 +fi + +# shellcheck disable=SC1090 +set -a +source "${pins}" +set +a + +artifacts_json="$(cat <-{tag}.tar.gz with WATCHDOG_CM_SNAPSHOT_DIR; cartesi-machine in the watchdog image matches CARTESI_MACHINE_VERSION.", + }, +} +json.dump(manifest, sys.stdout, indent=2) +sys.stdout.write("\n") +PY +} + +if [[ -n "${output}" ]]; then + emit_manifest > "${output}" +else + emit_manifest +fi diff --git a/scripts/load-toolchain-pins.sh b/scripts/load-toolchain-pins.sh new file mode 100755 index 0000000..340a4e3 --- /dev/null +++ b/scripts/load-toolchain-pins.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +# Parse toolchain-pins.env and append KEY=value lines to a GitHub env file. +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +pins="${TOOLCHAIN_PINS_FILE:-${root}/toolchain-pins.env}" +output="${1:-${GITHUB_ENV:-}}" + +usage() { + echo "usage: $0 OUTPUT_FILE" >&2 + echo " $0 - # print parsed KEY=value lines to stdout" >&2 + exit 1 +} + +trim() { + local value="$1" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + printf '%s' "${value}" +} + +emit() { + if [[ "${output}" == "-" ]]; then + printf '%s\n' "$1" + else + printf '%s\n' "$1" >> "${output}" + fi +} + +if [[ -z "${output}" ]]; then + usage +fi +if [[ ! -f "${pins}" ]]; then + echo "missing ${pins}" >&2 + exit 1 +fi + +while IFS= read -r line || [[ -n "${line}" ]]; do + line="${line%%#*}" + line="$(trim "${line}")" + if [[ -z "${line}" ]]; then + continue + fi + if [[ ! "${line}" =~ ^[A-Za-z_][A-Za-z0-9_]*= ]]; then + echo "invalid line in ${pins}: ${line}" >&2 + exit 1 + fi + emit "${line}" +done < "${pins}" diff --git a/scripts/test-watchdog-divergence-drill.sh b/scripts/test-watchdog-divergence-drill.sh new file mode 100755 index 0000000..16fe8d5 --- /dev/null +++ b/scripts/test-watchdog-divergence-drill.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Run the divergence drill; recipe success requires exit 2 (production divergence code). +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +export WATCHDOG_LUA_DEPS="${WATCHDOG_LUA_DEPS:-${root}/.deps/lua}" + +set +e +lua "${root}/watchdog/tests/drill_divergence.lua" +code=$? +set -e + +if [[ "${code}" -eq 0 ]]; then + echo "expected drill exit 2, got 0" >&2 + exit 1 +fi +if [[ "${code}" -ne 2 ]]; then + echo "expected drill exit 2, got ${code}" >&2 + exit 1 +fi diff --git a/scripts/verify-toolchain-pins.sh b/scripts/verify-toolchain-pins.sh new file mode 100755 index 0000000..8e17668 --- /dev/null +++ b/scripts/verify-toolchain-pins.sh @@ -0,0 +1,34 @@ +#!/usr/bin/env bash +# Fail if toolchain-pins.env drifts from other pinned artifacts in-tree. +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +pins="${root}/toolchain-pins.env" + +if [[ ! -f "${pins}" ]]; then + echo "missing ${pins}" >&2 + exit 1 +fi + +# shellcheck disable=SC1090 +set -a +source "${pins}" +set +a + +errors=0 + +rust_channel="$( + grep -E '^\s*channel\s*=' "${root}/rust-toolchain.toml" \ + | head -1 \ + | sed -E 's/.*=[[:space:]]*"([^"]+)".*/\1/' +)" +if [[ "${rust_channel}" != "${RUST_TOOLCHAIN}" ]]; then + echo "rust-toolchain.toml channel=${rust_channel} != RUST_TOOLCHAIN=${RUST_TOOLCHAIN}" >&2 + errors=$((errors + 1)) +fi + +if [[ "${errors}" -ne 0 ]]; then + exit 1 +fi + +echo "toolchain pins aligned" diff --git a/scripts/watchdog-lua-deps.sh b/scripts/watchdog-lua-deps.sh new file mode 100755 index 0000000..d9c283c --- /dev/null +++ b/scripts/watchdog-lua-deps.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Build the watchdog's native Lua dep: lcurl (lua-cURLv3) -> .deps/lua/lcurl.so. +# +# Sources are vendored in-tree under watchdog/third_party/lua-curl/src (a curated +# C subset; see watchdog/third_party/lua-curl/UPSTREAM for provenance). There is +# no build-time download and no pin to verify -- the compiled bytes are exactly +# the in-tree source. libcurl must be installed on the host. JSON is pure Lua +# under watchdog/third_party/json.lua (no compile step). +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +src_dir="${root}/watchdog/third_party/lua-curl/src" +out_dir="${root}/.deps/lua" +out_so="${out_dir}/lcurl.so" + +mkdir -p "${out_dir}" + +resolve_lua_bin() { + if [[ -n "${LUA_BIN:-}" ]]; then + echo "${LUA_BIN}" + return + fi + for bin in lua5.4 lua; do + if command -v "${bin}" >/dev/null 2>&1; then + echo "${bin}" + return + fi + done +} +lua_bin="$(resolve_lua_bin || true)" + +lcurl_loadable() { + [[ -n "${lua_bin}" ]] || return 1 + "${lua_bin}" -e "package.cpath='${out_dir}/?.so;'..package.cpath; require('lcurl')" >/dev/null 2>&1 +} + +# Rebuild only if the .so is missing/unloadable or older than any vendored source. +needs_build=1 +if [[ -f "${out_so}" ]] && lcurl_loadable; then + needs_build=0 + while IFS= read -r src; do + if [[ "${src}" -nt "${out_so}" ]]; then + needs_build=1 + break + fi + done < <(find "${src_dir}" -type f \( -name '*.c' -o -name '*.h' \)) +fi +if [[ "${needs_build}" -eq 0 ]]; then + exit 0 +fi + +# Lua headers: prefer pkg-config (covers nix / Homebrew / Debian), then the +# usual Debian include dirs, then an explicit LUA_INC override. +lua_cflags="" +if [[ -n "${LUA_INC:-}" ]]; then + lua_cflags="-I${LUA_INC}" +else + for impl in lua5.4 lua5.3 lua; do + if pkg-config --exists "${impl}" 2>/dev/null; then + lua_cflags="$(pkg-config --cflags "${impl}")" + break + fi + done +fi +if [[ -z "${lua_cflags}" ]]; then + for dir in /usr/include/lua5.4 /usr/include/lua5.3 /usr/include/lua; do + if [[ -f "${dir}/lua.h" ]]; then + lua_cflags="-I${dir}" + break + fi + done +fi +if [[ -z "${lua_cflags}" ]]; then + echo "watchdog-lua-deps: Lua headers not found; install lua5.4-dev or set LUA_INC" >&2 + exit 1 +fi + +if ! pkg-config --exists libcurl 2>/dev/null; then + echo "watchdog-lua-deps: libcurl dev package not found (libcurl4-openssl-dev or similar)" >&2 + exit 1 +fi + +# Match the upstream Makefile's essential flags; on macOS a Lua C module is a +# bundle with dynamic_lookup, on Linux a plain shared object. +case "$(uname)" in + Darwin) os_flags=(-bundle -undefined dynamic_lookup) ;; + *) os_flags=(-shared) ;; +esac + +echo "watchdog-lua-deps: compiling vendored lcurl.so" >&2 +# shellcheck disable=SC2046 # intentional word-splitting of pkg-config output +"${CC:-cc}" -O2 -pipe -fPIC "${os_flags[@]}" -Wall -Wno-unused-value -DPTHREADS \ + ${lua_cflags} $(pkg-config --cflags libcurl) \ + "${src_dir}"/*.c \ + -o "${out_so}" \ + $(pkg-config --libs libcurl) + +if [[ -z "${lua_bin}" ]]; then + echo "watchdog-lua-deps: install lua5.4 (or set LUA_BIN) to verify lcurl.so" >&2 + exit 1 +fi +if ! lcurl_loadable; then + echo "watchdog-lua-deps: built lcurl.so but ${lua_bin} cannot load it (Lua version mismatch?)" >&2 + exit 1 +fi diff --git a/sequencer-core/src/application/mod.rs b/sequencer-core/src/application/mod.rs index 1844d94..d8bddec 100644 --- a/sequencer-core/src/application/mod.rs +++ b/sequencer-core/src/application/mod.rs @@ -185,4 +185,20 @@ pub trait Application: Send + Sized { /// is a pure function of `prefix`: callers may invoke it without /// loading the dump or instantiating the Application. fn state_file_in_dump(prefix: &Path) -> PathBuf; + + /// Deterministic canonical state bytes (SSZ for the toy wallet). Used by + /// CM `inspect_state` and the watchdog's `/finalized_state` compare. + /// Default: not implemented. + fn canonical_snapshot_bytes(&self) -> Result, AppError> { + Err(AppError::Internal { + reason: "canonical snapshot bytes are not implemented".to_string(), + }) + } + + /// Optional human-readable JSON for debugging only (not loaded on recovery). + fn export_state(&self) -> Result { + Err(AppError::Internal { + reason: "application state export is not implemented".to_string(), + }) + } } diff --git a/sequencer/src/egress/api/state.rs b/sequencer/src/egress/api/state.rs index de15396..1edb5c1 100644 --- a/sequencer/src/egress/api/state.rs +++ b/sequencer/src/egress/api/state.rs @@ -1,8 +1,7 @@ // (c) Cartesi and individual authors (see AUTHORS) // SPDX-License-Identifier: Apache-2.0 (see LICENSE) -//! Egress-side axum state — feeds the WS subscribe handler today; will grow as -//! more egress routes are added. +//! Egress-side axum state for WS subscribe and health probes. use std::sync::Arc; diff --git a/sequencer/src/http.rs b/sequencer/src/http.rs index 987f2b0..f9147f0 100644 --- a/sequencer/src/http.rs +++ b/sequencer/src/http.rs @@ -218,7 +218,6 @@ pub fn start_on_listener( config.ws_max_subscribers, config.ws_max_catchup_events, )); - let app: Router = crate::ingress::api::router(submit_state) .merge(crate::egress::api::router( subscribe_state, diff --git a/sequencer/src/l1/partition.rs b/sequencer/src/l1/partition.rs index 0e39d7e..09383a1 100644 --- a/sequencer/src/l1/partition.rs +++ b/sequencer/src/l1/partition.rs @@ -89,17 +89,230 @@ pub fn error_message_matches_retry_codes(error_message: &str, codes: &[String]) codes.iter().any(|c| error_message.contains(c)) } +/// Simulates partitioned `eth_getLogs` bisect (same rules as `watchdog/l1_reader.lua`). +/// `get_logs` returns `Ok` for a successful leaf query or `Err(message)` on RPC failure. +/// Records every attempted `(from_block, to_block)` in call order. +pub fn simulate_partitioned_get_logs( + start_block: u64, + end_block: u64, + long_block_range_error_codes: &[String], + mut get_logs: F, +) -> (Vec<(u64, u64)>, Result<(), String>) +where + F: FnMut(u64, u64) -> Result<(), String>, +{ + if end_block < start_block { + return (Vec::new(), Ok(())); + } + + fn go( + start_block: u64, + end_block: u64, + long_block_range_error_codes: &[String], + get_logs: &mut F, + calls: &mut Vec<(u64, u64)>, + ) -> Result<(), String> + where + F: FnMut(u64, u64) -> Result<(), String>, + { + calls.push((start_block, end_block)); + match get_logs(start_block, end_block) { + Ok(()) => Ok(()), + Err(err) => { + if start_block < end_block + && error_message_matches_retry_codes(&err, long_block_range_error_codes) + { + let middle = start_block + (end_block - start_block) / 2; + go( + start_block, + middle, + long_block_range_error_codes, + get_logs, + calls, + )?; + go( + middle + 1, + end_block, + long_block_range_error_codes, + get_logs, + calls, + )?; + Ok(()) + } else { + Err(err) + } + } + } + } + + let mut calls = Vec::new(); + let result = go( + start_block, + end_block, + long_block_range_error_codes, + &mut get_logs, + &mut calls, + ); + (calls, result) +} + +/// Total order for merged logs (block, tx index, log index) — matches Lua `sort_logs`. +pub fn sort_logs_by_l1_order( + logs: &mut [T], + block: FBlock, + tx_index: FTx, + log_index: FLog, +) where + FBlock: Fn(&T) -> u64, + FTx: Fn(&T) -> u64, + FLog: Fn(&T) -> u64, +{ + logs.sort_by(|a, b| { + block(a) + .cmp(&block(b)) + .then_with(|| tx_index(a).cmp(&tx_index(b))) + .then_with(|| log_index(a).cmp(&log_index(b))) + }); +} + pub fn decode_evm_advance_input(input: &[u8]) -> Result { EvmAdvanceCall::abi_decode(input).map_err(|err| err.to_string()) } #[cfg(test)] mod tests { + use std::collections::HashMap; + use alloy_primitives::{U256, address}; use alloy_sol_types::SolCall; use cartesi_rollups_contracts::inputs::Inputs::EvmAdvanceCall; + use serde::Deserialize; + + use super::{ + DEFAULT_LONG_BLOCK_RANGE_ERROR_CODES, decode_evm_advance_input, + error_message_matches_retry_codes, simulate_partitioned_get_logs, sort_logs_by_l1_order, + }; + + const PARTITION_VECTOR: &str = include_str!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../tests/fixtures/l1_partition_vector.json" + )); + + #[derive(Debug, Deserialize)] + struct FailRange { + from: u64, + to: u64, + message: String, + } + + #[derive(Debug, Deserialize)] + struct PartitionScenario { + name: String, + start_block: u64, + end_block: u64, + fail_ranges: Vec, + expect_calls: Vec<[u64; 2]>, + expect_ok: bool, + } + + #[derive(Debug, Deserialize)] + struct LogSortFixture { + unsorted: Vec, + expect_block_order: Vec, + expect_log_index_order: Vec, + } - use super::{decode_evm_advance_input, error_message_matches_retry_codes}; + #[derive(Debug, Deserialize)] + struct LogSortEntry { + #[serde(rename = "blockNumber")] + block_number: String, + #[serde(rename = "transactionIndex", default)] + transaction_index: String, + #[serde(rename = "logIndex", default)] + log_index: String, + } + + #[derive(Debug, Deserialize)] + struct PartitionVector { + long_block_range_error_codes: Vec, + scenarios: Vec, + log_sort: LogSortFixture, + } + + fn load_partition_vector() -> PartitionVector { + serde_json::from_str(PARTITION_VECTOR).expect("parse l1_partition_vector.json") + } + + fn fail_map(fail_ranges: &[FailRange]) -> HashMap<(u64, u64), String> { + fail_ranges + .iter() + .map(|r| ((r.from, r.to), r.message.clone())) + .collect() + } + + #[test] + fn fixture_default_codes_match_rust_defaults() { + let vector = load_partition_vector(); + let expected: Vec = DEFAULT_LONG_BLOCK_RANGE_ERROR_CODES + .iter() + .map(|c| (*c).to_string()) + .collect(); + assert_eq!(vector.long_block_range_error_codes, expected); + } + + #[test] + fn partition_vector_simulation_matches_watchdog() { + let vector = load_partition_vector(); + for scenario in vector.scenarios { + let fails = fail_map(&scenario.fail_ranges); + let codes = vector.long_block_range_error_codes.clone(); + let (calls, result) = simulate_partitioned_get_logs( + scenario.start_block, + scenario.end_block, + &codes, + |from, to| { + if let Some(message) = fails.get(&(from, to)) { + Err(message.clone()) + } else { + Ok(()) + } + }, + ); + + let expected_calls: Vec<(u64, u64)> = scenario + .expect_calls + .iter() + .map(|pair| (pair[0], pair[1])) + .collect(); + assert_eq!(calls, expected_calls, "scenario {}", scenario.name); + assert_eq!( + result.is_ok(), + scenario.expect_ok, + "scenario {}", + scenario.name + ); + } + } + + #[test] + fn log_sort_vector_matches_watchdog_order() { + let vector = load_partition_vector(); + let mut logs = vector.log_sort.unsorted; + sort_logs_by_l1_order( + &mut logs, + |e| parse_hex_quantity(&e.block_number), + |e| parse_hex_quantity(&e.transaction_index), + |e| parse_hex_quantity(&e.log_index), + ); + let blocks: Vec = logs.iter().map(|e| e.block_number.clone()).collect(); + let indices: Vec = logs.iter().map(|e| e.log_index.clone()).collect(); + assert_eq!(blocks, vector.log_sort.expect_block_order); + assert_eq!(indices, vector.log_sort.expect_log_index_order); + } + + fn parse_hex_quantity(value: &str) -> u64 { + u64::from_str_radix(value.strip_prefix("0x").unwrap_or(value), 16).expect("hex quantity") + } #[test] fn error_message_matches_retry_codes_returns_true_when_message_contains_code() { diff --git a/sequencer/src/runtime/mod.rs b/sequencer/src/runtime/mod.rs index 846c8a7..0c15f56 100644 --- a/sequencer/src/runtime/mod.rs +++ b/sequencer/src/runtime/mod.rs @@ -37,7 +37,7 @@ const INPUT_READER_POLL_INTERVAL: Duration = Duration::from_secs(2); pub async fn run(app: A, config: RunConfig) -> Result<(), RunError> where - A: Application + 'static, + A: Application + Clone + Sync + 'static, { // ── Bootstrap ──────────────────────────────────────────── std::fs::create_dir_all(&config.data_dir)?; diff --git a/sequencer/src/runtime/workers.rs b/sequencer/src/runtime/workers.rs index 630e963..e3bc6ce 100644 --- a/sequencer/src/runtime/workers.rs +++ b/sequencer/src/runtime/workers.rs @@ -83,7 +83,7 @@ pub(crate) struct Workers { impl Workers { /// Build the worker configs, spawn each worker, return the owning struct. /// Logs `listening` once the HTTP server is bound. - pub(crate) async fn spawn( + pub(crate) async fn spawn( cfg: WorkersConfig, ) -> Result { let WorkersConfig { diff --git a/tests/e2e/Cargo.toml b/tests/e2e/Cargo.toml index b227077..7f0bbee 100644 --- a/tests/e2e/Cargo.toml +++ b/tests/e2e/Cargo.toml @@ -9,8 +9,13 @@ repository.workspace = true readme.workspace = true authors.workspace = true +[[bin]] +name = "devnet-stack" +path = "src/bin/devnet_stack.rs" + [dependencies] rollups-harness = { path = "../harness" } +tracing-subscriber = { version = "0.3", features = ["env-filter"] } app-core = { path = "../../examples/app-core" } sequencer-core = { path = "../../sequencer-core" } sequencer-rust-client = { path = "../../sdk/rust-client" } @@ -19,4 +24,6 @@ alloy-sol-types = "1.4.1" futures = "0.3" libtest-mimic = "0.6.1" ssz = { package = "ethereum_ssz", version = "0.10" } -tokio = { version = "1.35", features = ["macros", "rt-multi-thread", "time", "net"] } +tokio = { version = "1.35", features = ["macros", "rt-multi-thread", "time", "net", "process", "io-util", "signal"] } +serde_json = "1.0" +tempfile = "3.10" diff --git a/tests/e2e/src/bin/devnet_stack.rs b/tests/e2e/src/bin/devnet_stack.rs new file mode 100644 index 0000000..e49ca4b --- /dev/null +++ b/tests/e2e/src/bin/devnet_stack.rs @@ -0,0 +1,73 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +//! Local Anvil + rollups devnet + `sequencer-devnet` for manual watchdog runs. +//! +//! Prints `WATCHDOG_*` exports, then blocks until Ctrl+C. + +use rollups_harness::{ + HarnessResult, ManagedSequencer, devnet_sequencer_config_no_faketime, paths, +}; + +#[tokio::main] +async fn main() -> HarnessResult<()> { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")), + ) + .init(); + + let runtime = + ManagedSequencer::spawn(devnet_sequencer_config_no_faketime("devnet-stack")).await?; + + let machine_image = paths::devnet_machine_image_path(); + let state_dir = std::env::temp_dir().join("watchdog-state-devnet"); + + eprintln!(); + eprintln!("=== Devnet stack is up ==="); + eprintln!("Sequencer HTTP: {}", runtime.endpoint()); + eprintln!("L1 RPC: {}", runtime.l1_endpoint()); + eprintln!("App address: {}", runtime.app_address()); + eprintln!("InputBox: {}", runtime.input_box_address()); + eprintln!(); + eprintln!( + "--- export these, then run: ./watchdog/sequencer-watchdog init && ./watchdog/sequencer-watchdog tick ---" + ); + eprintln!("export WATCHDOG_SEQUENCER_URL={}", runtime.endpoint()); + eprintln!("export WATCHDOG_L1_RPC_URL={}", runtime.l1_endpoint()); + eprintln!( + "export WATCHDOG_INPUTBOX_ADDRESS={}", + runtime.input_box_address() + ); + eprintln!("export WATCHDOG_APP_ADDRESS={}", runtime.app_address()); + eprintln!("export WATCHDOG_STATE_DIR={}", state_dir.display()); + eprintln!( + "export WATCHDOG_CM_SNAPSHOT_DIR={}", + machine_image.display() + ); + eprintln!("export WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK=0"); + eprintln!( + "export WATCHDOG_LUA_DEPS={}/.deps/lua", + paths::workspace_root().display() + ); + eprintln!(); + eprintln!("Wait for finalized snapshot (404 until promotion):"); + eprintln!( + " curl -s {}/finalized_state/inclusion_block", + runtime.endpoint() + ); + eprintln!(); + eprintln!("Run watchdog (from repo root, after `just watchdog-lua-deps`):"); + eprintln!(" export WATCHDOG_LUA_ROOT=$(pwd)"); + eprintln!(" export WATCHDOG_LUA_BIN=lua"); + eprintln!(" ./watchdog/sequencer-watchdog init"); + eprintln!(" ./watchdog/sequencer-watchdog tick"); + eprintln!(); + eprintln!("Press Ctrl+C here to stop Anvil + sequencer."); + + tokio::signal::ctrl_c() + .await + .map_err(|err| std::io::Error::other(err.to_string()))?; + runtime.shutdown().await +} diff --git a/tests/e2e/src/lib.rs b/tests/e2e/src/lib.rs index 879df69..5b72017 100644 --- a/tests/e2e/src/lib.rs +++ b/tests/e2e/src/lib.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 (see LICENSE) pub mod test_cases; +mod watchdog_compare; use std::future::Future; use std::pin::Pin; diff --git a/tests/e2e/src/main.rs b/tests/e2e/src/main.rs index 7b3c903..1807605 100644 --- a/tests/e2e/src/main.rs +++ b/tests/e2e/src/main.rs @@ -3,7 +3,9 @@ use libtest_mimic::{Arguments, Trial}; use rollups_e2e::run_trial; -use rollups_harness::{ManagedSequencer, default_devnet_sequencer_config}; +use rollups_harness::{ + ManagedSequencer, default_devnet_sequencer_config, devnet_sequencer_config_no_faketime, +}; fn main() { let mut args = Arguments::from_args(); @@ -14,10 +16,16 @@ fn main() { .map(|(name, scenario)| { Trial::test(name, move || { let log_prefix = format!("rollups-e2e-{name}"); + let spawn_config = if name == "watchdog_genesis_compare_test" + || name == "deposit_transfer_withdrawal_test" + || name == "watchdog_non_genesis_divergence_test" + { + devnet_sequencer_config_no_faketime(log_prefix) + } else { + default_devnet_sequencer_config(log_prefix) + }; run_trial(name, || async move { - let mut runtime = - ManagedSequencer::spawn(default_devnet_sequencer_config(log_prefix)) - .await?; + let mut runtime = ManagedSequencer::spawn(spawn_config).await?; let scenario_result = scenario(&mut runtime).await; // Post-test schema invariants: assert the DB's structural // invariants only if the scenario succeeded — otherwise diff --git a/tests/e2e/src/test_cases.rs b/tests/e2e/src/test_cases.rs index af3a41b..08e4df9 100644 --- a/tests/e2e/src/test_cases.rs +++ b/tests/e2e/src/test_cases.rs @@ -7,7 +7,7 @@ use crate::{ScenarioFn, ScenarioResult}; use alloy_primitives::{Address, U256}; use rollups_harness::{ ManagedSequencer, ReplayWalletApp, RespawnAttemptOutcome, RespawnPolicy, TcpProxy, TestSigner, - WalletL1Client, WsClient, sign_user_op_hex, + WalletL1Client, WalletL2Client, WsClient, sign_user_op_hex, }; use sequencer_core::api::{TxRequest, WsTxMessage}; use sequencer_core::fee::fee_to_linear; @@ -305,12 +305,34 @@ pub fn test_cases() -> Vec<(&'static str, ScenarioFn)> { ) }, ), + ("watchdog_genesis_compare_test", |runtime| { + Box::pin(crate::watchdog_compare::run_watchdog_genesis_compare_test( + runtime, + )) + }), + ("watchdog_non_genesis_divergence_test", |runtime| { + Box::pin(run_watchdog_non_genesis_divergence_test(runtime)) + }), ] } async fn run_deposit_transfer_withdrawal_test( runtime: &mut ManagedSequencer, ) -> ScenarioResult<()> { + prepare_non_genesis_watchdog_state(runtime).await?; + crate::watchdog_compare::run_watchdog_non_genesis_compare_test(runtime).await?; + Ok(()) +} + +async fn run_watchdog_non_genesis_divergence_test( + runtime: &mut ManagedSequencer, +) -> ScenarioResult<()> { + prepare_non_genesis_watchdog_state(runtime).await?; + crate::watchdog_compare::run_watchdog_non_genesis_divergence_test(runtime).await?; + Ok(()) +} + +async fn prepare_non_genesis_watchdog_state(runtime: &mut ManagedSequencer) -> ScenarioResult<()> { let alice = TestSigner::from_default(1)?; let bob = TestSigner::from_default(2)?; let alice_address = alice.address(); @@ -322,7 +344,9 @@ async fn run_deposit_transfer_withdrawal_test( let mut bob_l2 = runtime.wallet_l2(bob)?; let mut replay = ReplayWalletApp::devnet(); - let deposit_amount = U256::from(600_000_u64); + // Large deposit leaves headroom for the post-scenario batch-close pump + // (~150 user ops) that drives a gold finalized snapshot for watchdog compare. + let deposit_amount = U256::from(10_000_000_u64); let transfer_amount = U256::from(400_000_u64); let withdrawal_amount = U256::from(150_000_u64); let gas = fee_to_linear(DEFAULT_FRAME_FEE); @@ -335,7 +359,6 @@ async fn run_deposit_transfer_withdrawal_test( bob_l2.withdraw(withdrawal_amount).await?; replay.apply(ws.expect_user_op_from(bob_address).await?)?; - // Alice: 600_000 - 400_000 - gas. Bob: 400_000 - 150_000 - gas. assert_wallet_state( &replay, ExpectedWalletState { @@ -350,6 +373,45 @@ async fn run_deposit_transfer_withdrawal_test( }, 3, ); + + drive_finalized_gold_batch_for_watchdog( + runtime, + &mut ws, + &mut replay, + &mut alice_l2, + alice_address, + ) + .await?; + Ok(()) +} + +/// Close the open batch, land it on L1, and wait for snapshot promotion so +/// `/finalized_state` reports `inclusion_block > 0`. +async fn drive_finalized_gold_batch_for_watchdog( + runtime: &ManagedSequencer, + ws: &mut WsClient, + replay: &mut ReplayWalletApp, + alice_l2: &mut WalletL2Client, + alice_address: Address, +) -> ScenarioResult<()> { + const TRANSFERS_TO_FORCE_BATCH_CLOSE: usize = 150; + const SUBMITTER_TICK_WAIT: Duration = Duration::from_secs(7); + + let batches_before = runtime.count_batches()?; + for _ in 0..TRANSFERS_TO_FORCE_BATCH_CLOSE { + alice_l2.transfer(alice_address, U256::from(1_u64)).await?; + replay.apply(ws.expect_user_op_from(alice_address).await?)?; + } + let batches_after = runtime.count_batches()?; + if batches_after.sealed <= batches_before.sealed { + return Err(format!( + "expected batch close before watchdog compare: before={batches_before:?} after={batches_after:?}" + ) + .into()); + } + + tokio::time::sleep(SUBMITTER_TICK_WAIT).await; + runtime.mine_l1_blocks(3).await?; Ok(()) } @@ -894,7 +956,9 @@ async fn run_recovery_after_stale_batches_test( let mut alice_l2 = runtime.wallet_l2(alice.clone())?; let mut replay_before = ReplayWalletApp::devnet(); - let deposit_amount = U256::from(600_000_u64); + // Large deposit leaves headroom for the post-recovery batch-close pump + // that drives a finalized snapshot for watchdog compare. + let deposit_amount = U256::from(10_000_000_u64); let transfer_amount = U256::from(100_000_u64); let post_recovery_transfer = U256::from(200_000_u64); let gas = fee_to_linear(DEFAULT_FRAME_FEE); @@ -983,6 +1047,16 @@ async fn run_recovery_after_stale_batches_test( ); assert_eq!(replay_after.current_user_nonce(alice_address), 1); + drive_finalized_gold_batch_for_watchdog( + runtime, + &mut ws_after, + &mut replay_after, + &mut alice_l2_fresh, + alice_address, + ) + .await?; + crate::watchdog_compare::run_watchdog_non_genesis_compare_test(runtime).await?; + Ok(()) } diff --git a/tests/e2e/src/watchdog_compare.rs b/tests/e2e/src/watchdog_compare.rs new file mode 100644 index 0000000..fc17589 --- /dev/null +++ b/tests/e2e/src/watchdog_compare.rs @@ -0,0 +1,601 @@ +// (c) Cartesi and individual authors (see AUTHORS) +// SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +//! Watchdog compare harness: Anvil + devnet sequencer + live CM inspect. + +use std::path::Path; +use std::process::Stdio; +use std::time::Duration; + +use app_core::application::{WalletApp, WalletConfig}; +use app_core::wallet_snapshot; +use rollups_harness::ManagedSequencer; +use rollups_harness::paths; +use sequencer_core::application::Application; +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::TcpStream; +use tokio::process::Command; + +use crate::ScenarioResult; + +const DEVNET_MACHINE_IMAGE: &str = "examples/canonical-app/out/canonical-machine-image"; +const SEPOLIA_MACHINE_IMAGE: &str = "examples/canonical-app/out/canonical-machine-image-sepolia"; +const GENESIS_SAFE_BLOCK: &str = "0"; + +fn require_cartesi_machine() { + assert!( + std::process::Command::new("cartesi-machine") + .arg("--version") + .stdout(std::process::Stdio::null()) + .stderr(std::process::Stdio::null()) + .status() + .is_ok(), + "cartesi-machine not found on PATH — install Cartesi tools (or run `nix develop`)" + ); +} + +pub async fn run_watchdog_genesis_compare_test( + runtime: &mut ManagedSequencer, +) -> ScenarioResult<()> { + require_cartesi_machine(); + + let workspace = paths::workspace_root(); + let machine_image = workspace.join(DEVNET_MACHINE_IMAGE); + if !machine_image.is_dir() { + return Err(format!( + "canonical machine image missing at {}; run: just canonical-build-machine-image", + machine_image.display() + ) + .into()); + } + + // `sequencer-devnet` uses `WalletConfig::devnet()` (not `default()` / Sepolia). + let expected_snapshot = wallet_snapshot::encode(&WalletApp::new(WalletConfig::devnet())); + + eprintln!("[watchdog-harness] step 1/6: wait for sequencer GET /finalized_state"); + let finalized_url = format!("{}/finalized_state", runtime.endpoint()); + let (_status, body, headers) = + wait_for_finalized_state(finalized_url.as_str(), Duration::from_secs(30)).await?; + let inclusion_block = header_u64(&headers, "x-inclusion-block") + .ok_or("finalized_state response missing X-Inclusion-Block header")?; + eprintln!( + "[watchdog-harness] sequencer inclusion_block={inclusion_block} snapshot_bytes={}", + body.len() + ); + if body.as_slice() != expected_snapshot.as_slice() { + return Err(format!( + "finalized_state bytes mismatch (len {} vs expected {})", + body.len(), + expected_snapshot.len() + ) + .into()); + } + + eprintln!("[watchdog-harness] step 2/6: prove CM inspect SSZ on genesis image"); + let inspect_state = + prove_cm_inspect_genesis(workspace.as_path(), machine_image.as_path()).await?; + if inspect_state.as_slice() != expected_snapshot.as_slice() { + return Err(format!( + "CM inspect bytes mismatch (len {} vs expected {})", + inspect_state.len(), + expected_snapshot.len() + ) + .into()); + } + + eprintln!("[watchdog-harness] step 3/6: prepare watchdog state dir"); + let state_dir = tempfile::tempdir() + .map_err(|err| format!("temp watchdog state dir: {err}"))? + .keep(); + + eprintln!("[watchdog-harness] step 4/6: initialize watchdog state (machine_cartesi)"); + run_lua_main_success( + runtime, + workspace.as_path(), + state_dir.as_path(), + machine_image.as_path(), + "init", + ) + .await?; + + eprintln!( + "[watchdog-harness] step 5/6: run production watchdog tick (genesis unchanged -> idle skip)" + ); + // The Rust checks above prove sequencer/CM byte parity at genesis. The + // watchdog tick itself intentionally idles because init selected block 0 + // and the sequencer finalized block is still 0; init is not a compare. + run_lua_main_success( + runtime, + workspace.as_path(), + state_dir.as_path(), + machine_image.as_path(), + "tick", + ) + .await?; + + eprintln!("[watchdog-harness] step 6/6: run a second tick (idempotent idle skip)"); + run_lua_main_success( + runtime, + workspace.as_path(), + state_dir.as_path(), + machine_image.as_path(), + "tick", + ) + .await?; + + eprintln!("[watchdog-harness] compare pass completed successfully"); + Ok(()) +} + +pub async fn run_watchdog_non_genesis_compare_test( + runtime: &mut ManagedSequencer, +) -> ScenarioResult<()> { + require_cartesi_machine(); + + let workspace = paths::workspace_root(); + let machine_image = workspace.join(DEVNET_MACHINE_IMAGE); + if !machine_image.is_dir() { + return Err(format!( + "canonical machine image missing at {}; run: just canonical-build-machine-image", + machine_image.display() + ) + .into()); + } + + eprintln!("[watchdog-harness] step 1/4: wait for non-genesis GET /finalized_state"); + let finalized_url = format!("{}/finalized_state", runtime.endpoint()); + let (_status, body, headers) = wait_for_non_genesis_finalized_state( + finalized_url.as_str(), + runtime, + Duration::from_secs(60), + ) + .await?; + let inclusion_block = header_u64(&headers, "x-inclusion-block") + .ok_or("finalized_state response missing X-Inclusion-Block header")?; + eprintln!( + "[watchdog-harness] finalized non-genesis snapshot inclusion_block={inclusion_block} snapshot_bytes={}", + body.len() + ); + + let genesis_snapshot = wallet_snapshot::encode(&WalletApp::new(WalletConfig::devnet())); + if body.as_slice() == genesis_snapshot.as_slice() { + return Err( + "non-genesis finalized_state still matches empty devnet genesis snapshot".into(), + ); + } + let decoded = wallet_snapshot::decode(body.as_slice()) + .map_err(|err| format!("decode non-genesis finalized_state: {err}"))?; + if decoded.executed_input_count() == 0 { + return Err("expected non-genesis finalized_state executed_input_count > 0".into()); + } + + eprintln!("[watchdog-harness] step 2/5: prepare watchdog state dir"); + let state_dir = tempfile::tempdir() + .map_err(|err| format!("temp watchdog state dir: {err}"))? + .keep(); + + eprintln!("[watchdog-harness] step 3/5: initialize watchdog state (machine_cartesi)"); + run_lua_main_success( + runtime, + workspace.as_path(), + state_dir.as_path(), + machine_image.as_path(), + "init", + ) + .await?; + + eprintln!("[watchdog-harness] step 4/5: run production watchdog tick"); + run_lua_main_success( + runtime, + workspace.as_path(), + state_dir.as_path(), + machine_image.as_path(), + "tick", + ) + .await?; + + eprintln!( + "[watchdog-harness] step 5/5: run a second tick (idempotent re-run: unchanged finalized -> skip)" + ); + run_lua_main_success( + runtime, + workspace.as_path(), + state_dir.as_path(), + machine_image.as_path(), + "tick", + ) + .await?; + Ok(()) +} + +pub async fn run_watchdog_non_genesis_divergence_test( + runtime: &mut ManagedSequencer, +) -> ScenarioResult<()> { + require_cartesi_machine(); + + let workspace = paths::workspace_root(); + let mismatch_image = workspace.join(SEPOLIA_MACHINE_IMAGE); + if !mismatch_image.is_dir() { + return Err(format!( + "sepolia machine image missing at {}; run: just canonical-build-machine-image-sepolia", + mismatch_image.display() + ) + .into()); + } + + eprintln!("[watchdog-harness] divergence step 1/3: wait for non-genesis GET /finalized_state"); + let finalized_url = format!("{}/finalized_state", runtime.endpoint()); + let (_status, _body, headers) = wait_for_non_genesis_finalized_state( + finalized_url.as_str(), + runtime, + Duration::from_secs(60), + ) + .await?; + let inclusion_block = header_u64(&headers, "x-inclusion-block") + .ok_or("finalized_state response missing X-Inclusion-Block header")?; + eprintln!("[watchdog-harness] divergence target inclusion_block={inclusion_block}"); + + eprintln!("[watchdog-harness] divergence step 2/3: initialize mismatched state and run tick"); + let state_dir = tempfile::tempdir() + .map_err(|err| format!("temp watchdog state dir: {err}"))? + .keep(); + run_lua_main_success( + runtime, + workspace.as_path(), + state_dir.as_path(), + mismatch_image.as_path(), + "init", + ) + .await?; + let output = run_lua_main( + runtime, + workspace.as_path(), + state_dir.as_path(), + mismatch_image.as_path(), + "tick", + ) + .await?; + let exit = output.status.code().unwrap_or(-1); + if exit != 2 { + return Err(format!( + "expected watchdog divergence exit 2, got {exit}; stderr: {}", + String::from_utf8_lossy(output.stderr.as_slice()) + ) + .into()); + } + + eprintln!("[watchdog-harness] divergence step 3/3: assert watchdog_event kind=state_mismatch"); + let stderr = String::from_utf8_lossy(output.stderr.as_slice()); + if !stderr.contains("watchdog_event") || !stderr.contains("\"kind\":\"state_mismatch\"") { + return Err(format!("missing state_mismatch watchdog_event in stderr: {stderr}").into()); + } + Ok(()) +} + +fn compare_env( + runtime: &ManagedSequencer, + state_dir: &Path, + machine_image: &Path, + lua_deps: &Path, +) -> Vec<(String, String)> { + vec![ + ( + "WATCHDOG_LUA_DEPS".into(), + lua_deps.to_string_lossy().into_owned(), + ), + ( + "WATCHDOG_SEQUENCER_URL".into(), + runtime.endpoint().to_string(), + ), + ( + "WATCHDOG_L1_RPC_URL".into(), + runtime.l1_endpoint().to_string(), + ), + ( + "WATCHDOG_INPUTBOX_ADDRESS".into(), + runtime.input_box_address().to_string(), + ), + ( + "WATCHDOG_APP_ADDRESS".into(), + runtime.app_address().to_string(), + ), + ( + "WATCHDOG_STATE_DIR".into(), + state_dir.to_string_lossy().into_owned(), + ), + ( + "WATCHDOG_CM_SNAPSHOT_DIR".into(), + machine_image.to_string_lossy().into_owned(), + ), + ( + "WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK".into(), + GENESIS_SAFE_BLOCK.into(), + ), + ] +} + +async fn run_lua_main( + runtime: &mut ManagedSequencer, + workspace: &Path, + state_dir: &Path, + machine_image: &Path, + command_name: &str, +) -> ScenarioResult { + let lua_deps = workspace.join(".deps/lua"); + ensure_lcurl(lua_deps.as_path())?; + let mut command = Command::new(workspace.join("watchdog/sequencer-watchdog")); + command + .current_dir(workspace) + .arg(command_name) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()); + command.env("WATCHDOG_LUA_ROOT", workspace); + command.env("WATCHDOG_LUA_BIN", "lua"); + for (key, value) in compare_env(runtime, state_dir, machine_image, lua_deps.as_path()) { + command.env(key, value); + } + command + .output() + .await + .map_err(|err| format!("failed to run sequencer-watchdog: {err}").into()) +} + +async fn run_lua_main_success( + runtime: &mut ManagedSequencer, + workspace: &Path, + state_dir: &Path, + machine_image: &Path, + command_name: &str, +) -> ScenarioResult<()> { + let output = run_lua_main(runtime, workspace, state_dir, machine_image, command_name).await?; + if !output.status.success() { + return Err(format!( + "sequencer-watchdog {command_name} exited with {}; stderr: {}", + output.status, + String::from_utf8_lossy(output.stderr.as_slice()) + ) + .into()); + } + let stdout = String::from_utf8_lossy(output.stdout.as_slice()); + if !stdout.is_empty() { + eprint!("{stdout}"); + } + Ok(()) +} + +fn ensure_lcurl(lua_deps: &Path) -> ScenarioResult<()> { + let lcurl_so = lua_deps.join("lcurl.so"); + if !lcurl_so.is_file() { + return Err(format!( + "lcurl.so missing at {}; run: just watchdog-lua-deps (needs libcurl + Lua dev headers)", + lcurl_so.display() + ) + .into()); + } + Ok(()) +} + +async fn prove_cm_inspect_genesis( + workspace: &Path, + machine_image: &Path, +) -> ScenarioResult> { + let work_dir = tempfile::tempdir().map_err(|err| format!("temp cm work dir: {err}"))?; + let query_path = work_dir.path().join("inspect-query.bin"); + let report_path = work_dir.path().join("inspect-report-0.bin"); + std::fs::write(query_path.as_path(), b"state") + .map_err(|err| format!("write inspect query: {err}"))?; + + let status = Command::new("cartesi-machine") + .current_dir(workspace) + .arg("--no-rollback") + .arg(format!("--load={},sharing:none", machine_image.display())) + .arg(format!( + "--cmio-inspect-state=query:{},report:{}", + query_path.display(), + report_path.display() + )) + .arg("--quiet") + .status() + .await + .map_err(|err| format!("cartesi-machine inspect failed to start: {err}"))?; + if !status.success() { + return Err(format!("cartesi-machine inspect exited with {status}").into()); + } + + let report = std::fs::read(report_path.as_path()) + .map_err(|err| format!("read inspect report: {err}"))?; + if report.starts_with(b"inspect endpoint not implemented".as_slice()) { + return Err( + "CM dapp is stale (inspect not implemented); rebuild with: just canonical-build-machine-image" + .into(), + ); + } + // Pre-SSZ images returned JSON from export_state (~27 bytes for empty wallet). + if report.first() == Some(&b'{') { + return Err(format!( + "CM inspect returned JSON ({} bytes), expected SSZ; rebuild devnet image: \ + just canonical-build-machine-image (report starts with {:?})", + report.len(), + String::from_utf8_lossy(&report[..report.len().min(40)]) + ) + .into()); + } + Ok(report) +} + +async fn http_get(url: &str) -> std::io::Result<(u16, Vec, Vec<(String, String)>)> { + let remainder = url.strip_prefix("http://").ok_or_else(|| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "only http:// supported in harness", + ) + })?; + let (host_port, path) = match remainder.split_once('/') { + Some((host_port, path)) => (host_port.to_string(), format!("/{path}")), + None => (remainder.to_string(), "/".to_string()), + }; + + let mut stream = TcpStream::connect(host_port.as_str()).await?; + let request = format!("GET {path} HTTP/1.1\r\nHost: {host_port}\r\nConnection: close\r\n\r\n"); + stream.write_all(request.as_bytes()).await?; + stream.flush().await?; + + let mut raw = Vec::new(); + stream.read_to_end(&mut raw).await?; + let header_end = raw + .windows(4) + .position(|w| w == b"\r\n\r\n") + .ok_or_else(|| std::io::Error::other("missing HTTP body"))?; + let header_bytes = &raw[..header_end]; + let body_raw = raw[header_end + 4..].to_vec(); + let header_text = std::str::from_utf8(header_bytes).map_err(std::io::Error::other)?; + let status = header_text + .lines() + .next() + .and_then(|line| line.split_whitespace().nth(1)) + .and_then(|code| code.parse().ok()) + .unwrap_or(500); + let header_pairs: Vec<(String, String)> = header_text + .lines() + .skip(1) + .filter_map(|line| line.split_once(": ")) + .map(|(name, value)| (name.to_ascii_lowercase(), value.to_string())) + .collect(); + let body = decode_response_body(&header_pairs, body_raw)?; + Ok((status, body, header_pairs)) +} + +/// Axum streams snapshot files without `Content-Length`, so bodies are often +/// `Transfer-Encoding: chunked`. Raw TCP clients must decode (lcurl does this +/// automatically; this harness must too). +fn decode_response_body(headers: &[(String, String)], body: Vec) -> std::io::Result> { + let chunked = headers.iter().any(|(name, value)| { + name == "transfer-encoding" && value.to_ascii_lowercase().contains("chunked") + }); + if chunked { + return decode_chunked_body(body.as_slice()); + } + if let Some(len) = headers + .iter() + .find(|(name, _)| name == "content-length") + .and_then(|(_, value)| value.parse::().ok()) + { + let mut out = body; + out.truncate(len.min(out.len())); + return Ok(out); + } + Ok(body) +} + +fn decode_chunked_body(mut input: &[u8]) -> std::io::Result> { + let mut out = Vec::new(); + loop { + let line_end = input + .iter() + .position(|&b| b == b'\n') + .ok_or_else(|| std::io::Error::other("chunked body: missing size line"))?; + let size_line = std::str::from_utf8(&input[..line_end]) + .map_err(std::io::Error::other)? + .trim_end_matches('\r'); + let chunk_size = usize::from_str_radix(size_line, 16).map_err(std::io::Error::other)?; + input = &input[line_end + 1..]; + if chunk_size == 0 { + break; + } + if input.len() < chunk_size + 2 { + return Err(std::io::Error::other("chunked body: truncated chunk")); + } + out.extend_from_slice(&input[..chunk_size]); + input = &input[chunk_size + 2..]; + } + Ok(out) +} + +fn body_snippet_for_error(body: &[u8]) -> String { + if body.is_empty() { + return "(empty body)".to_string(); + } + match std::str::from_utf8(body) { + Ok(text) if text.len() <= 512 => text.to_string(), + Ok(text) => format!("{}…", &text[..512.min(text.len())]), + Err(_) => format!("{} binary octets", body.len()), + } +} + +fn header_u64(headers: &[(String, String)], name: &str) -> Option { + headers + .iter() + .find(|(key, _)| key == name) + .and_then(|(_, value)| value.parse().ok()) +} + +async fn wait_for_non_genesis_finalized_state( + url: &str, + runtime: &ManagedSequencer, + deadline: Duration, +) -> ScenarioResult<(u16, Vec, Vec<(String, String)>)> { + let started = std::time::Instant::now(); + let mut last = String::new(); + while started.elapsed() < deadline { + match http_get(url).await { + Ok((200, body, headers)) => { + let inclusion_block = header_u64(&headers, "x-inclusion-block") + .ok_or("finalized_state response missing X-Inclusion-Block header")?; + if inclusion_block > 0 { + return Ok((200, body, headers)); + } + last = format!("inclusion_block still at 0 (snapshot_bytes={})", body.len()); + } + Ok((404, body, _)) => { + last = body_snippet_for_error(body.as_slice()); + } + Ok((status, body, _)) => { + return Err(format!( + "GET /finalized_state returned HTTP {status}: {}", + body_snippet_for_error(body.as_slice()) + ) + .into()); + } + Err(err) => { + last = err.to_string(); + } + } + runtime + .mine_l1_blocks(1) + .await + .map_err(|err| format!("mine while waiting for non-genesis finalized state: {err}"))?; + tokio::time::sleep(Duration::from_secs(6)).await; + } + Err(format!( + "timed out waiting for GET /finalized_state with inclusion_block > 0; last: {last}" + ) + .into()) +} + +async fn wait_for_finalized_state( + url: &str, + deadline: Duration, +) -> ScenarioResult<(u16, Vec, Vec<(String, String)>)> { + let started = std::time::Instant::now(); + let mut last = String::new(); + while started.elapsed() < deadline { + match http_get(url).await { + Ok((200, body, headers)) => return Ok((200, body, headers)), + Ok((404, body, _)) => { + last = body_snippet_for_error(body.as_slice()); + } + Ok((status, body, _)) => { + return Err(format!( + "GET /finalized_state returned HTTP {status}: {}", + body_snippet_for_error(body.as_slice()) + ) + .into()); + } + Err(err) => { + last = err.to_string(); + } + } + tokio::time::sleep(Duration::from_millis(200)).await; + } + Err(format!("timed out waiting for GET /finalized_state 200; last response: {last}").into()) +} diff --git a/tests/fixtures/l1_partition_vector.json b/tests/fixtures/l1_partition_vector.json new file mode 100644 index 0000000..18bdf3f --- /dev/null +++ b/tests/fixtures/l1_partition_vector.json @@ -0,0 +1,104 @@ +{ + "long_block_range_error_codes": ["-32005", "-32600", "-32602", "-32616"], + "scenarios": [ + { + "name": "bisect_on_default_infura_code", + "start_block": 1, + "end_block": 4, + "fail_ranges": [ + { + "from": 1, + "to": 4, + "message": "RPC error -32005: query returned more than allowed" + } + ], + "expect_calls": [[1, 4], [1, 2], [3, 4]], + "expect_ok": true + }, + { + "name": "single_range_success", + "start_block": 10, + "end_block": 12, + "fail_ranges": [], + "expect_calls": [[10, 12]], + "expect_ok": true + }, + { + "name": "empty_range_no_calls", + "start_block": 5, + "end_block": 4, + "fail_ranges": [], + "expect_calls": [], + "expect_ok": true + }, + { + "name": "non_retryable_error_aborts", + "start_block": 1, + "end_block": 4, + "fail_ranges": [ + { + "from": 1, + "to": 4, + "message": "connection refused" + } + ], + "expect_calls": [[1, 4]], + "expect_ok": false + }, + { + "name": "nested_bisect_alchemy_code", + "start_block": 1, + "end_block": 8, + "fail_ranges": [ + { "from": 1, "to": 8, "message": "error -32600: block range too large" }, + { "from": 1, "to": 4, "message": "error -32600: block range too large" }, + { "from": 1, "to": 2, "message": "error -32600: block range too large" } + ], + "expect_calls": [ + [1, 8], + [1, 4], + [1, 2], + [1, 1], + [2, 2], + [3, 4], + [5, 8] + ], + "expect_ok": true + }, + { + "name": "single_block_fail_no_split", + "start_block": 7, + "end_block": 7, + "fail_ranges": [ + { + "from": 7, + "to": 7, + "message": "error -32616: range limit" + } + ], + "expect_calls": [[7, 7]], + "expect_ok": false + } + ], + "log_sort": { + "unsorted": [ + { + "blockNumber": "0x2", + "transactionIndex": "0x0", + "logIndex": "0x5" + }, + { + "blockNumber": "0x1", + "transactionIndex": "0x9", + "logIndex": "0x0" + }, + { + "blockNumber": "0x2", + "transactionIndex": "0x0", + "logIndex": "0x1" + } + ], + "expect_block_order": ["0x1", "0x2", "0x2"], + "expect_log_index_order": ["0x0", "0x1", "0x5"] + } +} diff --git a/tests/fixtures/wallet_snapshot_v1_empty.bin b/tests/fixtures/wallet_snapshot_v1_empty.bin new file mode 100644 index 0000000..047814d Binary files /dev/null and b/tests/fixtures/wallet_snapshot_v1_empty.bin differ diff --git a/tests/fixtures/wallet_snapshot_v1_empty.hex b/tests/fixtures/wallet_snapshot_v1_empty.hex new file mode 100644 index 0000000..624ddd1 --- /dev/null +++ b/tests/fixtures/wallet_snapshot_v1_empty.hex @@ -0,0 +1 @@ +aca6586a0cf05bd831f2501e7b4aea550da6562d1c7d4b196cb0c7b01d743fbc6116a902379c723816d5ff3fdd14e2a86fba77cbce6b3cd9c32b8ff34c0000004c0000000000000000000000 diff --git a/tests/harness/src/lib.rs b/tests/harness/src/lib.rs index a528461..d7dcbc0 100644 --- a/tests/harness/src/lib.rs +++ b/tests/harness/src/lib.rs @@ -18,6 +18,7 @@ pub use rollups::{DEVNET_CHAIN_ID, DevnetRollupsStack}; pub use sequencer::{ BatchCounts, DEFAULT_DEVNET_SEQUENCER_BIN, DEFAULT_TEST_LOGS_DIR, ManagedSequencer, ManagedSequencerConfig, RespawnAttemptOutcome, RespawnPolicy, default_devnet_sequencer_config, + devnet_sequencer_config_no_faketime, }; pub use wallet::{ TestSigner, WalletL1Client, WalletL2Client, address_from_signing_key, sign_user_op_hex, diff --git a/tests/harness/src/paths.rs b/tests/harness/src/paths.rs index 5098069..e0de961 100644 --- a/tests/harness/src/paths.rs +++ b/tests/harness/src/paths.rs @@ -38,3 +38,30 @@ pub fn mock_erc20_artifact_path() -> PathBuf { pub fn devnet_machine_image_path() -> PathBuf { workspace_root().join(DEFAULT_DEVNET_MACHINE_IMAGE_PATH) } + +const DEVNET_SEQUENCER_BIN: &str = "sequencer-devnet"; + +/// Resolve the `sequencer-devnet` binary built for the current Cargo invocation. +/// +/// Prefers `CARGO_TARGET_DIR` (set by `cargo run` / `cargo test` in sandboxes and +/// custom target dirs) over the workspace `target/debug/` tree, which may be stale +/// when builds only run through Cargo with a redirected target directory. +pub fn resolve_devnet_sequencer_bin() -> PathBuf { + if let Ok(path) = std::env::var("CARGO_BIN_EXE_SEQUENCER_DEVNET") { + let path = PathBuf::from(path); + if path.exists() { + return path; + } + } + if let Ok(target) = std::env::var("CARGO_TARGET_DIR") { + let path = PathBuf::from(target) + .join("debug") + .join(DEVNET_SEQUENCER_BIN); + if path.exists() { + return path; + } + } + workspace_root() + .join("target/debug") + .join(DEVNET_SEQUENCER_BIN) +} diff --git a/tests/harness/src/sequencer.rs b/tests/harness/src/sequencer.rs index f1e5f09..7f402df 100644 --- a/tests/harness/src/sequencer.rs +++ b/tests/harness/src/sequencer.rs @@ -33,6 +33,9 @@ pub struct ManagedSequencerConfig { pub sequencer_bin: PathBuf, pub log_prefix: String, pub logs_dir: PathBuf, + /// When false, the child runs without libfaketime (for tests that never + /// manipulate wall clock). Requires libfaketime in PATH when true. + pub faketime: bool, } /// Snapshot of the `batches` table. Returned by @@ -95,13 +98,12 @@ pub struct ManagedSequencer { /// When `None`, defaults to `DEVNET_CHAIN_ID` (matches Anvil). Set to /// a non-matching value to test chain-id-mismatch failure modes chain_id_override: Option, - /// Path to the file libfaketime re-reads for its offset, on every time - /// call (combined with `FAKETIME_NO_CACHE=1`). Writing to this file - /// shifts the sequencer's view of `SystemTime::now()` / `Instant::now()` - /// immediately — no respawn needed. - faketime_rc_path: PathBuf, - /// Cached libfaketime dylib/so path (computed once on spawn). - libfaketime_path: PathBuf, + /// Cached libfaketime dylib/so path (computed once on spawn). `None` when + /// [`ManagedSequencerConfig::faketime`] is false. + libfaketime_path: Option, + /// Path to the file libfaketime re-reads for its offset. `None` when + /// faketime is disabled. + faketime_rc_path: Option, /// Internal cumulative forward-offset tracker for /// [`Self::advance_wall_and_mine`]. Not touched by /// [`Self::set_faketime_offset`]. @@ -110,16 +112,31 @@ pub struct ManagedSequencer { pub fn default_devnet_sequencer_config(log_prefix: impl Into) -> ManagedSequencerConfig { ManagedSequencerConfig { - sequencer_bin: PathBuf::from(DEFAULT_DEVNET_SEQUENCER_BIN), + sequencer_bin: paths::resolve_devnet_sequencer_bin(), log_prefix: log_prefix.into(), logs_dir: PathBuf::from(DEFAULT_TEST_LOGS_DIR), + faketime: true, + } +} + +/// Devnet config without libfaketime (watchdog compare and other wall-clock-neutral tests). +pub fn devnet_sequencer_config_no_faketime( + log_prefix: impl Into, +) -> ManagedSequencerConfig { + ManagedSequencerConfig { + faketime: false, + ..default_devnet_sequencer_config(log_prefix) } } impl ManagedSequencer { pub async fn spawn(config: ManagedSequencerConfig) -> HarnessResult { let logs_dir = paths::resolve_from_workspace(&config.logs_dir); - let sequencer_bin = paths::resolve_from_workspace(&config.sequencer_bin); + let sequencer_bin = if config.sequencer_bin.is_absolute() { + config.sequencer_bin.clone() + } else { + paths::resolve_from_workspace(&config.sequencer_bin) + }; let log_prefix = config.log_prefix; let rollups = DevnetRollupsStack::spawn(log_prefix.as_str(), logs_dir.as_path()).await?; @@ -128,14 +145,15 @@ impl ManagedSequencer { .map_err(|err| io_other(format!("failed to create temp data dir: {err}")))?; let data_dir_path = data_dir.path().to_path_buf(); - // Set up faketime: locate libfaketime + create the rc file. Initial - // content `+0` means no offset; tests can overwrite with a new offset - // at any time and the running sequencer will see it on its next - // `SystemTime::now()` / `Instant::now()` call (FAKETIME_NO_CACHE=1). - let libfaketime_path = find_libfaketime()?; - let faketime_rc_path = data_dir_path.join("faketime.rc"); - fs::write(faketime_rc_path.as_path(), "+0\n") - .map_err(|err| io_other(format!("create faketime rc file: {err}")))?; + let (libfaketime_path, faketime_rc_path) = if config.faketime { + let libfaketime_path = find_libfaketime()?; + let faketime_rc_path = data_dir_path.join("faketime.rc"); + fs::write(faketime_rc_path.as_path(), "+0\n") + .map_err(|err| io_other(format!("create faketime rc file: {err}")))?; + (Some(libfaketime_path), Some(faketime_rc_path)) + } else { + (None, None) + }; let SpawnedSequencerProcess { child, @@ -149,8 +167,8 @@ impl ManagedSequencer { &rollups, None, None, - libfaketime_path.as_path(), - faketime_rc_path.as_path(), + libfaketime_path.as_deref(), + faketime_rc_path.as_deref(), ) .await?; @@ -210,8 +228,12 @@ impl ManagedSequencer { /// Replaces any cumulative advance tracked by /// [`Self::advance_wall_and_mine`], and resets its counter. pub fn set_faketime_offset(&mut self, offset: Option) -> HarnessResult<()> { + let rc = self + .faketime_rc_path + .as_ref() + .ok_or_else(|| io_other("faketime is disabled for this ManagedSequencer"))?; let s = offset.as_deref().unwrap_or("+0"); - fs::write(self.faketime_rc_path.as_path(), format!("{s}\n")) + fs::write(rc.as_path(), format!("{s}\n")) .map_err(|err| io_other(format!("write faketime rc file: {err}")))?; self.cumulative_offset_secs = 0; Ok(()) @@ -462,6 +484,10 @@ impl ManagedSequencer { self.rollups.app_address() } + pub fn input_box_address(&self) -> Address { + self.rollups.input_box_address() + } + pub fn erc20_portal_address(&self) -> Address { self.rollups.erc20_portal_address() } @@ -522,11 +548,12 @@ impl ManagedSequencer { let blocks = secs / SECONDS_PER_BLOCK; self.mine_l1_blocks(blocks).await?; self.cumulative_offset_secs = self.cumulative_offset_secs.saturating_add(secs); - fs::write( - self.faketime_rc_path.as_path(), - format!("+{}s\n", self.cumulative_offset_secs), - ) - .map_err(|err| io_other(format!("write faketime rc file: {err}")))?; + let rc = self + .faketime_rc_path + .as_ref() + .ok_or_else(|| io_other("faketime is disabled for this ManagedSequencer"))?; + fs::write(rc.as_path(), format!("+{}s\n", self.cumulative_offset_secs)) + .map_err(|err| io_other(format!("write faketime rc file: {err}")))?; Ok(()) } @@ -682,8 +709,8 @@ impl ManagedSequencer { &self.rollups, self.l1_endpoint_override.as_deref(), self.chain_id_override, - self.libfaketime_path.as_path(), - self.faketime_rc_path.as_path(), + self.libfaketime_path.as_deref(), + self.faketime_rc_path.as_deref(), ) .await?; self.child = child; @@ -772,8 +799,8 @@ async fn spawn_sequencer_process( rollups: &DevnetRollupsStack, l1_endpoint_override: Option<&str>, chain_id_override: Option, - libfaketime_path: &Path, - faketime_rc_path: &Path, + libfaketime_path: Option<&Path>, + faketime_rc_path: Option<&Path>, ) -> HarnessResult { let (endpoint, http_addr) = build_local_endpoint()?; let log_path = timestamped_log_path(logs_dir, log_prefix); @@ -796,7 +823,9 @@ async fn spawn_sequencer_process( // `Instant::now()` call thanks to FAKETIME_NO_CACHE=1, so tests can // shift the clock dynamically during a run. let mut cmd = Command::new(path_as_str(sequencer_bin)?); - apply_faketime_env(&mut cmd, libfaketime_path, faketime_rc_path)?; + if let (Some(lib), Some(rc)) = (libfaketime_path, faketime_rc_path) { + apply_faketime_env(&mut cmd, lib, rc)?; + } let chain_id = chain_id_override.unwrap_or(DEVNET_CHAIN_ID); let mut child = cmd diff --git a/toolchain-pins.env b/toolchain-pins.env new file mode 100644 index 0000000..98e745e --- /dev/null +++ b/toolchain-pins.env @@ -0,0 +1,18 @@ +# Single source of truth for CI + release toolchain pins. +# Workflows load this via .github/actions/load-toolchain-pins. +# Release tag (git v*) is the sequencer / watchdog / CM image bundle version. + +RUST_TOOLCHAIN=1.95.0 + +XGENEXT2FS_VERSION=v1.5.6 +XGENEXT2FS_SHA256_AMD64=996e4e68a638b5dc5967d3410f92ecb8d2f41e32218bbe0f8b4c4474d7eebc59 +XGENEXT2FS_SHA256_ARM64=e5aca81164b762bbe5447bacef41e4fa9e357fd9c8f44e519c5206227d43144d + +# Must match the cartesi-machine used to build canonical-machine-image-* tarballs +# and the watchdog OCI image (in-process cartesi Lua module + CLI). +CARTESI_MACHINE_VERSION=v0.20.0-test2 +CARTESI_MACHINE_SHA256_AMD64=39bbfc96a6cc6606307294b719df65f4f2725e8d200d062bcbd8c22355b99b56 +CARTESI_MACHINE_SHA256_ARM64=787d823756000cdecd72da8a3494b4c08613087379035959e561bbaef7a220ba + +# lua-cURLv3 is vendored in-tree under watchdog/third_party/lua-curl/src (no pin +# to track -- the compiled bytes are the in-tree source). See its UPSTREAM file. diff --git a/watchdog/Dockerfile b/watchdog/Dockerfile new file mode 100644 index 0000000..85b847e --- /dev/null +++ b/watchdog/Dockerfile @@ -0,0 +1,104 @@ +# Watchdog one-cycle runtime image. +# Build args RELEASE_TAG, GIT_COMMIT, and cartesi-machine pins must match toolchain-pins.env +# and the canonical-machine-image-* tarballs produced in the same release. + +# cartesi-machine v0.20.x libs require glibc >= 2.38 (bookworm is 2.36). +ARG DEBIAN_VERSION=trixie-slim +FROM debian:${DEBIAN_VERSION} AS build + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] + +ARG CARTESI_MACHINE_VERSION +ARG CARTESI_MACHINE_DEB_SHA256 +ARG TARGETARCH + +# wget + ca-certificates fetch the cartesi-machine .deb; gcc/pkg-config + Lua +# headers compile the vendored lcurl.so. lua-curl is vendored in-tree under +# watchdog/third_party/lua-curl/src, so the build needs no network for it. +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + wget \ + gcc \ + pkg-config \ + lua5.4 \ + liblua5.4-dev \ + libcurl4-openssl-dev \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /build + +COPY scripts/watchdog-lua-deps.sh scripts/watchdog-lua-deps.sh +COPY watchdog/third_party watchdog/third_party + +RUN bash scripts/watchdog-lua-deps.sh + +RUN case "${TARGETARCH}" in \ + amd64) CM_ARCH=amd64 ;; \ + arm64) CM_ARCH=arm64 ;; \ + *) echo "unsupported TARGETARCH=${TARGETARCH}" >&2; exit 1 ;; \ + esac \ + && wget -O /tmp/machine-emulator.deb \ + "https://github.com/cartesi/machine-emulator/releases/download/${CARTESI_MACHINE_VERSION}/machine-emulator_${CM_ARCH}.deb" \ + && echo "${CARTESI_MACHINE_DEB_SHA256} /tmp/machine-emulator.deb" | sha256sum --check \ + && dpkg-deb -x /tmp/machine-emulator.deb /tmp/cm \ + && mkdir -p /out/bin /out/lib /out/lib/lua/5.4 /out/share/cartesi-machine /out/share/lua/5.4 \ + && cp -a /tmp/cm/usr/bin/cartesi-machine /out/bin/ \ + && cp -a /tmp/cm/usr/share/cartesi-machine/. /out/share/cartesi-machine/ \ + && cp -a /tmp/cm/usr/lib/lua/5.4/. /out/lib/lua/5.4/ \ + && cp -a /tmp/cm/usr/share/lua/5.4/. /out/share/lua/5.4/ \ + && cp /tmp/cm/usr/lib/libcartesi*.so /out/lib/ \ + && cp /build/.deps/lua/lcurl.so /out/lib/lcurl.so + +FROM debian:${DEBIAN_VERSION} + +ARG RELEASE_TAG +ARG GIT_COMMIT +ARG CARTESI_MACHINE_VERSION + +LABEL org.opencontainers.image.title="sequencer-watchdog" \ + org.opencontainers.image.version="${RELEASE_TAG}" \ + org.opencontainers.image.revision="${GIT_COMMIT}" \ + org.cartesi.sequencer.release-tag="${RELEASE_TAG}" \ + org.cartesi.cartesi-machine.version="${CARTESI_MACHINE_VERSION}" + +RUN apt-get update && apt-get install -y --no-install-recommends \ + lua5.4 \ + libcurl4 \ + libslirp0 \ + libgomp1 \ + ca-certificates \ + util-linux \ + && rm -rf /var/lib/apt/lists/* + +COPY --from=build /out/bin/cartesi-machine /usr/local/bin/cartesi-machine +COPY --from=build /out/share/cartesi-machine /usr/local/share/cartesi-machine +COPY --from=build /out/lib/lcurl.so /opt/watchdog/lib/lcurl.so +COPY --from=build /out/lib/lua/5.4/ /usr/local/lib/lua/5.4/ +COPY --from=build /out/share/lua/5.4/ /usr/local/share/lua/5.4/ +COPY --from=build /out/share/lua/5.4/ /usr/share/lua/5.4/ +COPY --from=build /out/lib/libcartesi*.so /usr/local/lib/ + +COPY watchdog /opt/watchdog/lua/watchdog +COPY watchdog/third_party/json.lua /opt/watchdog/lua/watchdog/third_party/json.lua +# Shared cross-stack fixtures (watchdog/tests/run.lua expects repo-root tests/fixtures/). +COPY tests/fixtures /opt/watchdog/lua/tests/fixtures +COPY watchdog/sequencer-watchdog /usr/local/bin/sequencer-watchdog + +RUN chmod +x /usr/local/bin/sequencer-watchdog /usr/local/bin/cartesi-machine \ + && printf '%s\n' \ + "{" \ + " \"component\": \"sequencer-watchdog\"," \ + " \"release_tag\": \"${RELEASE_TAG}\"," \ + " \"git_commit\": \"${GIT_COMMIT}\"," \ + " \"cartesi_machine_version\": \"${CARTESI_MACHINE_VERSION}\"" \ + "}" > /opt/watchdog/RELEASE.json + +ENV WATCHDOG_LUA_DEPS=/opt/watchdog/lib \ + LUA_PATH="/opt/watchdog/lua/?.lua;/opt/watchdog/lua/?/init.lua;/usr/local/share/lua/5.4/?.lua;/usr/local/share/lua/5.4/?/init.lua;;" \ + LUA_CPATH="/opt/watchdog/lib/?.so;/usr/local/lib/lua/5.4/?.so;;" \ + LD_LIBRARY_PATH="/usr/local/lib" \ + PATH="/usr/local/share/cartesi-machine:${PATH}" + +WORKDIR /opt/watchdog +ENTRYPOINT ["/usr/local/bin/sequencer-watchdog"] +CMD [] diff --git a/watchdog/README.md b/watchdog/README.md new file mode 100644 index 0000000..e370514 --- /dev/null +++ b/watchdog/README.md @@ -0,0 +1,16 @@ +# Watchdog (Lua) + +Off-chain sidecar that compares the sequencer's finalized SSZ snapshot to state from the canonical Cartesi Machine. + +**Documentation:** [`docs/watchdog/operator-deployment.md`](../docs/watchdog/operator-deployment.md) (Sepolia / mainnet) · [`docs/watchdog/getting-started.md`](../docs/watchdog/getting-started.md) (local devnet) · [`docs/watchdog/README.md`](../docs/watchdog/README.md) (architecture) + +```bash +just doctor # lua + cartesi + lcurl + machine_cartesi load probe +just watchdog-lua-deps # .deps/lua/lcurl.so (needs libcurl + liblua5.4-dev) +just test-watchdog # unit tests (mocked HTTP; no lcurl required) +just devnet-for-watchdog # local Anvil + sequencer-devnet (prints WATCHDOG_* env) +``` + +Watchdog-local recipes also live in [`justfile`](justfile) (`just -f watchdog/justfile `). + +Host packages and build errors: [`docs/watchdog/README.md`](../docs/watchdog/README.md#host-dependencies-watchdog-lua-deps). diff --git a/watchdog/abi.lua b/watchdog/abi.lua new file mode 100644 index 0000000..0c423bd --- /dev/null +++ b/watchdog/abi.lua @@ -0,0 +1,138 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local abi = {} + +local WORD_HEX_LEN = 64 + +local function strip_0x(value) + assert(type(value) == "string", "hex value must be a string") + if value:sub(1, 2) == "0x" or value:sub(1, 2) == "0X" then + return value:sub(3) + end + return value +end + +local function assert_hex(value) + if value:match("^[0-9a-fA-F]*$") == nil then + error("invalid hex string") + end +end + +local function word_at(hex, index) + local start = (index * WORD_HEX_LEN) + 1 + local word = hex:sub(start, start + WORD_HEX_LEN - 1) + if #word ~= WORD_HEX_LEN then + error("ABI word out of bounds") + end + return word +end + +local function uint_word_to_number(word) + local value = 0 + for i = 1, #word do + local nibble = tonumber(word:sub(i, i), 16) + value = (value * 16) + nibble + if value > 9007199254740991 then + error("uint value too large for precise Lua number") + end + end + return value +end + +local function uint_word_to_hex(word) + local stripped = word:gsub("^0+", "") + if stripped == "" then + return "0x0" + end + return "0x" .. stripped:lower() +end + +local function address_from_word(word) + if word:sub(1, 24) ~= string.rep("0", 24) then + error("address word has non-zero high bytes") + end + return "0x" .. word:sub(25):lower() +end + +function abi.bytes_from_hex(hex) + hex = strip_0x(hex) + assert_hex(hex) + if (#hex % 2) ~= 0 then + error("hex string must have even length") + end + + return (hex:gsub("..", function(byte) + return string.char(tonumber(byte, 16)) + end)) +end + +function abi.hex_from_bytes(bytes) + return (bytes:gsub(".", function(char) + return string.format("%02x", char:byte()) + end)) +end + +function abi.decode_single_dynamic_bytes(encoded) + local hex = strip_0x(encoded) + assert_hex(hex) + local offset = uint_word_to_number(word_at(hex, 0)) + if (offset % 32) ~= 0 then + error("dynamic bytes offset is not word-aligned") + end + + local offset_words = offset // 32 + local len = uint_word_to_number(word_at(hex, offset_words)) + local data_hex_start = ((offset_words + 1) * WORD_HEX_LEN) + 1 + local data_hex = hex:sub(data_hex_start, data_hex_start + (len * 2) - 1) + if #data_hex ~= len * 2 then + error("dynamic bytes payload out of bounds") + end + return abi.bytes_from_hex(data_hex) +end + +function abi.decode_evm_advance_call(encoded) + local hex = strip_0x(encoded) + assert_hex(hex) + + -- EvmAdvanceCall is calldata, so accept and skip the 4-byte selector. + if (#hex % WORD_HEX_LEN) == 8 then + hex = hex:sub(9) + end + + local payload_offset = uint_word_to_number(word_at(hex, 7)) + if (payload_offset % 32) ~= 0 then + error("payload offset is not word-aligned") + end + + local payload_offset_words = payload_offset // 32 + local payload_len = uint_word_to_number(word_at(hex, payload_offset_words)) + local payload_hex_start = ((payload_offset_words + 1) * WORD_HEX_LEN) + 1 + local payload_hex = hex:sub(payload_hex_start, payload_hex_start + (payload_len * 2) - 1) + if #payload_hex ~= payload_len * 2 then + error("payload out of bounds") + end + + return { + chain_id_hex = uint_word_to_hex(word_at(hex, 0)), + app_contract = address_from_word(word_at(hex, 1)), + msg_sender = address_from_word(word_at(hex, 2)), + block_number = uint_word_to_number(word_at(hex, 3)), + block_timestamp_hex = uint_word_to_hex(word_at(hex, 4)), + prev_randao_hex = uint_word_to_hex(word_at(hex, 5)), + index_hex = uint_word_to_hex(word_at(hex, 6)), + payload = abi.bytes_from_hex(payload_hex), + } +end + +function abi.decode_input_added_log(log) + if type(log) ~= "table" or type(log.data) ~= "string" then + error("log.data is required") + end + local input = abi.decode_single_dynamic_bytes(log.data) + local decoded = abi.decode_evm_advance_call(abi.hex_from_bytes(input)) + decoded.raw_input = input + return decoded +end + +return abi diff --git a/watchdog/checkpoint.lua b/watchdog/checkpoint.lua new file mode 100644 index 0000000..b89141f --- /dev/null +++ b/watchdog/checkpoint.lua @@ -0,0 +1,224 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local checkpoint = {} + +checkpoint.HEAD_FILE = "head.json" + +local function join(...) + local parts = { ... } + return table.concat(parts, "/"):gsub("//+", "/") +end + +local function read_all(path) + local file, err = io.open(path, "rb") + if not file then + return nil, err + end + local data = file:read("*a") + file:close() + return data +end + +local function write_all(path, data) + local file, err = io.open(path, "wb") + if not file then + return nil, err + end + local ok, write_err = file:write(data) + if not ok then + file:close() + return nil, write_err + end + ok, err = file:close() + if not ok then + return nil, err + end + return true +end + +local function path_exists(path) + local ok, _, code = os.rename(path, path) + return ok or code == 13 +end + +local function shell_quote(value) + value = tostring(value) + return "'" .. value:gsub("'", "'\\''") .. "'" +end + +local function json_escape(value) + return value:gsub("\\", "\\\\"):gsub('"', '\\"'):gsub("\n", "\\n") +end + +local function manifest_json(manifest) + local fields = { + string.format('"safe_block":%d', manifest.safe_block), + string.format('"created_at":"%s"', json_escape(manifest.created_at or os.date("!%Y-%m-%dT%H:%M:%SZ"))), + } + if manifest.cm_image_hash then + table.insert(fields, string.format('"cm_image_hash":"%s"', json_escape(manifest.cm_image_hash))) + end + return "{" .. table.concat(fields, ",") .. "}\n" +end + +local function pointer_json(relative_path) + return string.format('{"checkpoint":"%s"}\n', json_escape(relative_path)) +end + +local function parse_pointer(data) + return data:match('"checkpoint"%s*:%s*"([^"]+)"') +end + +local function validate_pointer(relative_path) + if type(relative_path) ~= "string" or not relative_path:match("^checkpoints/%d+$") then + return nil, "invalid checkpoint pointer" + end + return relative_path +end + +-- Relative path head.json points at (e.g. "checkpoints/000...019"), or nil +-- if there is no pointer yet. Used by write() to find the checkpoint it +-- supersedes without enumerating the directory. +local function read_pointer(dir) + local data = read_all(join(dir, checkpoint.HEAD_FILE)) + if not data then + return nil + end + return validate_pointer(parse_pointer(data)) +end + +local function relative_checkpoint_path(safe_block) + return join("checkpoints", string.format("%020d", safe_block)) +end + +function checkpoint.safe_block_from_manifest(manifest_data) + local value = tostring(manifest_data or ""):match('"safe_block"%s*:%s*(%d+)') + if not value then + return nil, "manifest missing safe_block" + end + return tonumber(value) +end + +function checkpoint.load(dir) + local pointer_path = join(dir, checkpoint.HEAD_FILE) + if not path_exists(pointer_path) then + return nil, "missing " .. checkpoint.HEAD_FILE + end + + local pointer_data, err = read_all(pointer_path) + if not pointer_data then + return nil, err + end + local relative_path = parse_pointer(pointer_data) + local pointer_err + relative_path, pointer_err = validate_pointer(relative_path) + if not relative_path then + return nil, pointer_err + end + local checkpoint_dir = join(dir, relative_path) + local manifest, manifest_err = read_all(join(checkpoint_dir, "manifest.json")) + if not manifest then + return nil, manifest_err + end + local safe_block, safe_block_err = checkpoint.safe_block_from_manifest(manifest) + if not safe_block then + return nil, safe_block_err + end + return { + path = checkpoint_dir, + manifest_json = manifest, + safe_block = safe_block, + snapshot_dir = join(checkpoint_dir, "snapshot"), + } +end + +function checkpoint.prepare(dir, safe_block) + assert(type(safe_block) == "number", "safe_block must be a number") + + local relative_path = relative_checkpoint_path(safe_block) + local full_path = join(dir, relative_path) + local snapshot_dir = join(full_path, "snapshot") + + -- Crash mid-store can leave a half-written snapshot; clear before retrying this block. + local rm_ok = os.execute("rm -rf " .. shell_quote(snapshot_dir)) + if rm_ok ~= true and rm_ok ~= 0 then + return nil, "rm failed: " .. snapshot_dir + end + + local ok = os.execute("mkdir -p " .. shell_quote(full_path)) + if ok ~= true and ok ~= 0 then + return nil, "mkdir failed: " .. full_path + end + + return { + path = full_path, + snapshot_dir = snapshot_dir, + relative_path = relative_path, + } +end + +function checkpoint.commit_prepared(dir, prepared, safe_block, manifest) + assert(type(prepared) == "table", "prepared checkpoint is required") + assert(type(safe_block) == "number", "safe_block must be a number") + manifest = manifest or {} + manifest.safe_block = safe_block + + local ok, err = write_all(join(prepared.path, "manifest.json"), manifest_json(manifest)) + if not ok then + return nil, err + end + + local tmp_pointer = join(dir, checkpoint.HEAD_FILE .. ".tmp") + ok, err = write_all(tmp_pointer, pointer_json(prepared.relative_path)) + if not ok then + return nil, err + end + ok, err = os.rename(tmp_pointer, join(dir, checkpoint.HEAD_FILE)) + if not ok then + return nil, err + end + + return { + path = prepared.path, + snapshot_dir = prepared.snapshot_dir, + } +end + +function checkpoint.write(dir, safe_block, snapshot_writer, manifest) + assert(type(snapshot_writer) == "function", "snapshot_writer must be a function") + local relative_path = relative_checkpoint_path(safe_block) + local superseded = read_pointer(dir) + if superseded == relative_path then + return nil, "refusing to rewrite selected checkpoint: " .. relative_path + end + + local prepared, prepare_err = checkpoint.prepare(dir, safe_block) + if not prepared then + return nil, prepare_err + end + local ok, err = snapshot_writer(prepared.snapshot_dir) + if not ok then + return nil, err + end + + -- The checkpoint head.json points at before the flip is the one we are + -- superseding (nil on the first write). + + local committed, commit_err = checkpoint.commit_prepared(dir, prepared, safe_block, manifest) + if not committed then + return nil, commit_err + end + + -- Reclaim the superseded checkpoint AFTER the atomic pointer flip, so + -- head.json always names a complete checkpoint (a crash here leaves at + -- most one stale dir, never an orphaned pointer). Best-effort: GC must not + -- fail a cycle whose compare and checkpoint write already succeeded. + if superseded and superseded ~= prepared.relative_path then + os.execute("rm -rf " .. shell_quote(join(dir, superseded))) + end + + return committed +end + +return checkpoint diff --git a/watchdog/compare.lua b/watchdog/compare.lua new file mode 100644 index 0000000..927da56 --- /dev/null +++ b/watchdog/compare.lua @@ -0,0 +1,36 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local compare = {} + +function compare.first_mismatch_offset(a, b) + assert(type(a) == "string", "left value must be a string") + assert(type(b) == "string", "right value must be a string") + + local limit = math.min(#a, #b) + for i = 1, limit do + if a:byte(i) ~= b:byte(i) then + return i + end + end + + if #a ~= #b then + return limit + 1 + end + + return nil +end + +function compare.raw_equal(expected, actual) + local mismatch = compare.first_mismatch_offset(expected, actual) + return mismatch == nil, mismatch +end + +function compare.assert_state_response(state) + if type(state) ~= "string" or #state == 0 then + return nil, "state must be a non-empty string" + end + return true +end + +return compare diff --git a/watchdog/config.lua b/watchdog/config.lua new file mode 100644 index 0000000..76c694d --- /dev/null +++ b/watchdog/config.lua @@ -0,0 +1,147 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local config = {} + +config.VERSION = 1 + +local function required(name, env) + local value = env[name] + if value == nil or value == "" then + error(name .. " is required") + end + return value +end + +local function optional_number(name, default, env) + local value = env[name] + if value == nil or value == "" then + return default + end + local parsed = tonumber(value) + if not parsed then + error(name .. " must be a number") + end + return parsed +end + +local function optional_required_number(value_name, number_name, env) + if env[value_name] == nil or env[value_name] == "" then + return nil + end + local value = env[number_name] + if value == nil or value == "" then + error(number_name .. " is required when " .. value_name .. " is set") + end + return optional_number(number_name, nil, env) +end + +local function split_csv(value) + local out = {} + for part in tostring(value or ""):gmatch("[^,]+") do + table.insert(out, part) + end + return out +end + +local function normalize_env(env) + env = env or os.getenv + if type(env) == "function" then + local getenv = env + env = setmetatable({}, { + __index = function(_, key) + return getenv(key) + end, + }) + end + return env +end + +function config.load_state_dir(env) + env = normalize_env(env) + return required("WATCHDOG_STATE_DIR", env) +end + +function config.load_init(env) + env = normalize_env(env) + + -- The watchdog has one job: compare the sequencer's finalized state against + -- a canonical CM re-derivation. One cycle per process; infra schedules it. + return { + version = config.VERSION, + sequencer_url = required("WATCHDOG_SEQUENCER_URL", env), + input_box_address = required("WATCHDOG_INPUTBOX_ADDRESS", env), + app_address = required("WATCHDOG_APP_ADDRESS", env), + input_added_topic = env.WATCHDOG_INPUT_ADDED_TOPIC, + state_dir = required("WATCHDOG_STATE_DIR", env), + cm_snapshot_dir = required("WATCHDOG_CM_SNAPSHOT_DIR", env), + cm_snapshot_safe_block = optional_required_number( + "WATCHDOG_CM_SNAPSHOT_DIR", + "WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK", + env + ), + cm_image_hash = env.WATCHDOG_CM_IMAGE_HASH, + retry_attempts = optional_number("WATCHDOG_RETRY_ATTEMPTS", 3, env), + retry_delay_sec = optional_number("WATCHDOG_RETRY_DELAY_SEC", 5, env), + long_block_range_error_codes = split_csv( + env.WATCHDOG_LONG_BLOCK_RANGE_ERROR_CODES or "-32005,-32600,-32602,-32616" + ), + } +end + +local function required_field(data, name) + local value = data[name] + if value == nil or value == "" then + error("config.json missing " .. name) + end + return value +end + +local function optional_field_number(data, name, default) + local value = data[name] + if value == nil then + return default + end + if type(value) ~= "number" then + error("config.json " .. name .. " must be a number") + end + return value +end + +function config.persisted(cfg) + return { + version = config.VERSION, + sequencer_url = cfg.sequencer_url, + input_box_address = cfg.input_box_address, + app_address = cfg.app_address, + input_added_topic = cfg.input_added_topic, + cm_image_hash = cfg.cm_image_hash, + retry_attempts = cfg.retry_attempts, + retry_delay_sec = cfg.retry_delay_sec, + long_block_range_error_codes = cfg.long_block_range_error_codes, + } +end + +function config.from_persisted(state_dir, data, env) + env = normalize_env(env) + if data.version ~= config.VERSION then + error("unsupported config.json version: " .. tostring(data.version)) + end + return { + version = config.VERSION, + state_dir = state_dir, + sequencer_url = required_field(data, "sequencer_url"), + l1_rpc_url = required("WATCHDOG_L1_RPC_URL", env), + input_box_address = required_field(data, "input_box_address"), + app_address = required_field(data, "app_address"), + input_added_topic = data.input_added_topic, + cm_image_hash = data.cm_image_hash, + retry_attempts = optional_field_number(data, "retry_attempts", 3), + retry_delay_sec = optional_field_number(data, "retry_delay_sec", 5), + long_block_range_error_codes = data.long_block_range_error_codes or {}, + } +end + +config.load = config.load_init + +return config diff --git a/watchdog/http.lua b/watchdog/http.lua new file mode 100644 index 0000000..9eb53c6 --- /dev/null +++ b/watchdog/http.lua @@ -0,0 +1,100 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +--- HTTP client via in-tree lua-curl / lcurl (libcurl on the host). + +local http = {} + +local function parse_response_headers(header_lines) + local headers = {} + for _, line in ipairs(header_lines) do + local name, value = line:match("^([^:]+):%s*(.+)$") + if name and value then + headers[name:lower()] = value + end + end + return headers +end + +function http.new_curl() + local ok, curl = pcall(require, "cURL") + if not ok then + ok, curl = pcall(require, "lcurl") + end + if not ok then + error( + "lua-curl binding not found; run: just watchdog-lua-deps " + .. "(requires libcurl dev headers on the host)" + ) + end + + local client = {} + + local function perform_request(opts) + local chunks = {} + local header_lines = {} + local header_list = {} + for key, value in pairs(opts.headers or {}) do + table.insert(header_list, key .. ": " .. value) + end + + local easy = curl.easy({ + url = opts.url, + post = opts.post, + postfields = opts.postfields, + httpheader = header_list, + timeout = 30, + writefunction = function(chunk) + table.insert(chunks, chunk) + return #chunk + end, + headerfunction = function(line) + local trimmed = line:gsub("\r?\n$", "") + if trimmed ~= "" then + table.insert(header_lines, trimmed) + end + return #line + end, + }) + + local ok_perform, err = pcall(function() + easy:perform() + end) + if not ok_perform then + easy:close() + return nil, tostring(err) + end + + local status = easy:getinfo_response_code() + easy:close() + return { + status = status, + body = table.concat(chunks), + headers = parse_response_headers(header_lines), + } + end + + function client.post(_self, url, body, headers) + return perform_request({ + url = url, + post = true, + postfields = body, + headers = headers, + }) + end + + function client.get(_self, url, headers) + return perform_request({ + url = url, + headers = headers, + }) + end + + return client +end + +function http.new() + return http.new_curl() +end + +return http diff --git a/watchdog/json.lua b/watchdog/json.lua new file mode 100644 index 0000000..005f423 --- /dev/null +++ b/watchdog/json.lua @@ -0,0 +1,12 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +--- Watchdog JSON facade (pure Lua; see third_party/json.lua). + +local json = {} + +function json.new() + return require("watchdog.third_party.json") +end + +return json diff --git a/watchdog/jsonrpc.lua b/watchdog/jsonrpc.lua new file mode 100644 index 0000000..e2989de --- /dev/null +++ b/watchdog/jsonrpc.lua @@ -0,0 +1,107 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local jsonrpc = {} + +local function quantity(value) + assert(type(value) == "number" and value >= 0, "quantity must be a non-negative number") + return string.format("0x%x", value) +end + +local function strip_0x(value) + return tostring(value):gsub("^0[xX]", "") +end + +local function topic_address(address) + assert(type(address) == "string" and address ~= "", "topic address is required") + local raw = strip_0x(address):lower() + assert(#raw == 40 and raw:match("^[0-9a-f]+$") ~= nil, "topic address must be 20-byte hex") + return "0x" .. string.rep("0", 24) .. raw +end + +function jsonrpc.new(http, json, url) + assert(type(http) == "table" and type(http.post) == "function", "http.post is required") + assert(type(json) == "table" and type(json.encode) == "function", "json.encode is required") + assert(type(json.decode) == "function", "json.decode is required") + assert(type(url) == "string" and url ~= "", "url is required") + + local client = { + http = http, + json = json, + url = url, + next_id = 1, + } + + function client:call(method, params) + local id = self.next_id + self.next_id = self.next_id + 1 + local body = self.json.encode({ + jsonrpc = "2.0", + id = id, + method = method, + params = params or {}, + }) + + local response, http_err = self.http:post(self.url, body, { + ["content-type"] = "application/json", + }) + if not response then + return nil, http_err + end + if response.status < 200 or response.status >= 300 then + return nil, "HTTP " .. tostring(response.status) + end + + local decoded + local ok = pcall(function() + decoded = self.json.decode(response.body) + end) + if not ok then + return nil, "invalid JSON-RPC response JSON" + end + if type(decoded) ~= "table" then + return nil, "JSON-RPC response must be an object" + end + if decoded.id ~= id then + return nil, "JSON-RPC response id mismatch" + end + if decoded.error ~= nil then + local code = decoded.error.code or "unknown" + local message = decoded.error.message or "JSON-RPC error" + return nil, tostring(code) .. ": " .. tostring(message) + end + return decoded.result + end + + function client:get_logs(filter) + assert(type(filter.input_added_topic) == "string", "input_added_topic is required") + local topics = { filter.input_added_topic } + if filter.app_address then + topics[2] = topic_address(filter.app_address) + end + + return self:call("eth_getLogs", { + { + address = filter.address, + fromBlock = quantity(filter.from_block), + toBlock = quantity(filter.to_block), + topics = topics, + }, + }) + end + + function client:get_block_number_by_tag(tag) + local block, err = self:call("eth_getBlockByNumber", { tag, false }) + if not block then + return nil, err + end + if type(block.number) ~= "string" then + return nil, "block response missing number" + end + return tonumber(block.number:gsub("^0[xX]", ""), 16) + end + + return client +end + +return jsonrpc diff --git a/watchdog/justfile b/watchdog/justfile new file mode 100644 index 0000000..884ba3d --- /dev/null +++ b/watchdog/justfile @@ -0,0 +1,35 @@ +set shell := ["bash", "-euo", "pipefail", "-c"] +set working-directory := ".." + +default: + @just --list --justfile {{source_file()}} + +test: + lua watchdog/tests/run.lua + +test-e2e: + lua watchdog/tests/e2e.lua + +test-divergence-drill: lua-deps + @bash scripts/test-watchdog-divergence-drill.sh + +lua-deps: + @bash scripts/watchdog-lua-deps.sh + +docker-smoke: + @bash scripts/ci-watchdog-docker-smoke.sh + +doctor: + @command -v lua >/dev/null || { echo "doctor: lua not on PATH"; exit 1; } + @command -v cartesi-machine >/dev/null || { echo "doctor: cartesi-machine not on PATH"; exit 1; } + @test -d examples/canonical-app/out/canonical-machine-image || { \ + echo "doctor: missing devnet machine image; run: just canonical-build-machine-image"; \ + exit 1; \ + } + @test -f .deps/lua/lcurl.so || { \ + echo "doctor: missing .deps/lua/lcurl.so; run: just watchdog-lua-deps"; \ + exit 1; \ + } + @lua -e "package.cpath = '.deps/lua/?.so;' .. package.cpath; require('lcurl'); print('doctor: lcurl ok')" + @lua -e "package.path = './?.lua;./?/init.lua;' .. package.path; local m=require('watchdog.machine_cartesi').new(); local inst,err=m:load('examples/canonical-app/out/canonical-machine-image'); if not inst then error('doctor: machine_cartesi cannot load devnet image: '..tostring(err)) end; print('doctor: machine_cartesi load ok')" + @echo "doctor: watchdog toolchain ok" diff --git a/watchdog/l1_reader.lua b/watchdog/l1_reader.lua new file mode 100644 index 0000000..e796f93 --- /dev/null +++ b/watchdog/l1_reader.lua @@ -0,0 +1,195 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local abi = require("watchdog.abi") + +local l1_reader = {} + +l1_reader.INPUT_ADDED_TOPIC = "0xc05d337121a6e8605c6ec0b72aa29c4210ffe6e5b9cefdd6a7058188a8f66f98" + +l1_reader.DEFAULT_LONG_BLOCK_RANGE_ERROR_CODES = { + "-32005", + "-32600", + "-32602", + "-32616", +} + +local function contains_any(message, codes) + message = tostring(message or "") + for _, code in ipairs(codes or {}) do + if message:find(code, 1, true) then + return true + end + end + return false +end + +local function hex_quantity_to_number(value, field) + if type(value) == "number" then + return value + end + if type(value) ~= "string" or value:sub(1, 2) ~= "0x" then + error((field or "quantity") .. " must be an Ethereum hex quantity") + end + return tonumber(value:sub(3), 16) +end + +local function log_order_key(log) + return { + hex_quantity_to_number(log.blockNumber, "blockNumber"), + hex_quantity_to_number(log.transactionIndex or "0x0", "transactionIndex"), + hex_quantity_to_number(log.logIndex or "0x0", "logIndex"), + } +end + +function l1_reader.sort_logs(logs) + table.sort(logs, function(a, b) + local ak = log_order_key(a) + local bk = log_order_key(b) + if ak[1] ~= bk[1] then + return ak[1] < bk[1] + end + if ak[2] ~= bk[2] then + return ak[2] < bk[2] + end + return ak[3] < bk[3] + end) + return logs +end + +--- Return the RPC latest head when it is at least `target_block`; otherwise a transient retry error. +function l1_reader.ensure_rpc_head_at_least(rpc, target_block) + assert(type(rpc) == "table" and type(rpc.get_block_number_by_tag) == "function", "rpc.get_block_number_by_tag is required") + assert(type(target_block) == "number", "target_block is required") + + local head, err = rpc:get_block_number_by_tag("latest") + if not head then + return nil, err + end + if head < target_block then + return nil, string.format( + "L1 RPC latest head %d lags target block %d; retry", + head, + target_block + ) + end + return head +end + +local function scan_partitions(rpc, params, on_logs) + assert(type(rpc) == "table" and type(rpc.get_logs) == "function", "rpc.get_logs is required") + assert(type(params) == "table", "params are required") + assert(type(on_logs) == "function", "on_logs callback is required") + + local start_block = assert(params.start_block, "start_block is required") + local end_block = assert(params.end_block, "end_block is required") + local codes = params.long_block_range_error_codes or l1_reader.DEFAULT_LONG_BLOCK_RANGE_ERROR_CODES + local input_added_topic = params.input_added_topic or l1_reader.INPUT_ADDED_TOPIC + + local function go(from_block, to_block) + local logs, err = rpc:get_logs({ + address = params.input_box_address, + app_address = params.app_address, + from_block = from_block, + to_block = to_block, + input_added_topic = input_added_topic, + }) + if logs then + l1_reader.sort_logs(logs) + return on_logs(logs, { + from_block = from_block, + to_block = to_block, + }) + end + + if from_block < to_block and contains_any(err, codes) then + local mid = from_block + ((to_block - from_block) // 2) + local left_count, left_err = go(from_block, mid) + if not left_count then + return nil, left_err + end + local right_count, right_err = go(mid + 1, to_block) + if not right_count then + return nil, right_err + end + return left_count + right_count + end + + return nil, err + end + + if end_block < start_block then + return 0 + end + + return go(start_block, end_block) +end + +function l1_reader.for_each_log_chunk_partitioned(rpc, params, on_logs) + return scan_partitions(rpc, params, function(logs, range) + local ok, err = on_logs(logs, range) + if not ok then + return nil, err + end + return #logs + end) +end + +function l1_reader.fetch_logs_partitioned(rpc, params) + local all_logs = {} + local count, err = l1_reader.for_each_log_chunk_partitioned(rpc, params, function(logs) + for _, log in ipairs(logs) do + table.insert(all_logs, log) + end + return true + end) + if not count then + return nil, err + end + return all_logs +end + +function l1_reader.decode_and_validate_log(log) + local decoded = abi.decode_input_added_log(log) + local block_number = hex_quantity_to_number(log.blockNumber, "blockNumber") + if decoded.block_number ~= block_number then + error(string.format( + "InputAdded block number mismatch: log=%d payload=%d", + block_number, + decoded.block_number + )) + end + return decoded +end + +function l1_reader.for_each_input_chunk_partitioned(rpc, params, on_inputs) + assert(type(on_inputs) == "function", "on_inputs callback is required") + + return scan_partitions(rpc, params, function(logs, range) + local inputs = {} + for _, log in ipairs(logs) do + table.insert(inputs, l1_reader.decode_and_validate_log(log)) + end + local ok, err = on_inputs(inputs, range) + if not ok then + return nil, err + end + return #inputs + end) +end + +function l1_reader.fetch_inputs(rpc, params) + local inputs = {} + local count, err = l1_reader.for_each_input_chunk_partitioned(rpc, params, function(chunk) + for _, input in ipairs(chunk) do + table.insert(inputs, input) + end + return true + end) + if not count then + return nil, err + end + return inputs +end + +return l1_reader diff --git a/watchdog/machine_cartesi.lua b/watchdog/machine_cartesi.lua new file mode 100644 index 0000000..5de1382 --- /dev/null +++ b/watchdog/machine_cartesi.lua @@ -0,0 +1,203 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +--- In-process Cartesi Machine binding via the `cartesi` Lua module (ships with cartesi-machine). + +local machine_runner = require("watchdog.machine_runner") + +local machine_cartesi = {} + +local STATE_INSPECT_QUERY = "state" +local MAX_MCYCLE = (1 << 53) - 1 + +local function require_cartesi() + local ok, cartesi = pcall(require, "cartesi") + if not ok then + error( + "cartesi Lua module is required; ensure cartesi-machine is installed and LUA_PATH includes its scripts" + ) + end + return cartesi +end + +local function run_loop(machine, cartesi, handlers) + while true do + machine:run(MAX_MCYCLE) + if machine:read_reg("iflags_H") ~= 0 then + local exit_code = machine:read_reg("htif_tohost_data") >> 1 + return nil, "machine halted with exit code " .. tostring(exit_code) + end + if machine:read_reg("iflags_Y") ~= 0 then + local cmd, reason, data = machine:receive_cmio_request() + local err = handlers.on_manual(cmd, reason, data) + if err then + return nil, err + end + if machine:read_reg("iflags_Y") ~= 0 then + return true + end + elseif machine:read_reg("iflags_X") ~= 0 then + local cmd, reason, data = machine:receive_cmio_request() + handlers.on_automatic(cmd, reason, data) + end + end +end + +local function advance_inputs_on_machine(machine, cartesi, inputs) + local queue = {} + for _, input in ipairs(inputs) do + table.insert(queue, input) + end + local next_index = 0 + local outputs_done = true + + local handlers = { + on_manual = function(cmd, reason, data) + if reason == cartesi.CMIO_YIELD_MANUAL_REASON_TX_EXCEPTION then + return "CMIO exception: " .. tostring(data) + end + if next_index < #queue then + if reason ~= cartesi.CMIO_YIELD_MANUAL_REASON_RX_ACCEPTED + and reason ~= cartesi.CMIO_YIELD_MANUAL_REASON_RX_REJECTED + then + return "unexpected manual yield before feeding input" + end + next_index = next_index + 1 + local raw = queue[next_index].raw_input + if type(raw) ~= "string" then + return "input.raw_input is required" + end + machine:send_cmio_response(cartesi.CMIO_YIELD_REASON_ADVANCE_STATE, raw) + outputs_done = false + return nil + end + if not outputs_done then + if reason == cartesi.CMIO_YIELD_MANUAL_REASON_RX_ACCEPTED + or reason == cartesi.CMIO_YIELD_MANUAL_REASON_RX_REJECTED + then + outputs_done = true + end + return nil + end + return nil + end, + on_automatic = function(_cmd, reason, data) + if not outputs_done and reason == cartesi.CMIO_YIELD_AUTOMATIC_REASON_TX_OUTPUT then + return + end + if not outputs_done and reason == cartesi.CMIO_YIELD_AUTOMATIC_REASON_TX_REPORT then + return + end + end, + } + + local ok, err = run_loop(machine, cartesi, handlers) + if not ok then + return nil, err + end + if next_index < #queue then + return nil, "machine stopped before all inputs were fed" + end + return true +end + +local function inspect_on_machine(machine, cartesi) + local reports = {} + local query_sent = false + local query_done = false + + local handlers = { + on_manual = function(_cmd, reason, _data) + if not query_sent then + machine:send_cmio_response(cartesi.CMIO_YIELD_REASON_INSPECT_STATE, STATE_INSPECT_QUERY) + query_sent = true + return nil + end + if reason == cartesi.CMIO_YIELD_MANUAL_REASON_RX_ACCEPTED + or reason == cartesi.CMIO_YIELD_MANUAL_REASON_RX_REJECTED + then + query_done = true + end + return nil + end, + on_automatic = function(_cmd, reason, data) + if query_sent and not query_done and reason == cartesi.CMIO_YIELD_AUTOMATIC_REASON_TX_REPORT then + table.insert(reports, data) + end + end, + } + + local ok, err = run_loop(machine, cartesi, handlers) + if not ok then + return nil, err + end + if #reports == 0 then + return nil, "inspect produced no report" + end + if #reports > 1 then + return nil, "inspect produced multiple reports; multi-chunk state not supported yet" + end + return reports[1] +end + +function machine_cartesi.new(_opts) + local cartesi = require_cartesi() + local runtime_config = { + skip_root_hash_check = true, + skip_root_hash_store = true, + } + + local binding = {} + + local function load_machine(snapshot_dir, config) + local machine = cartesi.new() + machine:load(snapshot_dir, config) + return machine + end + + function binding.load_snapshot(snapshot_dir) + assert(type(snapshot_dir) == "string" and snapshot_dir ~= "", "snapshot_dir is required") + local ok, machine = pcall(load_machine, snapshot_dir, runtime_config) + if not ok then + return nil, tostring(machine) + end + return { + cartesi = cartesi, + machine = machine, + reference_block = 0, + } + end + + function binding.advance_inputs(instance, inputs, range) + assert(type(instance) == "table" and instance.machine, "machine instance is required") + if range.from_block > range.to_block then + return true + end + return advance_inputs_on_machine(instance.machine, instance.cartesi, inputs) + end + + function binding.inspect_state(instance) + assert(type(instance) == "table" and instance.machine, "machine instance is required") + return inspect_on_machine(instance.machine, instance.cartesi) + end + + function binding.store_snapshot(instance, snapshot_dir) + assert(type(instance) == "table" and instance.machine, "machine instance is required") + assert(type(snapshot_dir) == "string" and snapshot_dir ~= "", "snapshot_dir is required") + local ok, err = pcall(function() + instance.machine:store(snapshot_dir) + end) + if not ok then + return nil, tostring(err) + end + return true + end + + return machine_runner.new(binding) +end + +machine_cartesi._private = { + STATE_INSPECT_QUERY = STATE_INSPECT_QUERY, +} + +return machine_cartesi diff --git a/watchdog/machine_runner.lua b/watchdog/machine_runner.lua new file mode 100644 index 0000000..480acba --- /dev/null +++ b/watchdog/machine_runner.lua @@ -0,0 +1,87 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +--- Cartesi Machine driver: load, incremental advance, inspect, dump. + +local machine_runner = {} + +local function assert_contiguous_advance(instance, range) + assert(type(range) == "table", "range is required") + assert(type(range.from_block) == "number", "range.from_block is required") + assert(type(range.to_block) == "number", "range.to_block is required") + if range.from_block > range.to_block then + error("advance range is empty or inverted") + end + local expected_from = (instance.reference_block or 0) + 1 + if range.from_block ~= expected_from then + error(string.format( + "non-contiguous advance: expected from_block %d, got %d", + expected_from, + range.from_block + )) + end +end + +function machine_runner.new(binding) + assert(type(binding) == "table", "Cartesi Machine binding is required") + assert(type(binding.load_snapshot) == "function", "binding.load_snapshot is required") + assert(type(binding.advance_inputs) == "function", "binding.advance_inputs is required") + assert(type(binding.inspect_state) == "function", "binding.inspect_state is required") + assert(type(binding.store_snapshot) == "function", "binding.store_snapshot is required") + + local driver = { binding = binding } + + function driver:load(path, reference_block) + local instance, err = self.binding.load_snapshot(path) + if not instance then + return nil, err + end + instance.reference_block = reference_block or instance.reference_block or 0 + return instance + end + + function driver:advance(instance, inputs, range) + assert_contiguous_advance(instance, range) + local ok, err = self.binding.advance_inputs(instance, inputs or {}, range) + if not ok then + return nil, err + end + instance.reference_block = range.to_block + return true + end + + function driver:inspect(instance) + return self.binding.inspect_state(instance) + end + + function driver:dump(instance, path, reference_block) + assert(type(reference_block) == "number", "reference_block is required for dump") + if instance.reference_block ~= reference_block then + error(string.format( + "dump reference_block %d does not match instance reference_block %d", + reference_block, + instance.reference_block or -1 + )) + end + return self.binding.store_snapshot(instance, path) + end + + -- Legacy helpers kept for compatibility with older tests. + function driver:feed_inputs(instance, inputs) + local from = (instance.reference_block or 0) + 1 + return self:advance(instance, inputs, { from_block = from, to_block = from }) + end + + function driver:inspect_state(instance) + return self:inspect(instance) + end + + function driver:save(instance, path) + assert(instance.reference_block ~= nil, "cannot dump before advance establishes reference_block") + return self:dump(instance, path, instance.reference_block) + end + + return driver +end + +return machine_runner diff --git a/watchdog/main.lua b/watchdog/main.lua new file mode 100644 index 0000000..a7ecd97 --- /dev/null +++ b/watchdog/main.lua @@ -0,0 +1,249 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local config = require("watchdog.config") +local checkpoint = require("watchdog.checkpoint") +local http_mod = require("watchdog.http") +local json_mod = require("watchdog.json") +local jsonrpc = require("watchdog.jsonrpc") +local machine_cartesi = require("watchdog.machine_cartesi") +local retry = require("watchdog.retry") +local runner = require("watchdog.runner") +local sequencer_reader = require("watchdog.sequencer_reader") +local state = require("watchdog.state") + +local EXIT_OK = 0 +local EXIT_TRANSIENT = 1 +local EXIT_DIVERGENCE = 2 + +local function prepend_deps_cpath() + local deps = os.getenv("WATCHDOG_LUA_DEPS") + if deps and deps ~= "" then + package.cpath = deps .. "/?.so;" .. package.cpath + end +end + +local function load_machine(_cfg) + return machine_cartesi.new() +end + +local function log_step(message) + io.stderr:write("watchdog_step " .. tostring(message) .. "\n") +end + +local function is_table_with_kind(value) + return type(value) == "table" and type(value.kind) == "string" +end + +local function is_terminal_error(value) + if not is_table_with_kind(value) then + return false + end + return value.kind == "state_mismatch" + or value.kind == "inclusion_block_regressed" +end + +local function emit_watchdog_event(json, payload, deps) + if deps and type(deps.on_watchdog_event) == "function" then + deps.on_watchdog_event(payload) + end + io.stderr:write("watchdog_event " .. json.encode(payload) .. "\n") +end + +local function default_machine_deps(cfg) + return { + machine = load_machine(cfg), + log_step = log_step, + } +end + +local function default_deps(cfg) + local http = http_mod.new() + local json = json_mod.new() + local deps = default_machine_deps(cfg) + deps.http = http + deps.rpc = jsonrpc.new(http, json, cfg.l1_rpc_url) + deps.sequencer = sequencer_reader.new(http, json, cfg.sequencer_url) + return deps, json +end + +local function run_init(cfg, deps) + deps = deps or default_machine_deps(cfg) + local json = json_mod.new() + + local existing, load_err = checkpoint.load(cfg.state_dir) + if existing then + return nil, "watchdog state already initialized" + end + if load_err ~= "missing " .. checkpoint.HEAD_FILE then + return nil, "failed to load watchdog head: " .. tostring(load_err) + end + + local ok, err = state.write_json_atomic(cfg.state_dir, "config.json", config.persisted(cfg), json) + if not ok then + return nil, err + end + + local machine = deps.machine or load_machine(cfg) + if type(deps.log_step) == "function" then + deps.log_step("load bootstrap CM snapshot") + end + local instance, load_snapshot_err = machine:load(cfg.cm_snapshot_dir, cfg.cm_snapshot_safe_block) + if not instance then + return nil, load_snapshot_err + end + + if type(deps.log_step) == "function" then + deps.log_step("persist initial watchdog checkpoint") + end + local written, write_err = checkpoint.write(cfg.state_dir, cfg.cm_snapshot_safe_block, function(snapshot_dir) + return machine:dump(instance, snapshot_dir, cfg.cm_snapshot_safe_block) + end, { + created_at = os.date("!%Y-%m-%dT%H:%M:%SZ"), + cm_image_hash = cfg.cm_image_hash, + }) + if not written then + return nil, write_err + end + + return { + ok = true, + safe_block = cfg.cm_snapshot_safe_block, + } +end + +local function load_tick_config(env) + local json = json_mod.new() + local state_dir = config.load_state_dir(env) + local persisted, err = state.read_json(state_dir, "config.json", json) + if not persisted then + error("failed to load config.json: " .. tostring(err)) + end + return config.from_persisted(state_dir, persisted, env) +end + +local function run_compare_cycle(cfg, deps) + deps = deps or select(1, default_deps(cfg)) + local json = json_mod.new() + local result, err = retry.with_retries(function() + local ok, value, run_err = pcall(runner.run_once, cfg, deps) + if not ok then + return nil, value + end + return value, run_err + end, { + attempts = cfg.retry_attempts, + delay_sec = cfg.retry_delay_sec, + should_retry = function(retry_err) + return not is_terminal_error(retry_err) + end, + }) + if result == nil then + if is_terminal_error(err) then + emit_watchdog_event(json, err, deps) + return EXIT_DIVERGENCE, err + end + return EXIT_TRANSIENT, err + end + return EXIT_OK, result +end + +local function run_tick(cfg, deps) + return run_compare_cycle(cfg, deps) +end + +local function usage() + return "usage: watchdog " +end + +local function load_or_error(loader) + local ok, value = pcall(loader) + if not ok then + return nil, value + end + return value +end + +local function exit_for_result(command, exit_code, err) + if exit_code == EXIT_DIVERGENCE then + os.exit(EXIT_DIVERGENCE) + end + if exit_code == EXIT_TRANSIENT then + io.stderr:write("watchdog " .. command .. " failed: " .. tostring(err) .. "\n") + os.exit(EXIT_TRANSIENT) + end + os.exit(EXIT_OK) +end + +-- One compare cycle per `tick` process. Infra (systemd timer / k8s CronJob) +-- schedules re-runs and reacts to the exit code; the watchdog itself does not loop. +local function main(argv) + argv = argv or arg or {} + prepend_deps_cpath() + + local command = argv[1] + if command == "init" then + local cfg, cfg_err = load_or_error(function() + return config.load_init() + end) + if not cfg then + io.stderr:write("watchdog init failed: " .. tostring(cfg_err) .. "\n") + os.exit(EXIT_TRANSIENT) + end + local ok, result, err = pcall(run_init, cfg) + if not ok then + io.stderr:write("watchdog init failed: " .. tostring(result) .. "\n") + os.exit(EXIT_TRANSIENT) + end + if not result then + io.stderr:write("watchdog init failed: " .. tostring(err) .. "\n") + os.exit(EXIT_TRANSIENT) + end + os.exit(EXIT_OK) + end + + if command == "tick" then + local cfg, cfg_err = load_or_error(function() + return load_tick_config() + end) + if not cfg then + io.stderr:write("watchdog tick failed: " .. tostring(cfg_err) .. "\n") + os.exit(EXIT_TRANSIENT) + end + local ok, exit_code, err = pcall(run_tick, cfg) + if not ok then + io.stderr:write("watchdog tick failed: " .. tostring(exit_code) .. "\n") + os.exit(EXIT_TRANSIENT) + end + if not exit_code then + exit_code = EXIT_TRANSIENT + end + exit_for_result(command, exit_code, err) + end + + io.stderr:write(usage() .. "\n") + os.exit(EXIT_TRANSIENT) +end + +local function invoked_as_script() + return type(arg) == "table" + and type(arg[0]) == "string" + and arg[0]:match("watchdog[/\\]main%.lua$") ~= nil +end + +if invoked_as_script() then + main(arg) +end + +return { + main = main, + run_init = run_init, + run_tick = run_tick, + run_compare_cycle = run_compare_cycle, + load_tick_config = load_tick_config, + EXIT_OK = EXIT_OK, + EXIT_TRANSIENT = EXIT_TRANSIENT, + EXIT_DIVERGENCE = EXIT_DIVERGENCE, +} diff --git a/watchdog/retry.lua b/watchdog/retry.lua new file mode 100644 index 0000000..c4c09a0 --- /dev/null +++ b/watchdog/retry.lua @@ -0,0 +1,43 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local retry = {} + +local function default_sleep(seconds) + seconds = tonumber(seconds) + if not seconds or seconds <= 0 then + return + end + local ok, how, code = os.execute("exec sleep " .. tostring(seconds)) + if not ok and how == "signal" and code == 2 then + os.exit(130, true) + end +end + +function retry.with_retries(fn, opts) + opts = opts or {} + local attempts = math.max(1, opts.attempts or 1) + local delay_sec = opts.delay_sec or 0 + local should_retry = opts.should_retry or function(_err) + return true + end + local sleep = opts.sleep or default_sleep + + local last_err + for attempt = 1, attempts do + local result, err = fn(attempt) + if result then + return result + end + last_err = err + if not should_retry(err) then + break + end + if attempt < attempts then + sleep(delay_sec) + end + end + return nil, last_err +end + +return retry diff --git a/watchdog/runner.lua b/watchdog/runner.lua new file mode 100644 index 0000000..393c67a --- /dev/null +++ b/watchdog/runner.lua @@ -0,0 +1,241 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local checkpoint = require("watchdog.checkpoint") +local compare = require("watchdog.compare") +local l1_reader = require("watchdog.l1_reader") + +local runner = {} + +local function require_dep(deps, name) + local value = deps[name] + assert(value ~= nil, "missing dependency: " .. name) + return value +end + +local function load_checkpoint(cfg, checkpoint_mod) + local loaded, load_err = checkpoint_mod.load(cfg.state_dir) + if loaded then + return loaded + end + error("failed to load watchdog head: " .. tostring(load_err)) +end + +local function ensure_rpc_head_covers_target(deps, target_block) + if deps.fetch_inputs or deps.for_each_input_chunk then + return true + end + + local rpc = deps.rpc + if not rpc or type(rpc.get_block_number_by_tag) ~= "function" then + return true + end + + local head, err = l1_reader.ensure_rpc_head_at_least(rpc, target_block) + if not head then + return nil, err + end + return true +end + +local function l1_params(cfg, from_block, to_block) + return { + start_block = from_block, + end_block = to_block, + input_box_address = cfg.input_box_address, + app_address = cfg.app_address, + input_added_topic = cfg.input_added_topic, + long_block_range_error_codes = cfg.long_block_range_error_codes, + } +end + +local function for_each_input_chunk(cfg, deps, from_block, to_block, on_chunk) + if from_block > to_block then + return 0 + end + + if deps.for_each_input_chunk then + return deps.for_each_input_chunk(from_block, to_block, on_chunk) + end + + if deps.fetch_inputs then + local inputs, err = deps.fetch_inputs(from_block, to_block) + if not inputs then + return nil, err + end + local ok, callback_err = on_chunk(inputs, { + from_block = from_block, + to_block = to_block, + }) + if not ok then + return nil, callback_err + end + return #inputs + end + + local rpc = require_dep(deps, "rpc") + return l1_reader.for_each_input_chunk_partitioned(rpc, l1_params(cfg, from_block, to_block), on_chunk) +end + +local function step(deps, message) + if deps and type(deps.log_step) == "function" then + deps.log_step(message) + end +end + +local function compare_and_checkpoint(cfg, deps, instance, safe_block_prev, safe_block_next, input_count) + local checkpoint_mod = deps.checkpoint or checkpoint + local machine = require_dep(deps, "machine") + + step(deps, "run CM inspect (state query)") + local cm_state, inspect_err = machine:inspect(instance) + if not cm_state then + return nil, inspect_err + end + + local sequencer = require_dep(deps, "sequencer") + step(deps, "fetch sequencer GET /finalized_state") + local finalized, state_err = sequencer:get_finalized_state() + if not finalized then + return nil, state_err + end + if finalized.not_modified then + return nil, "finalized state unexpectedly returned 304 during compare" + end + if finalized.inclusion_block ~= safe_block_next then + return nil, string.format( + "finalized inclusion_block moved during compare cycle (%s -> %s); retry", + tostring(safe_block_next), + tostring(finalized.inclusion_block) + ) + end + + step(deps, "compare finalized SSZ bytes against CM inspect report") + local equal, mismatch_offset = compare.raw_equal(finalized.state, cm_state) + if not equal then + return nil, { + kind = "state_mismatch", + previous_safe_block = safe_block_prev, + sequencer_inclusion_block = finalized.inclusion_block, + mismatch_offset = mismatch_offset, + } + end + + -- Compare succeeded: persist a fresh block-named checkpoint before flipping + -- head.json. The previous checkpoint is pruned after the pointer flip. + step(deps, "persist new manifest-backed checkpoint") + local written, write_err = checkpoint_mod.write(cfg.state_dir, safe_block_next, function(snapshot_dir) + return machine:dump(instance, snapshot_dir, safe_block_next) + end, { + created_at = os.date("!%Y-%m-%dT%H:%M:%SZ"), + cm_image_hash = cfg.cm_image_hash, + }) + if not written then + return nil, write_err + end + + return { + ok = true, + previous_safe_block = safe_block_prev, + safe_block = safe_block_next, + input_count = input_count, + } +end + +--- L1 fetch → CM advance → inspect → compare → checkpoint. +local function run_pass(cfg, deps, loaded, safe_block_prev, safe_block_next) + step(deps, string.format("check L1 RPC head covers target block %d", safe_block_next)) + local head_ok, head_err = ensure_rpc_head_covers_target(deps, safe_block_next) + if not head_ok then + return nil, head_err + end + + local machine = require_dep(deps, "machine") + step(deps, "load CM snapshot directory") + local instance, load_err = machine:load(loaded.snapshot_dir, safe_block_prev) + if not instance then + return nil, load_err + end + + step(deps, string.format( + "stream L1 InputAdded logs for blocks %s..%s", + tostring(safe_block_prev + 1), + tostring(safe_block_next) + )) + local input_count = 0 + local advanced_count, input_err = for_each_input_chunk( + cfg, + deps, + safe_block_prev + 1, + safe_block_next, + function(inputs, range) + input_count = input_count + #inputs + step(deps, string.format( + "feed %d decoded inputs into CM for blocks %d..%d", + #inputs, + range.from_block, + range.to_block + )) + return machine:advance(instance, inputs, range) + end + ) + if not advanced_count then + return nil, input_err + end + + return compare_and_checkpoint(cfg, deps, instance, safe_block_prev, safe_block_next, input_count) +end + +local function skip_result(safe_block_prev, safe_block_next, reason) + return { + ok = true, + skipped = true, + skip_reason = reason, + previous_safe_block = safe_block_prev, + safe_block = safe_block_next, + input_count = 0, + } +end + +function runner.run_once(cfg, deps) + deps = deps or {} + local checkpoint_mod = deps.checkpoint or checkpoint + + step(deps, "load watchdog checkpoint") + local loaded = load_checkpoint(cfg, checkpoint_mod) + + local safe_block_prev = loaded.safe_block or 0 + local sequencer = require_dep(deps, "sequencer") + step(deps, "fetch sequencer GET /finalized_state/inclusion_block") + local head, head_err = sequencer:get_finalized_inclusion_block() + if not head then + return nil, head_err + end + + local safe_block_next = head.inclusion_block + step(deps, string.format( + "check inclusion_block monotonicity (prev=%s next=%s)", + tostring(safe_block_prev), + tostring(safe_block_next) + )) + if safe_block_next < safe_block_prev then + return nil, { + kind = "inclusion_block_regressed", + previous_safe_block = safe_block_prev, + sequencer_inclusion_block = safe_block_next, + } + end + + if safe_block_next == safe_block_prev then + step(deps, "finalized inclusion_block unchanged; skip L1/CM/compare cycle") + return skip_result(safe_block_prev, safe_block_next, "finalized_unchanged") + end + + local result, err = run_pass(cfg, deps, loaded, safe_block_prev, safe_block_next) + if result then + step(deps, "compare pass complete") + end + return result, err +end + +return runner diff --git a/watchdog/sequencer-watchdog b/watchdog/sequencer-watchdog new file mode 100755 index 0000000..285729c --- /dev/null +++ b/watchdog/sequencer-watchdog @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +# Production CLI wrapper for watchdog subcommands. +set -euo pipefail + +if [[ "${WATCHDOG_PRINT_RELEASE_INFO:-0}" == "1" ]]; then + cat /opt/watchdog/RELEASE.json || true + cartesi-machine --version 2>&1 | sed 's/^/cartesi-machine: /' || true + exit 0 +fi + +lua_root="${WATCHDOG_LUA_ROOT:-/opt/watchdog/lua}" +lua_bin="${WATCHDOG_LUA_BIN:-lua5.4}" + +cd "${lua_root}" + +if [[ "$#" -gt 0 && ( "$1" == "init" || "$1" == "tick" ) ]]; then + : "${WATCHDOG_STATE_DIR:?WATCHDOG_STATE_DIR is required}" + mkdir -p "$WATCHDOG_STATE_DIR" + exec 9>"$WATCHDOG_STATE_DIR/run.lock" + if ! flock -n 9; then + echo "watchdog state is already locked: $WATCHDOG_STATE_DIR/run.lock" >&2 + exit 1 + fi +fi + +exec "$lua_bin" watchdog/main.lua "$@" diff --git a/watchdog/sequencer_reader.lua b/watchdog/sequencer_reader.lua new file mode 100644 index 0000000..b03f96c --- /dev/null +++ b/watchdog/sequencer_reader.lua @@ -0,0 +1,100 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local compare = require("watchdog.compare") + +local sequencer_reader = {} + +local function parse_header_number(headers, name) + if type(headers) ~= "table" then + return nil, name .. " header missing" + end + local value = headers[name:lower()] + if value == nil then + return nil, name .. " header missing" + end + local number = tonumber(value) + if number == nil then + return nil, name .. " header must be numeric" + end + return number +end + +function sequencer_reader.new(http, json, base_url) + assert(type(http) == "table" and type(http.get) == "function", "http.get is required") + assert(type(json) == "table" and type(json.decode) == "function", "json.decode is required") + assert(type(base_url) == "string" and base_url ~= "", "base_url is required") + + local client = { + http = http, + json = json, + base_url = base_url:gsub("/+$", ""), + } + + function client:get_finalized_inclusion_block() + local response, err = self.http:get(self.base_url .. "/finalized_state/inclusion_block") + if not response then + return nil, err + end + if response.status < 200 or response.status >= 300 then + return nil, "HTTP " .. tostring(response.status) + end + + local decoded + local ok_decode = pcall(function() + decoded = self.json.decode(response.body) + end) + if not ok_decode or type(decoded) ~= "table" then + return nil, "invalid finalized inclusion_block response JSON" + end + if type(decoded.inclusion_block) ~= "number" then + return nil, "inclusion_block must be a number" + end + if type(decoded.l2_tx_index) ~= "number" then + return nil, "l2_tx_index must be a number" + end + return decoded + end + + function client:get_finalized_state(request_headers) + local response, err = self.http:get( + self.base_url .. "/finalized_state", + request_headers or {} + ) + if not response then + return nil, err + end + if response.status == 304 then + return { not_modified = true } + end + if response.status < 200 or response.status >= 300 then + return nil, "HTTP " .. tostring(response.status) + end + + local inclusion_block, inclusion_err = + parse_header_number(response.headers, "X-Inclusion-Block") + if not inclusion_block then + return nil, inclusion_err + end + local l2_tx_index, index_err = parse_header_number(response.headers, "X-L2-Tx-Index") + if not l2_tx_index then + return nil, index_err + end + + local ok, validation_err = compare.assert_state_response(response.body) + if not ok then + return nil, validation_err + end + + return { + inclusion_block = inclusion_block, + l2_tx_index = l2_tx_index, + state = response.body, + etag = response.headers and response.headers["etag"], + } + end + + return client +end + +return sequencer_reader diff --git a/watchdog/state.lua b/watchdog/state.lua new file mode 100644 index 0000000..ae17c48 --- /dev/null +++ b/watchdog/state.lua @@ -0,0 +1,98 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local state = {} + +local function join(...) + local parts = { ... } + return table.concat(parts, "/"):gsub("//+", "/") +end + +local function shell_quote(value) + value = tostring(value) + return "'" .. value:gsub("'", "'\\''") .. "'" +end + +local function read_all(path) + local file, err = io.open(path, "rb") + if not file then + return nil, err + end + local data = file:read("*a") + file:close() + return data +end + +local function write_all(path, data) + local file, err = io.open(path, "wb") + if not file then + return nil, err + end + local ok, write_err = file:write(data) + if not ok then + file:close() + return nil, write_err + end + ok, err = file:close() + if not ok then + return nil, err + end + return true +end + +local function mkdir_p(path) + local ok = os.execute("mkdir -p " .. shell_quote(path)) + if ok ~= true and ok ~= 0 then + return nil, "mkdir failed: " .. path + end + return true +end + +function state.ensure_dir(dir) + assert(type(dir) == "string" and dir ~= "", "state dir is required") + return mkdir_p(dir) +end + +function state.write_json_atomic(dir, name, value, json) + assert(type(dir) == "string" and dir ~= "", "state dir is required") + assert(type(name) == "string" and name ~= "", "file name is required") + assert(type(json) == "table" and type(json.encode) == "function", "json encoder is required") + + local ok, err = state.ensure_dir(dir) + if not ok then + return nil, err + end + + local path = join(dir, name) + local tmp = path .. ".tmp" + ok, err = write_all(tmp, json.encode(value) .. "\n") + if not ok then + return nil, err + end + ok, err = os.rename(tmp, path) + if not ok then + return nil, err + end + return true +end + +function state.read_json(dir, name, json) + assert(type(dir) == "string" and dir ~= "", "state dir is required") + assert(type(name) == "string" and name ~= "", "file name is required") + assert(type(json) == "table" and type(json.decode) == "function", "json decoder is required") + + local data, err = read_all(join(dir, name)) + if not data then + return nil, err + end + local decoded + local ok = pcall(function() + decoded = json.decode(data) + end) + if not ok or type(decoded) ~= "table" then + return nil, "invalid " .. name + end + return decoded +end + +return state diff --git a/watchdog/tests/drill_divergence.lua b/watchdog/tests/drill_divergence.lua new file mode 100644 index 0000000..b47bc63 --- /dev/null +++ b/watchdog/tests/drill_divergence.lua @@ -0,0 +1,108 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) +-- +-- Staging drill: drive watchdog/main.lua divergence handling with injected +-- fake deps (same pattern as watchdog/tests/run.lua). +-- +-- Usage: +-- lua watchdog/tests/drill_divergence.lua + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local deps_lua = os.getenv("WATCHDOG_LUA_DEPS") +if deps_lua and deps_lua ~= "" then + package.cpath = deps_lua .. "/?.so;" .. package.cpath +end + +local main_mod = require("watchdog.main") +local log = dofile("watchdog/tests/e2e_log.lua") + +local function fake_cfg() + return { + state_dir = "/tmp/watchdog-drill", + cm_snapshot_dir = "/tmp/genesis-snapshot", + cm_snapshot_safe_block = 0, + input_box_address = "0xinputbox", + app_address = "0x1111111111111111111111111111111111111111", + input_added_topic = "0xtopic", + long_block_range_error_codes = require("watchdog.l1_reader").DEFAULT_LONG_BLOCK_RANGE_ERROR_CODES, + retry_attempts = 1, + retry_delay_sec = 0, + } +end + +local function fake_machine(inspect_state) + return { + load = function(_self, path, reference_block) + return { path = path, reference_block = reference_block or 0 } + end, + advance = function(_self, instance, _inputs, range) + instance.reference_block = range.to_block + return true + end, + inspect = function() + return inspect_state + end, + dump = function(_self, instance, snapshot_dir, reference_block) + instance.reference_block = reference_block + return true + end, + } +end + +local captured_event = nil +local deps = { + checkpoint = { + load = function(_dir) + return { snapshot_dir = "/tmp/snapshot", safe_block = 0 } + end, + write = function() + return true + end, + }, + sequencer = { + get_finalized_inclusion_block = function() + return { inclusion_block = 1, l2_tx_index = 0 } + end, + get_finalized_state = function() + return { + inclusion_block = 1, + l2_tx_index = 0, + state = string.char(0x01, 0x02, 0x03, 0x04), + } + end, + }, + fetch_inputs = function(from_block, to_block) + if from_block ~= 1 or to_block ~= 1 then + error(string.format("unexpected input range %s..%s", from_block, to_block)) + end + return {} + end, + machine = fake_machine(string.char(0xAA, 0xBB, 0xCC, 0xDD)), + on_watchdog_event = function(payload) + captured_event = payload + end, +} + +log.banner("divergence-signal-drill") +log.step("divergence-signal-drill", 1, 1, "run main.lua compare cycle with injected mismatch deps") + +local exit_code, err = main_mod.run_compare_cycle(fake_cfg(), deps) +if exit_code ~= main_mod.EXIT_DIVERGENCE then + log.fail("divergence-signal-drill", string.format("expected exit %d, got %s (%s)", main_mod.EXIT_DIVERGENCE, tostring(exit_code), tostring(err))) + os.exit(1) +end +if type(captured_event) ~= "table" or captured_event.kind ~= "state_mismatch" then + log.fail("divergence-signal-drill", "expected watchdog_event state_mismatch payload") + os.exit(1) +end +if type(captured_event.mismatch_offset) ~= "number" or captured_event.mismatch_offset < 0 then + log.fail("divergence-signal-drill", "expected non-negative mismatch_offset in event") + os.exit(1) +end + +log.pass( + "divergence-signal-drill", + string.format("main.lua emitted state_mismatch; mismatch_offset=%s", tostring(captured_event.mismatch_offset)) +) +os.exit(main_mod.EXIT_DIVERGENCE) diff --git a/watchdog/tests/e2e.lua b/watchdog/tests/e2e.lua new file mode 100644 index 0000000..8322de7 --- /dev/null +++ b/watchdog/tests/e2e.lua @@ -0,0 +1,301 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) +-- +-- Real watchdog end-to-end checks against cartesi-machine (and optionally a +-- live sequencer). Run from repo root: +-- lua watchdog/tests/e2e.lua +-- or: +-- just test-watchdog-e2e + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local checkpoint = require("watchdog.checkpoint") +local machine_cartesi = require("watchdog.machine_cartesi") +local runner = require("watchdog.runner") +local log = dofile("watchdog/tests/e2e_log.lua") + +local MACHINE_IMAGE = "examples/canonical-app/out/canonical-machine-image" +local GENESIS_SAFE_BLOCK = 0 + +local scenarios = {} +local failures = 0 +local skips = 0 +local machine_cartesi_probe = nil + +local function assert_true(value, message) + if not value then + error(message or "assertion failed", 2) + end +end + +local function command_exists(name) + local ok = os.execute("command -v " .. name .. " >/dev/null 2>&1") + return ok == true or ok == 0 +end + +local function path_is_dir(path) + local ok, err, code = os.rename(path, path) + if ok then + return true + end + if code == 13 then + return true + end + return false, err +end + +local function temp_dir(prefix) + -- Keep os.tmpname()'s full path (under the system temp dir) so scratch dirs + -- land in TMPDIR, not the repo root. Stripping the dir left them in cwd. + local base = os.tmpname() + os.remove(base) + local dir = string.format("%s-%s", base, prefix) + local ok = os.execute('mkdir -p "' .. dir .. '"') + if ok ~= true and ok ~= 0 then + error("mkdir failed for " .. dir) + end + return dir +end + +local function make_step_logger(scenario, total) + local index = 0 + return function(message) + index = index + 1 + log.step(scenario, index, total, message) + end +end + +local function run_scenario(name, fn) + log.banner(name) + local ok, result = pcall(fn) + if not ok then + failures = failures + 1 + log.fail(name, result) + return + end + if result == "skip" then + skips = skips + 1 + return + end + log.pass(name) +end + +local function skip(scenario, reason) + log.skip(scenario, reason) + return "skip" +end + +-- Toolchain prerequisites are hard requirements, not skips: in CI a missing +-- dep must fail the run, never pass vacuously. (The genuinely-optional +-- live-sequencer scenario still skips on its own, below.) +local function require_cartesi_machine() + if not command_exists("cartesi-machine") then + error("cartesi-machine not on PATH (install via nix develop / Cartesi tools)", 0) + end +end + +local function require_machine_image() + if not path_is_dir(MACHINE_IMAGE) then + error( + "canonical machine image missing at " .. MACHINE_IMAGE .. " (run: just canonical-build-machine-image)", + 0 + ) + end +end + +-- A missing in-process cartesi binding is the exact failure that must not pass +-- silently in CI (the prerequisites scenario does not cover it). Probe is +-- cached so the machine is only loaded once. +local function require_machine_cartesi_binding() + if machine_cartesi_probe == nil then + local machine = machine_cartesi.new() + local instance, err = machine:load(MACHINE_IMAGE) + if instance then + machine_cartesi_probe = { ok = true } + else + machine_cartesi_probe = { + ok = false, + err = "machine_cartesi binding unavailable on this host: " .. tostring(err), + } + end + end + if not machine_cartesi_probe.ok then + error(machine_cartesi_probe.err, 0) + end +end + +table.insert(scenarios, { + name = "prerequisites", + fn = function() + local scenario = "prerequisites" + log.step(scenario, 1, 3, "check cartesi-machine is on PATH") + if not command_exists("cartesi-machine") then + error("cartesi-machine not on PATH") + end + log.step(scenario, 2, 3, "check canonical machine image directory exists") + if not path_is_dir(MACHINE_IMAGE) then + error("missing machine image at " .. MACHINE_IMAGE) + end + log.step(scenario, 3, 3, "record paths used by later scenarios") + log.info("machine image: " .. MACHINE_IMAGE) + log.info("genesis safe_block: " .. tostring(GENESIS_SAFE_BLOCK)) + end, +}) + +table.insert(scenarios, { + name = "cm-inspect-state-query", + fn = function() + local scenario = "cm-inspect-state-query" + require_cartesi_machine() + require_machine_image() + require_machine_cartesi_binding() + + log.step(scenario, 1, 4, "create machine_cartesi adapter") + local machine = machine_cartesi.new() + + log.step(scenario, 2, 4, "load genesis snapshot from " .. MACHINE_IMAGE) + local instance = assert(machine:load(MACHINE_IMAGE), "load snapshot failed") + + log.step(scenario, 3, 4, "run --cmio-inspect-state with query=state (no new inputs)") + local report, inspect_err = machine:inspect_state(instance) + assert_true(report, "inspect failed: " .. tostring(inspect_err)) + + log.step(scenario, 4, 4, "validate inspect report is SSZ (not legacy JSON)") + if report:find("inspect endpoint not implemented", 1, true) then + return skip( + scenario, + "machine image dapp is stale; rebuild with: just canonical-build-machine-image" + ) + end + if report:sub(1, 1) == "{" then + return skip( + scenario, + "machine image still returns JSON export_state; rebuild with: just canonical-build-machine-image" + ) + end + assert_true(#report >= 76, "inspect SSZ report too short: " .. tostring(#report)) + log.info("inspect report bytes=" .. tostring(#report)) + end, +}) + +table.insert(scenarios, { + name = "compare-runner-with-sequencer", + fn = function() + local scenario = "compare-runner-with-sequencer" + local sequencer_url = os.getenv("WATCHDOG_E2E_SEQUENCER_URL") + if not sequencer_url or sequencer_url == "" then + return skip( + scenario, + "set WATCHDOG_E2E_SEQUENCER_URL to a live sequencer base URL to run this scenario" + ) + end + require_cartesi_machine() + require_machine_image() + require_machine_cartesi_binding() + + local http_mod = require("watchdog.http") + local jsonrpc = require("watchdog.jsonrpc") + local sequencer_reader = require("watchdog.sequencer_reader") + local json = require("watchdog.json").new() + local main_mod = require("watchdog.main") + + local state_dir = temp_dir("watchdog-e2e-compare") + log.step(scenario, 1, 2, "prepare watchdog deps (sequencer=" .. sequencer_url .. ")") + log.step(scenario, 2, 2, "run compare runner against live sequencer + CM") + + local http = http_mod.new() + local cfg = { + sequencer_url = sequencer_url, + state_dir = state_dir, + cm_snapshot_dir = MACHINE_IMAGE, + cm_snapshot_safe_block = GENESIS_SAFE_BLOCK, + l1_rpc_url = os.getenv("WATCHDOG_E2E_L1_RPC_URL") or "http://127.0.0.1:8545", + input_box_address = os.getenv("WATCHDOG_E2E_INPUTBOX_ADDRESS") + or "0x0000000000000000000000000000000000000000", + app_address = os.getenv("WATCHDOG_E2E_APP_ADDRESS") + or "0x1111111111111111111111111111111111111111", + long_block_range_error_codes = { "-32005" }, + retry_attempts = 1, + retry_delay_sec = 0, + } + + assert_true(main_mod.run_init(cfg, { + machine = machine_cartesi.new(), + }), "watchdog init failed") + + local step_no = 0 + local result, err = runner.run_once(cfg, { + http = http, + rpc = jsonrpc.new(http, json, cfg.l1_rpc_url), + sequencer = sequencer_reader.new(http, json, sequencer_url), + machine = machine_cartesi.new(), + log_step = function(message) + step_no = step_no + 1 + log.step(scenario .. "/runner", step_no, 12, message) + end, + }) + + assert_true(result, "compare run failed: " .. tostring(err)) + log.info(string.format( + "compare ok: safe_block=%s input_count=%s", + tostring(result.safe_block), + tostring(result.input_count) + )) + end, +}) + +table.insert(scenarios, { + name = "machine-cartesi-store-reload-advance", + fn = function() + local scenario = "machine-cartesi-store-reload-advance" + require_cartesi_machine() + require_machine_image() + require_machine_cartesi_binding() + + local checkpoint_dir = temp_dir("watchdog-e2e-store-reload") + + -- Advance the in-process CM one block from `source_dir` and store a + -- checkpoint at `block` via checkpoint.write (the production store path). + local function advance_and_store(source_dir, prev_block, block) + local machine = machine_cartesi.new() + local instance = assert(machine:load(source_dir, prev_block), "load failed") + assert(machine:advance(instance, {}, { from_block = prev_block + 1, to_block = block })) + return checkpoint.write(checkpoint_dir, block, function(snapshot_dir) + return machine:dump(instance, snapshot_dir, block) + end, { created_at = "2026-01-01T00:00:00Z" }) + end + + log.step(scenario, 1, 4, "advance genesis -> block 1 and store a checkpoint") + assert_true(advance_and_store(MACHINE_IMAGE, GENESIS_SAFE_BLOCK, 1), "first store failed") + + log.step(scenario, 2, 4, "reload the stored block-1 snapshot and advance -> block 2") + local reloaded = checkpoint.load(checkpoint_dir) + assert_true(reloaded and reloaded.safe_block == 1, "checkpoint at block 1 missing") + assert_true(advance_and_store(reloaded.snapshot_dir, 1, 2), "reload + store failed") + + log.step(scenario, 3, 4, "load current checkpoint metadata (block 1 should be GC'd)") + local current = checkpoint.load(checkpoint_dir) + assert_true(current and current.safe_block == 2, "current safe_block mismatch") + + log.step(scenario, 4, 4, "verify snapshot directory exists") + assert_true(path_is_dir(current.snapshot_dir), "stored snapshot directory missing") + end, +}) + +log.info("starting watchdog real end-to-end suite (" .. #scenarios .. " scenarios)") +for _, scenario in ipairs(scenarios) do + run_scenario(scenario.name, scenario.fn) +end + +io.write("\n[watchdog-e2e] ───────────────────────────────────────────────────────\n") +io.write(string.format( + "[watchdog-e2e] SUMMARY: %d passed, %d skipped, %d failed (of %d scenarios)\n", + #scenarios - failures - skips, + skips, + failures, + #scenarios +)) + +if failures > 0 then + os.exit(1) +end diff --git a/watchdog/tests/e2e_log.lua b/watchdog/tests/e2e_log.lua new file mode 100644 index 0000000..1e3525a --- /dev/null +++ b/watchdog/tests/e2e_log.lua @@ -0,0 +1,45 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +local e2e_log = {} + +function e2e_log.banner(title) + io.write("\n[watchdog-e2e] ═══════════════════════════════════════════════════════\n") + io.write("[watchdog-e2e] SCENARIO: " .. tostring(title) .. "\n") + io.write("[watchdog-e2e] ═══════════════════════════════════════════════════════\n") + io.flush() +end + +function e2e_log.step(scenario, index, total, message) + io.write(string.format( + "[watchdog-e2e] [%s] step %02d/%02d: %s\n", + tostring(scenario), + index, + total, + tostring(message) + )) + io.flush() +end + +function e2e_log.info(message) + io.write("[watchdog-e2e] INFO: " .. tostring(message) .. "\n") + io.flush() +end + +function e2e_log.skip(scenario, reason) + io.write(string.format("[watchdog-e2e] [%s] SKIP: %s\n", tostring(scenario), tostring(reason))) + io.flush() +end + +function e2e_log.pass(scenario, detail) + local suffix = detail and (": " .. tostring(detail)) or "" + io.write(string.format("[watchdog-e2e] [%s] PASS%s\n", tostring(scenario), suffix)) + io.flush() +end + +function e2e_log.fail(scenario, err) + io.write(string.format("[watchdog-e2e] [%s] FAIL: %s\n", tostring(scenario), tostring(err))) + io.flush() +end + +return e2e_log diff --git a/watchdog/tests/fixtures/input_added_evm_advance.lua b/watchdog/tests/fixtures/input_added_evm_advance.lua new file mode 100644 index 0000000..da542eb --- /dev/null +++ b/watchdog/tests/fixtures/input_added_evm_advance.lua @@ -0,0 +1,30 @@ +-- Static fixture for an InputBox `InputAdded(address,uint256,bytes)` log whose +-- `bytes input` field contains an EvmAdvance calldata envelope. + +return { + log = { + blockNumber = "0x63", + transactionIndex = "0x0", + logIndex = "0x0", + data = "0x" + .. "0000000000000000000000000000000000000000000000000000000000000020" + .. "0000000000000000000000000000000000000000000000000000000000000144" + .. "1234567800000000000000000000000000000000000000000000000000000000" + .. "00007a6900000000000000000000000011111111111111111111111111111111" + .. "1111111100000000000000000000000022222222222222222222222222222222" + .. "2222222200000000000000000000000000000000000000000000000000000000" + .. "0000006300000000000000000000000000000000000000000000000000000000" + .. "000004d200000000000000000000000000000000000000000000000000000000" + .. "0000000000000000000000000000000000000000000000000000000000000000" + .. "0000000300000000000000000000000000000000000000000000000000000000" + .. "0000010000000000000000000000000000000000000000000000000000000000" + .. "0000000400aabbcc000000000000000000000000000000000000000000000000" + .. "0000000000000000000000000000000000000000000000000000000000000000", + }, + expected = { + app_contract = "0x1111111111111111111111111111111111111111", + msg_sender = "0x2222222222222222222222222222222222222222", + block_number = 99, + payload_hex = "00aabbcc", + }, +} diff --git a/watchdog/tests/run.lua b/watchdog/tests/run.lua new file mode 100644 index 0000000..d949dcb --- /dev/null +++ b/watchdog/tests/run.lua @@ -0,0 +1,1085 @@ +-- (c) Cartesi and individual authors (see AUTHORS) +-- SPDX-License-Identifier: Apache-2.0 (see LICENSE) + +package.path = "./?.lua;./?/init.lua;" .. package.path + +local abi = require("watchdog.abi") +local checkpoint = require("watchdog.checkpoint") +local compare = require("watchdog.compare") +local config = require("watchdog.config") +local jsonrpc = require("watchdog.jsonrpc") +local l1_reader = require("watchdog.l1_reader") +local main_mod = require("watchdog.main") +local retry = require("watchdog.retry") +local runner = require("watchdog.runner") +local sequencer_reader = require("watchdog.sequencer_reader") +local state_mod = require("watchdog.state") + +local tests = {} + +local function test(name, fn) + table.insert(tests, { name = name, fn = fn }) +end + +local function assert_eq(actual, expected) + if actual ~= expected then + error(string.format("expected %q, got %q", tostring(expected), tostring(actual)), 2) + end +end + +test("raw compare fails byte-different JSON", function() + local ok, offset = compare.raw_equal('{"a":1}', '{ "a": 1 }') + assert_eq(ok, false) + assert(offset ~= nil, "expected mismatch offset") +end) + +test("decodes InputAdded log EvmAdvance envelope", function() + local fixture = dofile("watchdog/tests/fixtures/input_added_evm_advance.lua") + local decoded = abi.decode_input_added_log(fixture.log) + assert_eq(decoded.app_contract, fixture.expected.app_contract) + assert_eq(decoded.msg_sender, fixture.expected.msg_sender) + assert_eq(decoded.block_number, fixture.expected.block_number) + assert_eq(abi.hex_from_bytes(decoded.payload), fixture.expected.payload_hex) + assert(decoded.raw_input ~= nil and #decoded.raw_input > 0, "fixture keeps raw input bytes") +end) + +test("sorts logs in L1 order", function() + local logs = { + { blockNumber = "0x2", transactionIndex = "0x0", logIndex = "0x5" }, + { blockNumber = "0x1", transactionIndex = "0x9", logIndex = "0x0" }, + { blockNumber = "0x2", transactionIndex = "0x0", logIndex = "0x1" }, + } + l1_reader.sort_logs(logs) + assert_eq(logs[1].blockNumber, "0x1") + assert_eq(logs[2].logIndex, "0x1") + assert_eq(logs[3].logIndex, "0x5") +end) + +local function load_partition_vector() + local json = require("watchdog.json").new() + local path = "tests/fixtures/l1_partition_vector.json" + local file, err = io.open(path, "rb") + if not file then + error("open " .. path .. ": " .. tostring(err)) + end + local body = file:read("*a") + file:close() + return json.decode(body) +end + +local function fail_lookup(fail_ranges) + local map = {} + for _, entry in ipairs(fail_ranges) do + map[entry.from .. ":" .. entry.to] = entry.message + end + return map +end + +local function load_wallet_snapshot_hex_fixture() + local path = "tests/fixtures/wallet_snapshot_v1_empty.hex" + local file, err = io.open(path, "rb") + if not file then + error("open " .. path .. ": " .. tostring(err)) + end + local hex = file:read("*a"):gsub("%s+", "") + file:close() + local bytes = {} + for i = 1, #hex, 2 do + table.insert(bytes, string.char(tonumber(hex:sub(i, i + 1), 16))) + end + return table.concat(bytes) +end + +test("wallet SSZ golden fixture loads for cross-stack parity", function() + local bytes = load_wallet_snapshot_hex_fixture() + assert(#bytes > 0, "golden fixture must not be empty") + -- Fixed prefix from WalletSnapshotV1 default config (see wallet_snapshot.rs tests). + assert_eq(bytes:byte(1), 0xac) + assert_eq(bytes:byte(2), 0xa6) +end) + +test("shared partition vector matches l1_reader bisect plan", function() + local vector = load_partition_vector() + local codes = vector.long_block_range_error_codes + + local defaults = l1_reader.DEFAULT_LONG_BLOCK_RANGE_ERROR_CODES + assert_eq(#codes, #defaults) + for i, code in ipairs(codes) do + assert_eq(code, defaults[i]) + end + + for _, scenario in ipairs(vector.scenarios) do + local calls = {} + local fails = fail_lookup(scenario.fail_ranges) + local rpc = {} + function rpc.get_logs(_self, filter) + table.insert(calls, { filter.from_block, filter.to_block }) + local message = fails[filter.from_block .. ":" .. filter.to_block] + if message then + return nil, message + end + return {} + end + + local logs, err = l1_reader.fetch_logs_partitioned(rpc, { + start_block = scenario.start_block, + end_block = scenario.end_block, + input_box_address = "0xinputbox", + app_address = "0x1111111111111111111111111111111111111111", + long_block_range_error_codes = codes, + }) + + if scenario.expect_ok then + assert(logs, scenario.name .. ": " .. tostring(err)) + else + assert_eq(logs, nil, scenario.name) + assert(type(err) == "string", scenario.name) + end + + assert_eq(#calls, #scenario.expect_calls, scenario.name .. " call count") + for i, expected in ipairs(scenario.expect_calls) do + assert_eq(calls[i][1], expected[1], scenario.name .. " call " .. i .. " from") + assert_eq(calls[i][2], expected[2], scenario.name .. " call " .. i .. " to") + end + end +end) + +test("l1_reader streams successful partitions in L1 order", function() + local calls = {} + local chunks = {} + local rpc = {} + function rpc.get_logs(_self, filter) + table.insert(calls, { filter.from_block, filter.to_block }) + if filter.from_block == 1 and filter.to_block == 4 then + return nil, "-32005: range too large" + end + if filter.from_block == 1 and filter.to_block == 2 then + return { + { blockNumber = "0x2", transactionIndex = "0x0", logIndex = "0x2" }, + { blockNumber = "0x1", transactionIndex = "0x0", logIndex = "0x1" }, + } + end + if filter.from_block == 3 and filter.to_block == 4 then + return { + { blockNumber = "0x4", transactionIndex = "0x0", logIndex = "0x0" }, + } + end + error("unexpected range") + end + + local count, err = l1_reader.for_each_log_chunk_partitioned(rpc, { + start_block = 1, + end_block = 4, + input_box_address = "0xinputbox", + app_address = "0x1111111111111111111111111111111111111111", + long_block_range_error_codes = { "-32005" }, + }, function(logs, range) + table.insert(chunks, { + from_block = range.from_block, + to_block = range.to_block, + first_block = logs[1] and logs[1].blockNumber, + count = #logs, + }) + return true + end) + + assert(count, err) + assert_eq(count, 3) + assert_eq(#calls, 3) + assert_eq(calls[1][1], 1) + assert_eq(calls[1][2], 4) + assert_eq(calls[2][1], 1) + assert_eq(calls[2][2], 2) + assert_eq(calls[3][1], 3) + assert_eq(calls[3][2], 4) + assert_eq(#chunks, 2) + assert_eq(chunks[1].from_block, 1) + assert_eq(chunks[1].to_block, 2) + assert_eq(chunks[1].first_block, "0x1") + assert_eq(chunks[2].from_block, 3) + assert_eq(chunks[2].to_block, 4) +end) + +test("l1_reader ensure_rpc_head_at_least accepts head at target", function() + local head, err = l1_reader.ensure_rpc_head_at_least({ + get_block_number_by_tag = function(_self, tag) + assert_eq(tag, "latest") + return 42 + end, + }, 42) + assert_eq(head, 42) + assert_eq(err, nil) +end) + +test("l1_reader ensure_rpc_head_at_least rejects lagging head", function() + local head, err = l1_reader.ensure_rpc_head_at_least({ + get_block_number_by_tag = function() + return 8 + end, + }, 10) + assert_eq(head, nil) + assert(type(err) == "string", "expected retry error") + assert(err:find("lags target block", 1, true) ~= nil, err) +end) + +test("shared log sort vector matches l1_reader.sort_logs", function() + local vector = load_partition_vector() + local logs = vector.log_sort.unsorted + l1_reader.sort_logs(logs) + + for i, expected in ipairs(vector.log_sort.expect_block_order) do + assert_eq(logs[i].blockNumber, expected, "block order at " .. i) + end + for i, expected in ipairs(vector.log_sort.expect_log_index_order) do + assert_eq(logs[i].logIndex, expected, "log index order at " .. i) + end +end) + +test("jsonrpc get_logs builds InputAdded app filter", function() + local captured = nil + local json = {} + function json.encode(value) + captured = value + return "encoded" + end + function json.decode(_body) + return { jsonrpc = "2.0", id = 1, result = {} } + end + + local http = {} + function http.post(_self, url, body, headers) + assert_eq(url, "http://rpc") + assert_eq(body, "encoded") + assert_eq(headers["content-type"], "application/json") + return { status = 200, body = "{}" } + end + + local client = jsonrpc.new(http, json, "http://rpc") + local logs, err = client:get_logs({ + address = "0x9999999999999999999999999999999999999999", + app_address = "0x1111111111111111111111111111111111111111", + from_block = 10, + to_block = 12, + input_added_topic = l1_reader.INPUT_ADDED_TOPIC, + }) + + assert(logs, err) + assert(type(captured) == "table", "json request captured") + local request = captured + assert_eq(request.method, "eth_getLogs") + local filter = request.params[1] + assert_eq(filter.fromBlock, "0xa") + assert_eq(filter.toBlock, "0xc") + assert_eq(filter.address, "0x9999999999999999999999999999999999999999") + assert_eq(filter.topics[1], l1_reader.INPUT_ADDED_TOPIC) + assert_eq( + filter.topics[2], + "0x0000000000000000000000001111111111111111111111111111111111111111" + ) +end) + +test("config loads snapshot directory safe block and optional topic", function() + local env = { + WATCHDOG_SEQUENCER_URL = "http://seq", + WATCHDOG_INPUTBOX_ADDRESS = "0x9999999999999999999999999999999999999999", + WATCHDOG_APP_ADDRESS = "0x1111111111111111111111111111111111111111", + WATCHDOG_INPUT_ADDED_TOPIC = "0xtopic", + WATCHDOG_STATE_DIR = "/tmp/watchdog-state", + WATCHDOG_CM_SNAPSHOT_DIR = "/tmp/snapshot", + WATCHDOG_CM_SNAPSHOT_SAFE_BLOCK = "42", + } + + local cfg = config.load(env) + + assert_eq(cfg.input_added_topic, "0xtopic") + assert_eq(cfg.state_dir, "/tmp/watchdog-state") + assert_eq(cfg.cm_snapshot_dir, "/tmp/snapshot") + assert_eq(cfg.cm_snapshot_safe_block, 42) + assert_eq(cfg.l1_rpc_url, nil) +end) + +test("config requires a sequencer URL", function() + local ok, err = pcall(function() + config.load({ + WATCHDOG_INPUTBOX_ADDRESS = "0x9999999999999999999999999999999999999999", + WATCHDOG_APP_ADDRESS = "0x1111111111111111111111111111111111111111", + WATCHDOG_STATE_DIR = "/tmp/watchdog-state", + }) + end) + assert_eq(ok, false) + assert(tostring(err):find("WATCHDOG_SEQUENCER_URL", 1, true) ~= nil, "sequencer URL is required") +end) + +test("checkpoint writes manifest-backed head pointer", function() + local dir = os.tmpname() + os.remove(dir) + os.execute(string.format('mkdir -p "%s"', dir)) + + local written, err = checkpoint.write(dir, 12, function(snapshot_dir) + os.execute(string.format('mkdir -p "%s"', snapshot_dir)) + local file = io.open(snapshot_dir .. "/marker", "wb") + assert(file ~= nil, "marker file opened") + file:write("snapshot") + file:close() + return true + end, { + created_at = "2026-04-28T00:00:00Z", + }) + assert(written, err) + + local loaded, load_err = checkpoint.load(dir) + assert(loaded, load_err) + assert_eq(loaded.snapshot_dir, dir .. "/checkpoints/00000000000000000012/snapshot") + assert(loaded.manifest_json:find('"safe_block":12', 1, true) ~= nil, "manifest has safe block") +end) + +test("checkpoint load rejects missing head pointer", function() + local dir = os.tmpname() + os.remove(dir) + os.execute(string.format('mkdir -p "%s"', dir)) + + local loaded, err = checkpoint.load(dir) + assert_eq(loaded, nil) + assert_eq(err, "missing head.json") +end) + +test("checkpoint rejects head pointer outside checkpoint namespace", function() + local dir = os.tmpname() + os.remove(dir) + os.execute(string.format('mkdir -p "%s"', dir)) + local file = assert(io.open(dir .. "/head.json", "wb"), "head pointer opened") + file:write('{"checkpoint":"../outside"}\n') + file:close() + + local loaded, err = checkpoint.load(dir) + assert_eq(loaded, nil) + assert_eq(err, "invalid checkpoint pointer") +end) + +test("checkpoint rejects manifest without safe block", function() + local safe_block, err = checkpoint.safe_block_from_manifest("{}") + assert_eq(safe_block, nil) + assert_eq(err, "manifest missing safe_block") +end) + +test("checkpoint prepare clears stale snapshot dir before write", function() + local dir = os.tmpname() + os.remove(dir) + local stale_snapshot = dir .. "/checkpoints/00000000000000000012/snapshot" + os.execute(string.format('mkdir -p "%s"', stale_snapshot)) + local stale = io.open(stale_snapshot .. "/garbage", "wb") + assert(stale ~= nil, "stale file opened") + stale:write("leftover") + stale:close() + + local written, err = checkpoint.write(dir, 12, function(snapshot_dir) + os.execute(string.format('mkdir -p "%s"', snapshot_dir)) + local file = io.open(snapshot_dir .. "/marker", "wb") + assert(file ~= nil, "marker file opened") + file:write("fresh") + file:close() + return true + end) + assert(written, err) + + local marker = io.open(stale_snapshot .. "/marker", "rb") + assert(marker ~= nil, "fresh marker exists") + assert_eq(marker:read("*a"), "fresh") + marker:close() + assert_eq(io.open(stale_snapshot .. "/garbage", "rb"), nil) +end) + +test("checkpoint refuses same-block rewrite before clearing selected snapshot", function() + local dir = os.tmpname() + os.remove(dir) + + local written, err = checkpoint.write(dir, 12, function(snapshot_dir) + os.execute(string.format('mkdir -p "%s"', snapshot_dir)) + local file = io.open(snapshot_dir .. "/marker", "wb") + assert(file ~= nil, "marker file opened") + file:write("original") + file:close() + return true + end) + assert(written, err) + + local second, second_err = checkpoint.write(dir, 12, function(_snapshot_dir) + error("same-block rewrite must fail before snapshot_writer") + end) + assert_eq(second, nil) + assert(tostring(second_err):find("refusing to rewrite selected checkpoint", 1, true) ~= nil, tostring(second_err)) + + local marker = io.open(dir .. "/checkpoints/00000000000000000012/snapshot/marker", "rb") + assert(marker ~= nil, "selected checkpoint snapshot remains intact") + assert_eq(marker:read("*a"), "original") + marker:close() +end) + +test("checkpoint write keeps only the current checkpoint (prunes predecessor)", function() + local dir = os.tmpname() + os.remove(dir) + os.execute(string.format('mkdir -p "%s"', dir)) + + -- A non-checkpoint sentinel must never be touched by GC: head.json never + -- points at it. + local sentinel = dir .. "/genesis-image" + os.execute(string.format('mkdir -p "%s"', sentinel)) + + local function write_block(safe_block) + local written, err = checkpoint.write(dir, safe_block, function(snapshot_dir) + os.execute(string.format('mkdir -p "%s"', snapshot_dir)) + local file = assert(io.open(snapshot_dir .. "/marker", "wb"), "marker opened") + file:write("snapshot") + file:close() + return true + end) + assert(written, err) + end + + local function dir_exists(path) + local ok, _, code = os.rename(path, path) + return ok or code == 13 + end + + write_block(1) + write_block(2) + write_block(3) + + -- Only the latest checkpoint survives; the two predecessors are reclaimed. + assert(dir_exists(dir .. "/checkpoints/00000000000000000003"), "current checkpoint kept") + assert_eq(dir_exists(dir .. "/checkpoints/00000000000000000002"), false) + assert_eq(dir_exists(dir .. "/checkpoints/00000000000000000001"), false) + + -- head.json still resolves to the latest, and GC never touched the sentinel. + local loaded, load_err = checkpoint.load(dir) + assert(loaded, load_err) + assert_eq(loaded.safe_block, 3) + assert(dir_exists(sentinel), "non-checkpoint dir must be untouched") +end) + +local function fake_cfg() + return { + state_dir = "/tmp/watchdog-test", + sequencer_url = "http://sequencer", + l1_rpc_url = "http://rpc", + cm_snapshot_dir = "/tmp/genesis-snapshot", + cm_snapshot_safe_block = 0, + input_box_address = "0xinputbox", + app_address = "0x1111111111111111111111111111111111111111", + input_added_topic = "0xtopic", + long_block_range_error_codes = l1_reader.DEFAULT_LONG_BLOCK_RANGE_ERROR_CODES, + retry_attempts = 1, + retry_delay_sec = 0, + } +end + +local function fake_machine(inspect_state) + local machine = { + loaded_path = nil, + fed_inputs = nil, + advance_calls = {}, + } + function machine:load(path, reference_block) + self.loaded_path = path + return { path = path, reference_block = reference_block or 0 } + end + function machine:advance(_instance, inputs, range) + self.fed_inputs = inputs + table.insert(self.advance_calls, { + from_block = range.from_block, + to_block = range.to_block, + input_count = #inputs, + }) + _instance.reference_block = range.to_block + return true + end + function machine:inspect(_self, _instance) + return inspect_state + end + function machine:dump(_instance, snapshot_dir, reference_block) + self.saved_snapshot_dir = snapshot_dir + _instance.reference_block = reference_block + return true + end + function machine:feed_inputs(instance, inputs) + local from = (instance.reference_block or 0) + 1 + return self:advance(instance, inputs, { from_block = from, to_block = from }) + end + function machine:inspect_state(_self, instance) + return self:inspect(_self, instance) + end + function machine:save(instance, snapshot_dir) + return self:dump(instance, snapshot_dir, instance.reference_block) + end + return machine +end + +test("init stores bootstrap snapshot as watchdog head", function() + local dir = os.tmpname() + os.remove(dir) + + local cfg = fake_cfg() + cfg.state_dir = dir + cfg.cm_snapshot_safe_block = 5 + + local machine = fake_machine("{}") + local result, err = main_mod.run_init(cfg, { + machine = machine, + }) + assert(result, err) + assert_eq(result.safe_block, 5) + assert_eq(machine.loaded_path, "/tmp/genesis-snapshot") + + local loaded, load_err = checkpoint.load(dir) + assert(loaded, load_err) + assert_eq(loaded.safe_block, 5) + assert_eq(loaded.snapshot_dir, dir .. "/checkpoints/00000000000000000005/snapshot") + + local persisted, cfg_err = state_mod.read_json(dir, "config.json", require("watchdog.json").new()) + assert(persisted, cfg_err) + assert_eq(persisted.sequencer_url, "http://sequencer") + assert_eq(persisted.l1_rpc_url, nil) + + local tick_cfg = main_mod.load_tick_config({ + WATCHDOG_STATE_DIR = dir, + WATCHDOG_L1_RPC_URL = "http://tick-rpc", + }) + assert_eq(tick_cfg.state_dir, dir) + assert_eq(tick_cfg.sequencer_url, "http://sequencer") + assert_eq(tick_cfg.l1_rpc_url, "http://tick-rpc") +end) + +test("tick config requires current RPC URL outside persisted state", function() + local dir = os.tmpname() + os.remove(dir) + + local cfg = fake_cfg() + cfg.state_dir = dir + + local result, err = main_mod.run_init(cfg, { machine = fake_machine("{}") }) + assert(result, err) + + local ok, load_err = pcall(function() + main_mod.load_tick_config({ + WATCHDOG_STATE_DIR = dir, + }) + end) + assert_eq(ok, false) + assert(tostring(load_err):find("WATCHDOG_L1_RPC_URL", 1, true) ~= nil, tostring(load_err)) +end) + +test("init refuses an already initialized state directory", function() + local dir = os.tmpname() + os.remove(dir) + + local cfg = fake_cfg() + cfg.state_dir = dir + + local first, first_err = main_mod.run_init(cfg, { machine = fake_machine("{}") }) + assert(first, first_err) + + local second, second_err = main_mod.run_init(cfg, { machine = fake_machine("{}") }) + assert_eq(second, nil) + assert(tostring(second_err):find("already initialized", 1, true) ~= nil, tostring(second_err)) +end) + +test("runner happy path replays inputs and writes checkpoint", function() + local checkpoint_writes = {} + local checkpoint_mod = { + load = function(_dir) + return { + snapshot_dir = "/tmp/checkpoints/0001/snapshot", + safe_block = 10, + } + end, + write = function(dir, safe_block, snapshot_writer, manifest) + local ok, err = snapshot_writer("/tmp/new-snapshot") + assert(ok, err) + table.insert(checkpoint_writes, { + dir = dir, + safe_block = safe_block, + manifest = manifest, + }) + return true + end, + } + local machine = fake_machine('{"ok":true}') + local result, err = runner.run_once(fake_cfg(), { + checkpoint = checkpoint_mod, + sequencer = { + get_finalized_inclusion_block = function() + return { inclusion_block = 12, l2_tx_index = 0 } + end, + get_finalized_state = function() + return { + inclusion_block = 12, + l2_tx_index = 0, + state = '{"ok":true}', + } + end, + }, + fetch_inputs = function(from_block, to_block) + assert_eq(from_block, 11) + assert_eq(to_block, 12) + return { { payload = "a" }, { payload = "b" } } + end, + machine = machine, + }) + + assert(result, err) + assert_eq(result.safe_block, 12) + assert_eq(result.input_count, 2) + assert_eq(machine.loaded_path, "/tmp/checkpoints/0001/snapshot") + assert_eq(machine.saved_snapshot_dir, "/tmp/new-snapshot") + assert_eq(#machine.fed_inputs, 2) + assert_eq(#checkpoint_writes, 1) + assert_eq(checkpoint_writes[1].safe_block, 12) +end) + +test("runner advances CM as streamed input chunks arrive", function() + local checkpoint_writes = {} + local checkpoint_mod = { + load = function(_dir) + return { + snapshot_dir = "/tmp/checkpoints/0001/snapshot", + safe_block = 10, + } + end, + write = function(_dir, safe_block, snapshot_writer, _manifest) + local ok, err = snapshot_writer("/tmp/new-snapshot") + assert(ok, err) + table.insert(checkpoint_writes, safe_block) + return true + end, + } + local machine = fake_machine('{"ok":true}') + local result, err = runner.run_once(fake_cfg(), { + checkpoint = checkpoint_mod, + sequencer = { + get_finalized_inclusion_block = function() + return { inclusion_block = 12, l2_tx_index = 0 } + end, + get_finalized_state = function() + return { + inclusion_block = 12, + l2_tx_index = 0, + state = '{"ok":true}', + } + end, + }, + for_each_input_chunk = function(from_block, to_block, on_chunk) + assert_eq(from_block, 11) + assert_eq(to_block, 12) + local ok, chunk_err = on_chunk({ { raw_input = "a" } }, { + from_block = 11, + to_block = 11, + }) + assert(ok, chunk_err) + ok, chunk_err = on_chunk({ { raw_input = "b" }, { raw_input = "c" } }, { + from_block = 12, + to_block = 12, + }) + assert(ok, chunk_err) + return 3 + end, + machine = machine, + }) + + assert(result, err) + assert_eq(result.input_count, 3) + assert_eq(#machine.advance_calls, 2) + assert_eq(machine.advance_calls[1].from_block, 11) + assert_eq(machine.advance_calls[1].input_count, 1) + assert_eq(machine.advance_calls[2].from_block, 12) + assert_eq(machine.advance_calls[2].input_count, 2) + assert_eq(#checkpoint_writes, 1) + assert_eq(checkpoint_writes[1], 12) +end) + +test("runner advances CM over empty streamed partitions", function() + local machine = fake_machine('{"ok":true}') + local result, err = runner.run_once(fake_cfg(), { + checkpoint = { + load = function(_dir) + return { + snapshot_dir = "/tmp/checkpoints/0001/snapshot", + safe_block = 10, + } + end, + write = function(_dir, safe_block, snapshot_writer, _manifest) + local ok, write_err = snapshot_writer("/tmp/new-snapshot") + assert(ok, write_err) + assert_eq(safe_block, 12) + return true + end, + }, + sequencer = { + get_finalized_inclusion_block = function() + return { inclusion_block = 12, l2_tx_index = 0 } + end, + get_finalized_state = function() + return { + inclusion_block = 12, + l2_tx_index = 0, + state = '{"ok":true}', + } + end, + }, + for_each_input_chunk = function(_from_block, _to_block, on_chunk) + local ok, chunk_err = on_chunk({}, { + from_block = 11, + to_block = 11, + }) + assert(ok, chunk_err) + ok, chunk_err = on_chunk({ { raw_input = "a" } }, { + from_block = 12, + to_block = 12, + }) + assert(ok, chunk_err) + return 1 + end, + machine = machine, + }) + + assert(result, err) + assert_eq(result.input_count, 1) + assert_eq(#machine.advance_calls, 2) + assert_eq(machine.advance_calls[1].from_block, 11) + assert_eq(machine.advance_calls[1].to_block, 11) + assert_eq(machine.advance_calls[1].input_count, 0) + assert_eq(machine.advance_calls[2].from_block, 12) + assert_eq(machine.advance_calls[2].input_count, 1) +end) + +test("runner returns state mismatch payload", function() + local result, err = runner.run_once(fake_cfg(), { + checkpoint = { + load = function(_dir) + return { snapshot_dir = "/tmp/snapshot", safe_block = 1 } + end, + }, + sequencer = { + get_finalized_inclusion_block = function() + return { inclusion_block = 2, l2_tx_index = 0 } + end, + get_finalized_state = function() + return { + inclusion_block = 2, + l2_tx_index = 0, + state = '{"a":1}', + } + end, + }, + fetch_inputs = function(from_block, to_block) + assert_eq(from_block, 2) + assert_eq(to_block, 2) + return {} + end, + machine = fake_machine('{ "a": 1 }'), + }) + + assert_eq(result, nil) + assert(type(err) == "table", "expected mismatch payload") + assert_eq(err.kind, "state_mismatch") +end) + +test("runner refuses missing or corrupt watchdog head", function() + local ok, err = pcall(function() + return runner.run_once(fake_cfg(), { + checkpoint = { + load = function(_dir) + return nil, "invalid checkpoint pointer" + end, + }, + sequencer = { + get_finalized_inclusion_block = function() + error("sequencer must not be queried after corrupt checkpoint") + end, + }, + machine = fake_machine("{}"), + }) + end) + + assert_eq(ok, false) + assert(tostring(err):find("failed to load watchdog head", 1, true) ~= nil, tostring(err)) +end) + +test("runner returns transient error when L1 RPC head lags target block", function() + local result, err = runner.run_once(fake_cfg(), { + checkpoint = { + load = function(_dir) + return { snapshot_dir = "/tmp/snapshot", safe_block = 5 } + end, + }, + sequencer = { + get_finalized_inclusion_block = function() + return { inclusion_block = 10, l2_tx_index = 0 } + end, + }, + rpc = { + get_block_number_by_tag = function(_self, tag) + assert_eq(tag, "latest") + return 8 + end, + get_logs = function() + error("get_logs must not run when RPC head lags target block") + end, + }, + machine = fake_machine(string.char(1)), + }) + + assert_eq(result, nil) + assert(type(err) == "string", "expected transient retry error") + assert(err:find("RPC latest head", 1, true) ~= nil, err) +end) + +test("runner returns transient error when finalized inclusion_block moves during compare", function() + local result, err = runner.run_once(fake_cfg(), { + checkpoint = { + load = function(_dir) + return { snapshot_dir = "/tmp/snapshot", safe_block = 0 } + end, + }, + sequencer = { + get_finalized_inclusion_block = function() + return { inclusion_block = 1, l2_tx_index = 0 } + end, + get_finalized_state = function() + return { + inclusion_block = 2, + l2_tx_index = 0, + state = string.char(1), + } + end, + }, + fetch_inputs = function(from_block, to_block) + assert_eq(from_block, 1) + assert_eq(to_block, 1) + return {} + end, + machine = fake_machine(string.char(1)), + }) + + assert_eq(result, nil) + assert(type(err) == "string", "expected transient retry error") + assert(err:find("inclusion_block moved", 1, true) ~= nil, err) +end) + +test("runner skips compare cycle when finalized inclusion_block is unchanged", function() + local machine = fake_machine('{"ok":true}') + local result, err = runner.run_once(fake_cfg(), { + checkpoint = { + load = function(_dir) + return { snapshot_dir = "/tmp/snapshot", safe_block = 5 } + end, + }, + sequencer = { + get_finalized_inclusion_block = function() + return { inclusion_block = 5, l2_tx_index = 0 } + end, + get_finalized_state = function() + error("get_finalized_state must not run when inclusion_block is unchanged") + end, + }, + fetch_inputs = function() + error("fetch_inputs must not run when inclusion_block is unchanged") + end, + machine = machine, + }) + + assert(result, err) + assert_eq(result.skipped, true) + assert_eq(result.skip_reason, "finalized_unchanged") + assert_eq(result.safe_block, 5) + assert_eq(machine.fed_inputs, nil) +end) + +test("runner returns sequencer inclusion_block regression payload", function() + local result, err = runner.run_once(fake_cfg(), { + checkpoint = { + load = function(_dir) + return { snapshot_dir = "/tmp/snapshot", safe_block = 5 } + end, + }, + sequencer = { + get_finalized_inclusion_block = function() + return { inclusion_block = 4, l2_tx_index = 0 } + end, + get_finalized_state = function() + return { + inclusion_block = 4, + l2_tx_index = 0, + state = "{}", + } + end, + }, + machine = fake_machine("{}"), + }) + + assert_eq(result, nil) + assert(type(err) == "table", "expected regression payload") + assert_eq(err.kind, "inclusion_block_regressed") +end) + +test("sequencer client reads finalized inclusion_block", function() + local http = {} + function http.get(_self, url) + assert_eq(url, "http://sequencer/finalized_state/inclusion_block") + return { + status = 200, + body = '{"inclusion_block":7,"l2_tx_index":3}', + headers = {}, + } + end + local json = {} + function json.decode(body) + return { + inclusion_block = 7, + l2_tx_index = 3, + } + end + + local client = sequencer_reader.new(http, json, "http://sequencer/") + local head, err = client:get_finalized_inclusion_block() + assert(head, err) + assert_eq(head.inclusion_block, 7) + assert_eq(head.l2_tx_index, 3) +end) + +test("sequencer client reads finalized SSZ body and headers", function() + local http = {} + function http.get(_self, url, _headers) + assert_eq(url, "http://sequencer/finalized_state") + return { + status = 200, + body = "raw-state", + headers = { + ["x-inclusion-block"] = "9", + ["x-l2-tx-index"] = "1", + }, + } + end + local json = {} + function json.decode(_body) + error("unexpected JSON decode for finalized_state body") + end + + local client = sequencer_reader.new(http, json, "http://sequencer") + local state, err = client:get_finalized_state() + assert(state, err) + assert_eq(state.inclusion_block, 9) + assert_eq(state.l2_tx_index, 1) + assert_eq(state.state, "raw-state") +end) + +test("sequencer client rejects invalid inclusion_block JSON", function() + local http = {} + function http.get(_self, _url) + return { + status = 200, + body = "not-json", + headers = {}, + } + end + local json = {} + function json.decode(_body) + error("decode failed") + end + + local client = sequencer_reader.new(http, json, "http://sequencer") + local head, err = client:get_finalized_inclusion_block() + assert_eq(head, nil) + assert_eq(err, "invalid finalized inclusion_block response JSON") +end) + +test("retry succeeds after transient failures", function() + local attempts = 0 + local sleeps = 0 + local result, err = retry.with_retries(function() + attempts = attempts + 1 + if attempts < 3 then + return nil, "transient" + end + return "ok" + end, { + attempts = 3, + delay_sec = 1, + sleep = function(seconds) + assert_eq(seconds, 1) + sleeps = sleeps + 1 + end, + }) + + assert_eq(result, "ok") + assert_eq(err, nil) + assert_eq(attempts, 3) + assert_eq(sleeps, 2) +end) + +test("retry returns final error after exhaustion", function() + local attempts = 0 + local result, err = retry.with_retries(function() + attempts = attempts + 1 + return nil, "failed-" .. tostring(attempts) + end, { + attempts = 2, + delay_sec = 0, + sleep = function() end, + }) + + assert_eq(result, nil) + assert_eq(err, "failed-2") + assert_eq(attempts, 2) +end) + +test("retry stops immediately on terminal errors", function() + local attempts = 0 + local sleeps = 0 + local result, err = retry.with_retries(function() + attempts = attempts + 1 + return nil, { kind = "state_mismatch" } + end, { + attempts = 3, + delay_sec = 1, + should_retry = function(retry_err) + return not (type(retry_err) == "table" and retry_err.kind == "state_mismatch") + end, + sleep = function(_seconds) + sleeps = sleeps + 1 + end, + }) + + assert_eq(result, nil) + assert(type(err) == "table", "terminal payload returned") + assert_eq(err.kind, "state_mismatch") + assert_eq(attempts, 1) + assert_eq(sleeps, 0) +end) + +local passed = 0 +local failed = {} +for _, t in ipairs(tests) do + local ok, err = pcall(t.fn) + if ok then + passed = passed + 1 + io.write("ok - " .. t.name .. "\n") + else + table.insert(failed, { name = t.name, err = tostring(err) }) + io.stderr:write("FAIL - " .. t.name .. ": " .. tostring(err) .. "\n") + end +end + +local total = #tests +io.write(string.format("\nwatchdog unit tests: %d/%d passed\n", passed, total)) +if #failed > 0 then + io.stderr:write(string.format( + "\n*** %d TEST(S) FAILED ***\n", + #failed + )) + for i, entry in ipairs(failed) do + io.stderr:write(string.format(" %d. %s\n %s\n", i, entry.name, entry.err)) + end + io.stderr:write("\n") + os.exit(1) +end +io.write("all tests passed\n") diff --git a/watchdog/third_party/json.lua b/watchdog/third_party/json.lua new file mode 100644 index 0000000..aba62da --- /dev/null +++ b/watchdog/third_party/json.lua @@ -0,0 +1,379 @@ +-- +-- json.lua — vendored pure-Lua JSON (RPC + structured watchdog events). +-- UPSTREAM: https://github.com/rxi/json.lua @ 0.1.2 (commit on master, MIT) +-- See LICENSE note in file header below. + +-- +-- Copyright (c) 2020 rxi +-- +-- Permission is hereby granted, free of charge, to any person obtaining a copy of +-- this software and associated documentation files (the "Software"), to deal in +-- the Software without restriction, including without limitation the rights to +-- use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +-- of the Software, and to permit persons to whom the Software is furnished to do +-- so, subject to the following conditions: +-- +-- The above copyright notice and this permission notice shall be included in all +-- copies or substantial portions of the Software. +-- +-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +-- SOFTWARE. +-- + +local json = { _version = "0.1.2" } + +------------------------------------------------------------------------------- +-- Encode +------------------------------------------------------------------------------- + +local encode + +local escape_char_map = { + [ "\\" ] = "\\", + [ "\"" ] = "\"", + [ "\b" ] = "b", + [ "\f" ] = "f", + [ "\n" ] = "n", + [ "\r" ] = "r", + [ "\t" ] = "t", +} + +local escape_char_map_inv = { [ "/" ] = "/" } +for k, v in pairs(escape_char_map) do + escape_char_map_inv[v] = k +end + + +local function escape_char(c) + return "\\" .. (escape_char_map[c] or string.format("u%04x", c:byte())) +end + + +local function encode_nil(val) + return "null" +end + + +local function encode_table(val, stack) + local res = {} + stack = stack or {} + + -- Circular reference? + if stack[val] then error("circular reference") end + + stack[val] = true + + if rawget(val, 1) ~= nil or next(val) == nil then + -- Treat as array -- check keys are valid and it is not sparse + local n = 0 + for k in pairs(val) do + if type(k) ~= "number" then + error("invalid table: mixed or invalid key types") + end + n = n + 1 + end + if n ~= #val then + error("invalid table: sparse array") + end + -- Encode + for i, v in ipairs(val) do + table.insert(res, encode(v, stack)) + end + stack[val] = nil + return "[" .. table.concat(res, ",") .. "]" + + else + -- Treat as an object + for k, v in pairs(val) do + if type(k) ~= "string" then + error("invalid table: mixed or invalid key types") + end + table.insert(res, encode(k, stack) .. ":" .. encode(v, stack)) + end + stack[val] = nil + return "{" .. table.concat(res, ",") .. "}" + end +end + + +local function encode_string(val) + return '"' .. val:gsub('[%z\1-\31\\"]', escape_char) .. '"' +end + + +local function encode_number(val) + -- Check for NaN, -inf and inf + if val ~= val or val <= -math.huge or val >= math.huge then + error("unexpected number value '" .. tostring(val) .. "'") + end + return string.format("%.14g", val) +end + + +local type_func_map = { + [ "nil" ] = encode_nil, + [ "table" ] = encode_table, + [ "string" ] = encode_string, + [ "number" ] = encode_number, + [ "boolean" ] = tostring, +} + + +encode = function(val, stack) + local t = type(val) + local f = type_func_map[t] + if f then + return f(val, stack) + end + error("unexpected type '" .. t .. "'") +end + + +function json.encode(val) + return ( encode(val) ) +end + +------------------------------------------------------------------------------- +-- Decode +------------------------------------------------------------------------------- + +local parse + +local function create_set(...) + local res = {} + for i = 1, select("#", ...) do + res[ select(i, ...) ] = true + end + return res +end + +local space_chars = create_set(" ", "\t", "\r", "\n") +local delim_chars = create_set(" ", "\t", "\r", "\n", "]", "}", ",") +local escape_chars = create_set("\\", "/", '"', "b", "f", "n", "r", "t", "u") +local literals = create_set("true", "false", "null") + +local literal_map = { + [ "true" ] = true, + [ "false" ] = false, + [ "null" ] = nil, +} + + +local function next_char(str, idx, set, negate) + for i = idx, #str do + if set[str:sub(i, i)] ~= negate then + return i + end + end + return #str + 1 +end + + +local function decode_error(str, idx, msg) + local line_count = 1 + local col_count = 1 + for i = 1, idx - 1 do + col_count = col_count + 1 + if str:sub(i, i) == "\n" then + line_count = line_count + 1 + col_count = 1 + end + end + error( string.format("%s at line %d col %d", msg, line_count, col_count) ) +end + + +local function codepoint_to_utf8(n) + local f = math.floor + if n <= 0x7f then + return string.char(n) + elseif n <= 0x7ff then + return string.char(f(n / 64) + 192, n % 64 + 128) + elseif n <= 0xffff then + return string.char(f(n / 4096) + 224, f(n % 4096 / 64) + 128, n % 64 + 128) + elseif n <= 0x10ffff then + return string.char(f(n / 262144) + 240, f(n % 262144 / 4096) + 128, + f(n % 4096 / 64) + 128, n % 64 + 128) + end + error( string.format("invalid unicode codepoint '%x'", n) ) +end + + +local function parse_unicode_escape(s) + local n1 = tonumber( s:sub(1, 4), 16 ) + local n2 = tonumber( s:sub(7, 10), 16 ) + if n2 then + return codepoint_to_utf8((n1 - 0xd800) * 0x400 + (n2 - 0xdc00) + 0x10000) + else + return codepoint_to_utf8(n1) + end +end + + +local function parse_string(str, i) + local res = "" + local j = i + 1 + local k = j + + while j <= #str do + local x = str:byte(j) + + if x < 32 then + decode_error(str, j, "control character in string") + + elseif x == 92 then + res = res .. str:sub(k, j - 1) + j = j + 1 + local c = str:sub(j, j) + if c == "u" then + local hex = str:match("^[dD][89aAbB]%x%x\\u%x%x%x%x", j + 1) + or str:match("^%x%x%x%x", j + 1) + or decode_error(str, j - 1, "invalid unicode escape in string") + res = res .. parse_unicode_escape(hex) + j = j + #hex + else + if not escape_chars[c] then + decode_error(str, j - 1, "invalid escape char '" .. c .. "' in string") + end + res = res .. escape_char_map_inv[c] + end + k = j + 1 + + elseif x == 34 then + res = res .. str:sub(k, j - 1) + return res, j + 1 + end + + j = j + 1 + end + + decode_error(str, i, "expected closing quote for string") +end + + +local function parse_number(str, i) + local x = next_char(str, i, delim_chars) + local s = str:sub(i, x - 1) + local n = tonumber(s) + if not n then + decode_error(str, i, "invalid number '" .. s .. "'") + end + return n, x +end + + +local function parse_literal(str, i) + local x = next_char(str, i, delim_chars) + local word = str:sub(i, x - 1) + if not literals[word] then + decode_error(str, i, "invalid literal '" .. word .. "'") + end + return literal_map[word], x +end + + +local function parse_array(str, i) + local res = {} + local n = 1 + i = i + 1 + while 1 do + local x + i = next_char(str, i, space_chars, true) + if str:sub(i, i) == "]" then + i = i + 1 + break + end + x, i = parse(str, i) + res[n] = x + n = n + 1 + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "]" then break end + if chr ~= "," then decode_error(str, i, "expected ']' or ','") end + end + return res, i +end + + +local function parse_object(str, i) + local res = {} + i = i + 1 + while 1 do + local key, val + i = next_char(str, i, space_chars, true) + if str:sub(i, i) == "}" then + i = i + 1 + break + end + if str:sub(i, i) ~= '"' then + decode_error(str, i, "expected string for key") + end + key, i = parse(str, i) + i = next_char(str, i, space_chars, true) + if str:sub(i, i) ~= ":" then + decode_error(str, i, "expected ':' after key") + end + i = next_char(str, i + 1, space_chars, true) + val, i = parse(str, i) + res[key] = val + i = next_char(str, i, space_chars, true) + local chr = str:sub(i, i) + i = i + 1 + if chr == "}" then break end + if chr ~= "," then decode_error(str, i, "expected '}' or ','") end + end + return res, i +end + + +local char_func_map = { + [ '"' ] = parse_string, + [ "0" ] = parse_number, + [ "1" ] = parse_number, + [ "2" ] = parse_number, + [ "3" ] = parse_number, + [ "4" ] = parse_number, + [ "5" ] = parse_number, + [ "6" ] = parse_number, + [ "7" ] = parse_number, + [ "8" ] = parse_number, + [ "9" ] = parse_number, + [ "-" ] = parse_number, + [ "t" ] = parse_literal, + [ "f" ] = parse_literal, + [ "n" ] = parse_literal, + [ "[" ] = parse_array, + [ "{" ] = parse_object, +} + + +parse = function(str, idx) + local chr = str:sub(idx, idx) + local f = char_func_map[chr] + if f then + return f(str, idx) + end + decode_error(str, idx, "unexpected character '" .. chr .. "'") +end + + +function json.decode(str) + if type(str) ~= "string" then + error("expected argument of type string, got " .. type(str)) + end + local res, idx = parse(str, next_char(str, 1, space_chars, true)) + idx = next_char(str, idx, space_chars, true) + if idx <= #str then + decode_error(str, idx, "trailing garbage") + end + return res +end + + +return json diff --git a/watchdog/third_party/lua-curl/LICENSE b/watchdog/third_party/lua-curl/LICENSE new file mode 100644 index 0000000..3850127 --- /dev/null +++ b/watchdog/third_party/lua-curl/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014-2021 Alexey Melnichuk + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/watchdog/third_party/lua-curl/UPSTREAM b/watchdog/third_party/lua-curl/UPSTREAM new file mode 100644 index 0000000..373e0e4 --- /dev/null +++ b/watchdog/third_party/lua-curl/UPSTREAM @@ -0,0 +1,15 @@ +Lua-cURLv3 (lcurl native binding; libcurl must be installed on the host). + +URL: https://github.com/Lua-cURL/Lua-cURLv3 +Commit: 9f8b6dba8b5ef1b26309a571ae75cda4034279e5 +License: MIT (see LICENSE in this directory) + +Vendored in-tree: the curated C subset is committed under `src/` (the `src/*.c` ++ `src/*.h` that make up the `lcurl` C module). We do NOT vendor the optional +`cURL/*.lua` helper layer, the upstream Makefile/build glue, tests, docs, or CI +config. `scripts/watchdog-lua-deps.sh` compiles `src/*.c` directly into +`.deps/lua/lcurl.so` -- there is no build-time download and no pin to verify, +because the compiled bytes are exactly this in-tree source. + +Updating: replace `src/` from a newer upstream commit, refresh LICENSE, and +update the Commit line above. diff --git a/watchdog/third_party/lua-curl/src/l52util.c b/watchdog/third_party/lua-curl/src/l52util.c new file mode 100644 index 0000000..6373687 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/l52util.c @@ -0,0 +1,178 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2021 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#include "l52util.h" + +#include +#include /* for memset */ +#include + +#if LUA_VERSION_NUM >= 502 + +int luaL_typerror (lua_State *L, int narg, const char *tname) { + const char *msg = lua_pushfstring(L, "%s expected, got %s", tname, + luaL_typename(L, narg)); + return luaL_argerror(L, narg, msg); +} + +#ifndef luaL_register + +void luaL_register (lua_State *L, const char *libname, const luaL_Reg *l){ + if(libname) lua_newtable(L); + luaL_setfuncs(L, l, 0); +} + +#endif + +#else + +void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup){ + luaL_checkstack(L, nup, "too many upvalues"); + for (; l->name != NULL; l++) { /* fill the table with given functions */ + int i; + for (i = 0; i < nup; i++) /* copy upvalues to the top */ + lua_pushvalue(L, -nup); + lua_pushcclosure(L, l->func, nup); /* closure with those upvalues */ + lua_setfield(L, -(nup + 2), l->name); + } + lua_pop(L, nup); /* remove upvalues */ +} + +void lua_rawgetp(lua_State *L, int index, const void *p){ + index = lua_absindex(L, index); + lua_pushlightuserdata(L, (void *)p); + lua_rawget(L, index); +} + +void lua_rawsetp (lua_State *L, int index, const void *p){ + index = lua_absindex(L, index); + lua_pushlightuserdata(L, (void *)p); + lua_insert(L, -2); + lua_rawset(L, index); +} + +#endif + +int lutil_newmetatablep (lua_State *L, const void *p) { + lua_rawgetp(L, LUA_REGISTRYINDEX, p); + if (!lua_isnil(L, -1)) /* name already in use? */ + return 0; /* leave previous value on top, but return 0 */ + lua_pop(L, 1); + + lua_newtable(L); /* create metatable */ + lua_pushvalue(L, -1); /* duplicate metatable to set*/ + + lua_pushliteral (L, "__type"); + lua_pushstring(L, p); // push meta name + lua_settable (L, -3); // set meta name + + lua_rawsetp(L, LUA_REGISTRYINDEX, p); + + return 1; +} + +void lutil_getmetatablep (lua_State *L, const void *p) { + lua_rawgetp(L, LUA_REGISTRYINDEX, p); +} + +void lutil_setmetatablep (lua_State *L, const void *p) { + lutil_getmetatablep(L, p); + assert(lua_istable(L,-1)); + lua_setmetatable (L, -2); +} + +int lutil_isudatap (lua_State *L, int ud, const void *p) { + if (lua_isuserdata(L, ud)){ + if (lua_getmetatable(L, ud)) { /* does it have a metatable? */ + int res; + lutil_getmetatablep(L,p); /* get correct metatable */ + res = lua_rawequal(L, -1, -2); /* does it have the correct mt? */ + lua_pop(L, 2); /* remove both metatables */ + return res; + } + } + return 0; +} + +void *lutil_checkudatap (lua_State *L, int ud, const void *p) { + void *up = lua_touserdata(L, ud); + if (up != NULL) { /* value is a userdata? */ + if (lua_getmetatable(L, ud)) { /* does it have a metatable? */ + lutil_getmetatablep(L,p); /* get correct metatable */ + if (lua_rawequal(L, -1, -2)) { /* does it have the correct mt? */ + lua_pop(L, 2); /* remove both metatables */ + return up; + } + } + } + luaL_typerror(L, ud, p); /* else error */ + return NULL; /* to avoid warnings */ +} + +int lutil_createmetap (lua_State *L, const void *p, const luaL_Reg *methods, int nup) { + if (!lutil_newmetatablep(L, p)){ + lua_insert(L, -1 - nup); /* move mt prior upvalues */ + return 0; + } + + lua_insert(L, -1 - nup); /* move mt prior upvalues */ + luaL_setfuncs (L, methods, nup); /* define methods */ + lua_pushliteral (L, "__index"); /* define metamethods */ + lua_pushvalue (L, -2); + lua_settable (L, -3); + return 1; +} + +void *lutil_newudatap_impl(lua_State *L, size_t size, const void *p){ + void *obj = lua_newuserdata (L, size); + memset(obj, 0, size); + lutil_setmetatablep(L, p); + return obj; +} + +void lutil_pushint64(lua_State *L, int64_t v){ + if(sizeof(lua_Integer) >= sizeof(int64_t)){ + lua_pushinteger(L, (lua_Integer)v); + return; + } + lua_pushnumber(L, (lua_Number)v); +} + +void lutil_pushuint(lua_State *L, unsigned int v){ +#if LUA_VERSION_NUM >= 503 + lua_pushinteger(L, (lua_Integer)v); +#else + lua_pushnumber(L, (lua_Number)v); +#endif +} + +int64_t lutil_checkint64(lua_State *L, int idx){ + if(sizeof(lua_Integer) >= sizeof(int64_t)) + return luaL_checkinteger(L, idx); + return (int64_t)luaL_checknumber(L, idx); +} + +int64_t lutil_optint64(lua_State *L, int idx, int64_t v){ + if(sizeof(lua_Integer) >= sizeof(int64_t)) + return luaL_optinteger(L, idx, v); + return (int64_t)luaL_optnumber(L, idx, v); +} + +void lutil_pushnvalues(lua_State *L, int n){ + for(;n;--n) lua_pushvalue(L, -n); +} + +int lutil_is_null(lua_State *L, int i){ + return lua_islightuserdata(L, i) && 0 == lua_touserdata(L, i); +} + +void lutil_push_null(lua_State *L){ + lua_pushlightuserdata(L, (void*)0); +} diff --git a/watchdog/third_party/lua-curl/src/l52util.h b/watchdog/third_party/lua-curl/src/l52util.h new file mode 100644 index 0000000..97348a0 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/l52util.h @@ -0,0 +1,97 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2021 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#ifndef _L52UTIL_H_ +#define _L52UTIL_H_ + +#include "lua.h" +#include "lauxlib.h" +#include + +#if LUA_VERSION_NUM >= 503 /* Lua 5.3 */ + +#ifndef luaL_checkint +#define luaL_checkint luaL_checkinteger +#endif + +#ifndef luaL_checklong +#define luaL_checklong luaL_checkinteger +#endif + +#ifndef luaL_optint +#define luaL_optint luaL_optinteger +#endif + +#ifndef luaL_optlong +#define luaL_optlong luaL_optinteger +#endif + +#endif + +#if LUA_VERSION_NUM >= 502 /* Lua 5.2 */ + +/* lua_rawgetp */ +/* lua_rawsetp */ +/* luaL_setfuncs */ +/* lua_absindex */ + +#ifndef lua_objlen +#define lua_objlen lua_rawlen +#endif + +int luaL_typerror (lua_State *L, int narg, const char *tname); + +#ifndef luaL_register +void luaL_register (lua_State *L, const char *libname, const luaL_Reg *l); +#endif + +#ifndef lua_equal +#define lua_equal(L,idx1,idx2) lua_compare(L,(idx1),(idx2),LUA_OPEQ) +#endif + +#else /* Lua 5.1 */ + +/* functions from lua 5.2 */ + +# define lua_absindex(L, i) (((i)>0)?(i):((i)<=LUA_REGISTRYINDEX?(i):(lua_gettop(L)+(i)+1))) +# define lua_rawlen lua_objlen + +void lua_rawgetp (lua_State *L, int index, const void *p); +void lua_rawsetp (lua_State *L, int index, const void *p); +void luaL_setfuncs (lua_State *L, const luaL_Reg *l, int nup); + +#endif + +int lutil_newmetatablep (lua_State *L, const void *p); +void lutil_getmetatablep (lua_State *L, const void *p); +void lutil_setmetatablep (lua_State *L, const void *p); + +#define lutil_newudatap(L, TTYPE, TNAME) (TTYPE *)lutil_newudatap_impl(L, sizeof(TTYPE), TNAME) +int lutil_isudatap (lua_State *L, int ud, const void *p); +void *lutil_checkudatap (lua_State *L, int ud, const void *p); +int lutil_createmetap (lua_State *L, const void *p, const luaL_Reg *methods, int nup); + +void *lutil_newudatap_impl (lua_State *L, size_t size, const void *p); + +void lutil_pushuint(lua_State *L, unsigned int v); + +void lutil_pushint64(lua_State *L, int64_t v); + +int64_t lutil_checkint64(lua_State *L, int idx); + +int64_t lutil_optint64(lua_State *L, int idx, int64_t v); + +void lutil_pushnvalues(lua_State *L, int n); + +int lutil_is_null(lua_State *L, int i); + +void lutil_push_null(lua_State *L); + +#endif diff --git a/watchdog/third_party/lua-curl/src/lceasy.c b/watchdog/third_party/lua-curl/src/lceasy.c new file mode 100644 index 0000000..ad48022 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lceasy.c @@ -0,0 +1,2469 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2021 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#include "lcurl.h" +#include "lceasy.h" +#include "lcerror.h" +#include "lcutils.h" +#include "lchttppost.h" +#include "lcshare.h" +#include "lcmulti.h" +#include "lcmime.h" +#include "lcurlapi.h" +#include + +static const char *LCURL_ERROR_TAG = "LCURL_ERROR_TAG"; + +#define LCURL_EASY_NAME LCURL_PREFIX" Easy" +static const char *LCURL_EASY = LCURL_EASY_NAME; + +#if LCURL_CURL_VER_GE(7,21,5) +# define LCURL_E_UNKNOWN_OPTION CURLE_UNKNOWN_OPTION +#else +# define LCURL_E_UNKNOWN_OPTION CURLE_UNKNOWN_TELNET_OPTION +#endif + +/* Before call curl_XXX function which can call any callback + * need set Current Lua thread pointer in easy/multi contexts. + * But it also possible that we already in callback call. + * E.g. `curl_easy_pause` function may be called from write callback. + * and it even may be called in different thread. + * ```Lua + * multi:add_handle(easy) + * easy:setopt_writefunction(function(...) + * coroutine.wrap(function() multi:add_handle(easy2) end)() + * end) + * ``` + * So we have to restore previews Lua state in callback contexts. + * But if previews Lua state is NULL then we can just do not set it back. + * But set it to NULL make easier to debug code. + */ +void lcurl__easy_assign_lua(lua_State *L, lcurl_easy_t *p, lua_State *value, int assign_multi){ + if(p->multi && assign_multi){ + lcurl__multi_assign_lua(L, p->multi, value, 1); + } + else{ + p->L = value; + if(p->post){ + p->post->L = value; + } +#if LCURL_CURL_VER_GE(7,56,0) + if(p->mime){ + lcurl_mime_set_lua(L, p->mime, value); + } +#endif + } +} + +//{ + +int lcurl_easy_create(lua_State *L, int error_mode){ + lcurl_easy_t *p; + int i; + + lua_settop(L, 1); /* options */ + + p = lutil_newudatap(L, lcurl_easy_t, LCURL_EASY); + + p->curl = curl_easy_init(); + + p->err_mode = error_mode; + if(!p->curl) return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, CURLE_FAILED_INIT); + + p->magic = LCURL_EASY_MAGIC; + p->L = NULL; + p->post = NULL; + p->multi = NULL; +#if LCURL_CURL_VER_GE(7,56,0) + p->mime = NULL; +#endif + p->storage = lcurl_storage_init(L); + p->wr.cb_ref = p->wr.ud_ref = LUA_NOREF; + p->rd.cb_ref = p->rd.ud_ref = LUA_NOREF; + p->hd.cb_ref = p->hd.ud_ref = LUA_NOREF; + p->pr.cb_ref = p->pr.ud_ref = LUA_NOREF; + p->seek.cb_ref = p->seek.ud_ref = LUA_NOREF; + p->debug.cb_ref = p->debug.ud_ref = LUA_NOREF; + p->match.cb_ref = p->match.ud_ref = LUA_NOREF; + p->chunk_bgn.cb_ref = p->chunk_bgn.ud_ref = LUA_NOREF; + p->chunk_end.cb_ref = p->chunk_end.ud_ref = LUA_NOREF; +#if LCURL_CURL_VER_GE(7,19,6) + p->ssh_key.cb_ref = p->ssh_key.ud_ref = LUA_NOREF; +#endif +#if LCURL_CURL_VER_GE(7,64,0) + p->trailer.cb_ref = p->trailer.ud_ref = LUA_NOREF; +#endif +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + p->hstsread.cb_ref = p->hstsread.ud_ref = LUA_NOREF; + p->hstswrite.cb_ref = p->hstswrite.ud_ref = LUA_NOREF; +#endif + p->rbuffer.ref = LUA_NOREF; + for(i = 0; i < LCURL_LIST_COUNT; ++i){ + p->lists[i] = LUA_NOREF; + } + + if(lua_type(L, 1) == LUA_TTABLE){ + int ret = lcurl_utils_apply_options(L, 1, 2, 1, p->err_mode, LCURL_ERROR_EASY, LCURL_E_UNKNOWN_OPTION); + if(ret) return ret; + assert(lua_gettop(L) == 2); + } + + return 1; +} + +lcurl_easy_t *lcurl_geteasy_at(lua_State *L, int i){ + lcurl_easy_t *p = (lcurl_easy_t *)lutil_checkudatap (L, i, LCURL_EASY); + luaL_argcheck (L, p != NULL, 1, LCURL_EASY_NAME" object expected"); + return p; +} + +static int lcurl_easy_to_s(lua_State *L){ + lcurl_easy_t *p = (lcurl_easy_t *)lutil_checkudatap (L, 1, LCURL_EASY); + lua_pushfstring(L, LCURL_EASY_NAME" (%p)", (void*)p); + return 1; +} + +static int lcurl_easy_cleanup_storage(lua_State *L, lcurl_easy_t *p){ + int i; + + if(p->storage != LUA_NOREF){ + p->storage = lcurl_storage_free(L, p->storage); + } + + p->post = NULL; +#if LCURL_CURL_VER_GE(7,56,0) + p->mime = NULL; +#endif + + luaL_unref(L, LCURL_LUA_REGISTRY, p->wr.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->wr.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->rd.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->rd.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->pr.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->pr.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->seek.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->seek.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->debug.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->debug.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->match.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->match.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->chunk_bgn.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->chunk_bgn.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->chunk_end.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->chunk_end.ud_ref); +#if LCURL_CURL_VER_GE(7,19,6) + luaL_unref(L, LCURL_LUA_REGISTRY, p->ssh_key.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->ssh_key.ud_ref); +#endif +#if LCURL_CURL_VER_GE(7,64,0) + luaL_unref(L, LCURL_LUA_REGISTRY, p->trailer.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->trailer.ud_ref); +#endif +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + luaL_unref(L, LCURL_LUA_REGISTRY, p->hstsread.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->hstsread.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->hstswrite.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->hstswrite.ud_ref); +#endif + luaL_unref(L, LCURL_LUA_REGISTRY, p->hd.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->hd.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->rbuffer.ref); + + p->wr.cb_ref = p->wr.ud_ref = LUA_NOREF; + p->rd.cb_ref = p->rd.ud_ref = LUA_NOREF; + p->hd.cb_ref = p->hd.ud_ref = LUA_NOREF; + p->pr.cb_ref = p->pr.ud_ref = LUA_NOREF; + p->seek.cb_ref = p->seek.ud_ref = LUA_NOREF; + p->debug.cb_ref = p->debug.ud_ref = LUA_NOREF; + p->match.cb_ref = p->match.ud_ref = LUA_NOREF; + p->chunk_bgn.cb_ref = p->chunk_bgn.ud_ref = LUA_NOREF; + p->chunk_end.cb_ref = p->chunk_end.ud_ref = LUA_NOREF; +#if LCURL_CURL_VER_GE(7,19,6) + p->ssh_key.cb_ref = p->ssh_key.ud_ref = LUA_NOREF; +#endif +#if LCURL_CURL_VER_GE(7,64,0) + p->trailer.cb_ref = p->trailer.ud_ref = LUA_NOREF; +#endif +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + p->hstsread.cb_ref = p->hstsread.ud_ref = LUA_NOREF; + p->hstswrite.cb_ref = p->hstswrite.ud_ref = LUA_NOREF; +#endif + p->rbuffer.ref = LUA_NOREF; + + for(i = 0; i < LCURL_LIST_COUNT; ++i){ + p->lists[i] = LUA_NOREF; + } +} + +static int lcurl_easy_cleanup(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + lua_settop(L, 1); + + if(p->multi){ + LCURL_UNUSED_VAR CURLMcode code = lcurl__multi_remove_handle(L, p->multi, p); + + //! @todo what I can do if I can not remove it??? + } + + if(p->curl){ + lua_State *curL; + + // In my tests when I cleanup some easy handle. + // timerfunction called only for single multi handle. + // Also may be this function may call `close` callback + // for `curl_mimepart` structure. + curL = p->L; lcurl__easy_assign_lua(L, p, L, 1); + curl_easy_cleanup(p->curl); +#ifndef LCURL_RESET_NULL_LUA + if(curL != NULL) +#endif + lcurl__easy_assign_lua(L, p, curL, 1); + + p->curl = NULL; + } + + lcurl_easy_cleanup_storage(L, p); + + lua_pushnil(L); + lua_rawset(L, LCURL_USERVALUES); + + return 0; +} + +static int lcurl_easy_perform(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code; + lua_State *curL; + int top = 1; + lua_settop(L, top); + + assert(p->rbuffer.ref == LUA_NOREF); + + // store reference to current coroutine to callbacks + // User should not call `perform` if handle assign to multi + curL = p->L; lcurl__easy_assign_lua(L, p, L, 0); + code = curl_easy_perform(p->curl); +#ifndef LCURL_RESET_NULL_LUA + if(curL != NULL) +#endif + lcurl__easy_assign_lua(L, p, curL, 0); + + if(p->rbuffer.ref != LUA_NOREF){ + luaL_unref(L, LCURL_LUA_REGISTRY, p->rbuffer.ref); + p->rbuffer.ref = LUA_NOREF; + } + + if(code == CURLE_OK){ + lua_settop(L, 1); + return 1; + } + + if((lua_gettop(L) > top)&&(lua_touserdata(L, top + 1) == LCURL_ERROR_TAG)){ + return lua_error(L); + } + + if(code == CURLE_WRITE_ERROR){ + if(lua_gettop(L) > top){ + return lua_gettop(L) - top; + } + } + + if(code == CURLE_ABORTED_BY_CALLBACK){ + if(lua_gettop(L) > top){ + return lua_gettop(L) - top; + } + } + + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); +} + +static int lcurl_easy_escape(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + size_t data_size; const char *data = luaL_checklstring(L, 2, &data_size); + const char *ret = curl_easy_escape(p->curl, data, (int)data_size); + if(!ret){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, CURLE_OUT_OF_MEMORY); + } + lua_pushstring(L, ret); + curl_free((char*)ret); + return 1; +} + +static int lcurl_easy_unescape(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + size_t data_size; const char *data = luaL_checklstring(L, 2, &data_size); + int ret_size; const char *ret = curl_easy_unescape(p->curl, data, (int)data_size, &ret_size); + if(!ret){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, CURLE_OUT_OF_MEMORY); + } + lua_pushlstring(L, ret, ret_size); + curl_free((char*)ret); + return 1; +} + +static int lcurl_easy_reset(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + curl_easy_reset(p->curl); + lua_settop(L, 1); + + lcurl_easy_cleanup_storage(L, p); + p->storage = lcurl_storage_init(L); + + return 1; +} + +#if LCURL_CURL_VER_GE(7,56,0) + +static int lcurl_easy_mime(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_mime_create(L, p->err_mode); +} + +#endif + +#if LCURL_CURL_VER_GE(7,62,0) + +static int lcurl_easy_upkeep(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code = curl_easy_upkeep(p->curl); + if(code == CURLE_OK){ + lua_settop(L, 1); + return 1; + } + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); +} + +#endif + +//{ OPTIONS + +//{ set + +static int lcurl_opt_set_long_(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + long val; CURLcode code; + + if(lua_isboolean(L, 2)){ + val = lua_toboolean(L, 2); + if( val + && ( + (opt == CURLOPT_SSL_VERIFYHOST) +#if LCURL_CURL_VER_GE(7,52,0) + || (opt == CURLOPT_PROXY_SSL_VERIFYHOST) +#endif + ) + ){ + val = 2; + } + } + else{ + luaL_argcheck(L, lua_type(L, 2) == LUA_TNUMBER, 2, "number or boolean expected"); + val = luaL_checklong(L, 2); + } + + code = curl_easy_setopt(p->curl, opt, val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + lua_settop(L, 1); + return 1; +} + +#if LCURL_CURL_VER_GE(7,59,0) + +static int lcurl_opt_set_off_(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + curl_off_t val; CURLcode code; + + luaL_argcheck(L, lua_type(L, 2) == LUA_TNUMBER, 2, "number expected"); + val = lutil_checkint64(L, 2); + + code = curl_easy_setopt(p->curl, opt, val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + lua_settop(L, 1); + return 1; +} + +#endif + +static int lcurl_opt_set_string_(lua_State *L, int opt, int store){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code; const char *value; + + luaL_argcheck(L, lua_type(L, 2) == LUA_TSTRING || lutil_is_null(L, 2), 2, "string expected"); + + value = lua_tostring(L, 2); + code = curl_easy_setopt(p->curl, opt, value); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + if(store){ + if(value) + lcurl_storage_preserve_iv(L, p->storage, opt, 2); + else + lcurl_storage_remove_i(L, p->storage, opt); + } + + lua_settop(L, 1); + return 1; +} + +static int lcurl_opt_set_slist_(lua_State *L, int opt, int list_no){ + lcurl_easy_t *p = lcurl_geteasy(L); + struct curl_slist *list = lcurl_util_to_slist(L, 2); + CURLcode code; + int ref = p->lists[list_no]; + + luaL_argcheck(L, list || lua_istable(L, 2) || lutil_is_null(L, 2), 2, "array expected"); + + if(ref != LUA_NOREF){ + struct curl_slist *tmp = lcurl_storage_remove_slist(L, p->storage, ref); + curl_slist_free_all(tmp); + p->lists[list_no] = LUA_NOREF; + } + + code = curl_easy_setopt(p->curl, opt, list); + + if(code != CURLE_OK){ + curl_slist_free_all(list); + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + if (list) { + p->lists[list_no] = lcurl_storage_preserve_slist(L, p->storage, list); + } + + lua_settop(L, 1); + return 1; +} + +#if LCURL_CURL_VER_GE(7,73,0) + +static int lcurl_opt_set_blob_(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code; const char *value; size_t len; + struct curl_blob blob; + + luaL_argcheck(L, lua_type(L, 2) == LUA_TSTRING || lutil_is_null(L, 2), 2, "string expected"); + + value = lua_tolstring(L, 2, &len); + + blob.data = (void*)value; + blob.len = len; + blob.flags = CURL_BLOB_COPY; + + code = curl_easy_setopt(p->curl, opt, value); + + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lua_settop(L, 1); + return 1; +} + +#endif + +#define LCURL_STR_OPT(N, S) static int lcurl_easy_set_##N(lua_State *L){\ + return lcurl_opt_set_string_(L, CURLOPT_##N, (S)); \ +} + +#define LCURL_LST_OPT(N, S) static int lcurl_easy_set_##N(lua_State *L){\ + return lcurl_opt_set_slist_(L, CURLOPT_##N, LCURL_##N##_LIST);\ +} + +#define LCURL_LNG_OPT(N, S) static int lcurl_easy_set_##N(lua_State *L){\ + return lcurl_opt_set_long_(L, CURLOPT_##N);\ +} + +#define LCURL_OFF_OPT(N, S) static int lcurl_easy_set_##N(lua_State *L){\ + return lcurl_opt_set_off_(L, CURLOPT_##N);\ +} + +#define LCURL_BLB_OPT(N, S) static int lcurl_easy_set_##N(lua_State *L){\ + return lcurl_opt_set_blob_(L, CURLOPT_##N); \ +} + +#define OPT_ENTRY(L, N, T, S, D) LCURL_##T##_OPT(N, S) + +#include "lcopteasy.h" + +#undef OPT_ENTRY + +static int lcurl_easy_set_POSTFIELDS(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + size_t len; const char *val = luaL_checklstring(L, 2, &len); + CURLcode code; + if(lua_isnumber(L, 3)){ + size_t n = (size_t)lua_tonumber(L, 3); + luaL_argcheck(L, len <= n, 3, "data length too big"); + len = n; + } + code = curl_easy_setopt(p->curl, CURLOPT_POSTFIELDS, val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + lcurl_storage_preserve_iv(L, p->storage, CURLOPT_POSTFIELDS, 2); + code = curl_easy_setopt(p->curl, CURLOPT_POSTFIELDSIZE, (long)len); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + lua_settop(L, 1); + return 1; +} + +#undef LCURL_STR_OPT +#undef LCURL_LST_OPT +#undef LCURL_LNG_OPT +#undef LCURL_OFF_OPT +#undef LCURL_BLB_OPT + +static size_t lcurl_hpost_read_callback(char *buffer, size_t size, size_t nitems, void *arg); + +static int lcurl_easy_set_HTTPPOST(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + lcurl_hpost_t *post = lcurl_gethpost_at(L, 2); + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_HTTPPOST, post->post); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_preserve_iv(L, p->storage, CURLOPT_HTTPPOST, 2); + + if(post->stream){ + curl_easy_setopt(p->curl, CURLOPT_READFUNCTION, lcurl_hpost_read_callback); + } + + p->post = post; + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_set_SHARE(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + lcurl_share_t *sh = lcurl_getshare_at(L, 2); + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_SHARE, sh->curl); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_preserve_iv(L, p->storage, CURLOPT_SHARE, 2); + + lua_settop(L, 1); + return 1; +} + +#if LCURL_CURL_VER_GE(7,46,0) + +static int lcurl_easy_set_STREAM_DEPENDS_impl(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + lcurl_easy_t *e = lcurl_geteasy_at(L, 2); + CURLcode code = curl_easy_setopt(p->curl, opt, e->curl); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_preserve_iv(L, p->storage, opt, 2); + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_set_STREAM_DEPENDS(lua_State *L){ + return lcurl_easy_set_STREAM_DEPENDS_impl(L, CURLOPT_STREAM_DEPENDS); +} + +static int lcurl_easy_set_STREAM_DEPENDS_E(lua_State *L){ + return lcurl_easy_set_STREAM_DEPENDS_impl(L, CURLOPT_STREAM_DEPENDS_E); +} + +#endif + +#if LCURL_CURL_VER_GE(7,56,0) + +static int lcurl_easy_set_MIMEPOST(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + lcurl_mime_t *mime = lcurl_getmime_at(L, 2); + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_MIMEPOST, mime->mime); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_preserve_iv(L, p->storage, CURLOPT_MIMEPOST, 2); + + p->mime = mime; + + lua_settop(L, 1); + return 1; +} + +#endif + +#if LCURL_CURL_VER_GE(7,63,0) + +static int lcurl_easy_set_CURLU(lua_State *L) { + lcurl_easy_t *p = lcurl_geteasy(L); + lcurl_url_t *url = lcurl_geturl_at(L, 2); + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_CURLU, url->url); + if (code != CURLE_OK) { + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_preserve_iv(L, p->storage, CURLOPT_CURLU, 2); + + lua_settop(L, 1); + return 1; +} + +#endif + +//} + +//{ unset + +static int lcurl_opt_unset_long_(lua_State *L, int opt, long val){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code; + + code = curl_easy_setopt(p->curl, opt, val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + lua_settop(L, 1); + return 1; +} + +#if LCURL_CURL_VER_GE(7,59,0) + +static int lcurl_opt_unset_off_(lua_State *L, int opt, curl_off_t val){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code; + + code = curl_easy_setopt(p->curl, opt, val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + lua_settop(L, 1); + return 1; +} + +#endif + +static int lcurl_opt_unset_string_(lua_State *L, int opt, const char *val){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code; + + code = curl_easy_setopt(p->curl, opt, val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_remove_i(L, p->storage, opt); + + lua_settop(L, 1); + return 1; +} + +static int lcurl_opt_unset_slist_(lua_State *L, int opt, int list_no){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code; + int ref = p->lists[list_no]; + + code = curl_easy_setopt(p->curl, opt, NULL); + + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + if(ref != LUA_NOREF){ + struct curl_slist *list = lcurl_storage_remove_slist(L, p->storage, ref); + curl_slist_free_all(list); + p->lists[list_no] = LUA_NOREF; + } + + lua_settop(L, 1); + return 1; +} + +#if LCURL_CURL_VER_GE(7,73,0) + +static int lcurl_opt_unset_blob_(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code; + + code = curl_easy_setopt(p->curl, opt, 0); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lua_settop(L, 1); + return 1; +} + +#endif + +#define LCURL_STR_OPT(N, S, D) static int lcurl_easy_unset_##N(lua_State *L){\ + return lcurl_opt_unset_string_(L, CURLOPT_##N, (D)); \ +} + +#define LCURL_LST_OPT(N, S, D) static int lcurl_easy_unset_##N(lua_State *L){\ + return lcurl_opt_unset_slist_(L, CURLOPT_##N, LCURL_##N##_LIST);\ +} + +#define LCURL_LNG_OPT(N, S, D) static int lcurl_easy_unset_##N(lua_State *L){\ + return lcurl_opt_unset_long_(L, CURLOPT_##N, (D));\ +} + +#define LCURL_OFF_OPT(N, S, D) static int lcurl_easy_unset_##N(lua_State *L){\ + return lcurl_opt_unset_off_(L, CURLOPT_##N, (D));\ +} + +#define LCURL_BLB_OPT(N, S, D) static int lcurl_easy_unset_##N(lua_State *L){\ + return lcurl_opt_unset_blob_(L, CURLOPT_##N);\ +} + +#define OPT_ENTRY(L, N, T, S, D) LCURL_##T##_OPT(N, S, D) + +#include "lcopteasy.h" + +#undef OPT_ENTRY + +#undef LCURL_STR_OPT +#undef LCURL_LST_OPT +#undef LCURL_LNG_OPT +#undef LCURL_OFF_OPT +#undef LCURL_BLB_OPT + +static int lcurl_easy_unset_HTTPPOST(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_HTTPPOST, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_get_i(L, p->storage, CURLOPT_HTTPPOST); + if(!lua_isnil(L, -1)){ + lcurl_hpost_t *form = lcurl_gethpost_at(L, -1); + if(form->stream){ + /* with stream we do not set CURLOPT_READDATA but + we also unset it to be sure that there no way to + call default curl reader with our READDATA + */ + curl_easy_setopt(p->curl, CURLOPT_READFUNCTION, NULL); + curl_easy_setopt(p->curl, CURLOPT_READDATA, NULL); + } + lcurl_storage_remove_i(L, p->storage, CURLOPT_HTTPPOST); + } + + p->post = NULL; + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_SHARE(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_SHARE, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_remove_i(L, p->storage, CURLOPT_SHARE); + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_WRITEFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_WRITEFUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_WRITEDATA, NULL); + + luaL_unref(L, LCURL_LUA_REGISTRY, p->wr.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->wr.ud_ref); + p->wr.cb_ref = p->wr.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_READFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_READFUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_READDATA, NULL); + + luaL_unref(L, LCURL_LUA_REGISTRY, p->rd.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->rd.ud_ref); + p->rd.cb_ref = p->rd.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_HEADERFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_HEADERFUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_HEADERDATA, NULL); + + luaL_unref(L, LCURL_LUA_REGISTRY, p->hd.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->hd.ud_ref); + p->hd.cb_ref = p->hd.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_PROGRESSFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_PROGRESSFUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_PROGRESSDATA, NULL); + +#if LCURL_CURL_VER_GE(7,32,0) + curl_easy_setopt(p->curl, CURLOPT_XFERINFOFUNCTION, NULL); + curl_easy_setopt(p->curl, CURLOPT_XFERINFODATA, NULL); +#endif + + luaL_unref(L, LCURL_LUA_REGISTRY, p->pr.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->pr.ud_ref); + p->pr.cb_ref = p->pr.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_POSTFIELDS(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_POSTFIELDS, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + curl_easy_setopt(p->curl, CURLOPT_POSTFIELDSIZE, -1); + lcurl_storage_remove_i(L, p->storage, CURLOPT_POSTFIELDS); + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_SEEKFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_SEEKFUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_SEEKDATA, NULL); + + luaL_unref(L, LCURL_LUA_REGISTRY, p->seek.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->seek.ud_ref); + p->seek.cb_ref = p->seek.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_DEBUGFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_DEBUGFUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_DEBUGDATA, NULL); + + luaL_unref(L, LCURL_LUA_REGISTRY, p->debug.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->debug.ud_ref); + p->debug.cb_ref = p->debug.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +#if LCURL_CURL_VER_GE(7,19,6) + +static int lcurl_easy_unset_SSH_KEYFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_SSH_KEYFUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_SSH_KEYDATA, NULL); + + luaL_unref(L, LCURL_LUA_REGISTRY, p->ssh_key.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->ssh_key.ud_ref); + p->ssh_key.cb_ref = p->ssh_key.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +#endif + +#if LCURL_CURL_VER_GE(7,21,0) + +static int lcurl_easy_unset_FNMATCH_FUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_FNMATCH_FUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_FNMATCH_DATA, NULL); + + luaL_unref(L, LCURL_LUA_REGISTRY, p->match.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->match.ud_ref); + p->match.cb_ref = p->match.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_CHUNK_BGN_FUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_CHUNK_BGN_FUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + if(p->chunk_end.cb_ref == LUA_NOREF){ + // if other callback not set + curl_easy_setopt(p->curl, CURLOPT_CHUNK_DATA, NULL); + } + + luaL_unref(L, LCURL_LUA_REGISTRY, p->chunk_bgn.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->chunk_bgn.ud_ref); + p->chunk_bgn.cb_ref = p->chunk_bgn.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_CHUNK_END_FUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_CHUNK_END_FUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + if(p->chunk_bgn.cb_ref == LUA_NOREF){ + // if other callback not set + curl_easy_setopt(p->curl, CURLOPT_CHUNK_DATA, NULL); + } + + luaL_unref(L, LCURL_LUA_REGISTRY, p->chunk_end.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->chunk_end.ud_ref); + p->chunk_end.cb_ref = p->chunk_end.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +#endif + +#if LCURL_CURL_VER_GE(7,46,0) + +static int lcurl_easy_unset_STREAM_DEPENDS(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_STREAM_DEPENDS, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_remove_i(L, p->storage, CURLOPT_STREAM_DEPENDS); + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_STREAM_DEPENDS_E(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_STREAM_DEPENDS_E, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_remove_i(L, p->storage, CURLOPT_STREAM_DEPENDS_E); + + lua_settop(L, 1); + return 1; +} + +#endif + +#if LCURL_CURL_VER_GE(7,56,0) + +static int lcurl_easy_unset_MIMEPOST(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_MIMEPOST, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_remove_i(L, p->storage, CURLOPT_MIMEPOST); + + p->mime = NULL; + + lua_settop(L, 1); + return 1; +} + +#endif + +#if LCURL_CURL_VER_GE(7,63,0) + +static int lcurl_easy_unset_CURLU(lua_State *L) { + lcurl_easy_t *p = lcurl_geteasy(L); + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_CURLU, NULL); + if (code != CURLE_OK) { + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_storage_remove_i(L, p->storage, CURLOPT_CURLU); + + lua_settop(L, 1); + return 1; +} + +#endif + +#if LCURL_CURL_VER_GE(7,64,0) + +static int lcurl_easy_unset_TRAILERFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_TRAILERFUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_TRAILERDATA, NULL); + + luaL_unref(L, LCURL_LUA_REGISTRY, p->trailer.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->trailer.ud_ref); + p->trailer.cb_ref = p->trailer.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +#endif + +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + +static int lcurl_easy_unset_HSTSREADFUNCTION (lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_HSTSREADFUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_HSTSREADDATA, NULL); + + luaL_unref(L, LCURL_LUA_REGISTRY, p->hstsread.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->hstsread.ud_ref); + p->hstsread.cb_ref = p->hstsread.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_unset_HSTSWRITEFUNCTION (lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + + CURLcode code = curl_easy_setopt(p->curl, CURLOPT_HSTSWRITEFUNCTION, NULL); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, CURLOPT_HSTSWRITEDATA, NULL); + + luaL_unref(L, LCURL_LUA_REGISTRY, p->hstswrite.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->hstswrite.ud_ref); + p->hstswrite.cb_ref = p->hstswrite.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + return 1; +} + +#endif + +//} + +//} + +//{ info + +static int lcurl_info_get_long_(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + long val; CURLcode code; + + code = curl_easy_getinfo(p->curl, opt, &val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + +#if LUA_VERSION_NUM >= 503 /* Lua 5.3 */ + if(sizeof(lua_Integer) >= sizeof(val)) + lua_pushinteger(L, (lua_Integer)val); + else +#endif + lua_pushnumber(L, val); + + return 1; +} + +static int lcurl_info_get_double_(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + double val; CURLcode code; + + code = curl_easy_getinfo(p->curl, opt, &val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lua_pushnumber(L, val); + return 1; +} + +#if LCURL_CURL_VER_GE(7,55,0) + +static int lcurl_info_get_offset_(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + curl_off_t val; CURLcode code; + + code = curl_easy_getinfo(p->curl, opt, &val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lutil_pushint64(L, val); + return 1; +} + +#endif + +static int lcurl_info_get_string_(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + char *val; CURLcode code; + + code = curl_easy_getinfo(p->curl, opt, &val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lua_pushstring(L, val); + return 1; +} + +static int lcurl_info_get_slist_(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + struct curl_slist *val; CURLcode code; + + code = curl_easy_getinfo(p->curl, opt, &val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lcurl_util_slist_to_table(L, val); + curl_slist_free_all(val); + + return 1; +} + +static int lcurl_info_get_certinfo_(lua_State *L, int opt){ + lcurl_easy_t *p = lcurl_geteasy(L); + int decode = lua_toboolean(L, 2); + struct curl_certinfo * val; CURLcode code; + + code = curl_easy_getinfo(p->curl, opt, &val); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + + lua_newtable(L); + { int i = 0; for(;inum_of_certs; ++i){ + struct curl_slist *slist = val->certinfo[i]; + if (decode) { + lua_newtable(L); + for(;slist; slist = slist->next){ + const char *ptr = strchr(slist->data, ':'); + if(ptr){ + lua_pushlstring(L, slist->data, ptr - slist->data); + lua_pushstring(L, ptr + 1); + lua_rawset(L, -3); + } + } + } + else{ + lcurl_util_slist_to_table(L, slist); + } + lua_rawseti(L, -2, i + 1); + }} + + return 1; +} + +#define LCURL_STR_INFO(N, S) static int lcurl_easy_get_##N(lua_State *L){\ + return lcurl_info_get_string_(L, CURLINFO_##N); \ +} + +#define LCURL_LST_INFO(N, S) static int lcurl_easy_get_##N(lua_State *L){\ + return lcurl_info_get_slist_(L, CURLINFO_##N);\ +} + +#define LCURL_LNG_INFO(N, S) static int lcurl_easy_get_##N(lua_State *L){\ + return lcurl_info_get_long_(L, CURLINFO_##N);\ +} + +#define LCURL_DBL_INFO(N, S) static int lcurl_easy_get_##N(lua_State *L){\ + return lcurl_info_get_double_(L, CURLINFO_##N);\ +} + +#define LCURL_OFF_INFO(N, S) static int lcurl_easy_get_##N(lua_State *L){\ + return lcurl_info_get_offset_(L, CURLINFO_##N);\ +} + +#define LCURL_CERTINFO_INFO(N, S) static int lcurl_easy_get_##N(lua_State *L){\ + return lcurl_info_get_certinfo_(L, CURLINFO_##N);\ +} + +#define OPT_ENTRY(L, N, T, S) LCURL_##T##_INFO(N, S) + +#include "lcinfoeasy.h" + +#undef OPT_ENTRY + +#undef LCURL_STR_INFO +#undef LCURL_LST_INFO +#undef LCURL_LNG_INFO +#undef LCURL_DBL_INFO + +//} + +//{ CallBack + +static int lcurl_easy_set_callback(lua_State *L, + lcurl_easy_t *p, lcurl_callback_t *c, + int OPT_CB, int OPT_UD, + const char *method, void *func +) +{ + CURLcode code; + lcurl_set_callback(L, c, 2, method); + + code = curl_easy_setopt(p->curl, OPT_CB, (c->cb_ref == LUA_NOREF)?0:func); + if((code != CURLE_OK)&&(c->cb_ref != LUA_NOREF)){ + luaL_unref(L, LCURL_LUA_REGISTRY, c->cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, c->ud_ref); + c->cb_ref = c->ud_ref = LUA_NOREF; + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + curl_easy_setopt(p->curl, OPT_UD, (c->cb_ref == LUA_NOREF)?0:p); + + return 1; +} + +static size_t lcurl_write_callback_(lua_State*L, + lcurl_easy_t *p, lcurl_callback_t *c, + char *ptr, size_t size, size_t nmemb +){ + size_t ret = size * nmemb; + int top = lua_gettop(L); + int n = lcurl_util_push_cb(L, c); + + lua_pushlstring(L, ptr, ret); + if(lua_pcall(L, n, LUA_MULTRET, 0)){ + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top+1); + return 0; + } + + if(lua_gettop(L) > top){ + if(lua_isnil(L, top + 1)){ + if(lua_gettop(L) == (top+1)) lua_settop(L, top); + return 0; + } + if(lua_isnumber(L, top + 1)){ + ret = (size_t)lua_tonumber(L, top + 1); + } + else{ + if(!lua_toboolean(L, top + 1)) ret = 0; + } + } + + lua_settop(L, top); + return ret; +} + +//{ Writer + +static size_t lcurl_write_callback(char *ptr, size_t size, size_t nmemb, void *arg){ + lcurl_easy_t *p = arg; + assert(NULL != p->L); + return lcurl_write_callback_(p->L, p, &p->wr, ptr, size, nmemb); +} + +static int lcurl_easy_set_WRITEFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->wr, + CURLOPT_WRITEFUNCTION, CURLOPT_WRITEDATA, + "write", lcurl_write_callback + ); +} + +//} + +//{ Reader + +size_t lcurl_read_callback(lua_State *L, + lcurl_callback_t *rd, lcurl_read_buffer_t *rbuffer, + char *buffer, size_t size, size_t nitems +){ + const char *data; size_t data_size; + + size_t ret = size * nitems; + int n, top = lua_gettop(L); + + if(rbuffer->ref != LUA_NOREF){ + lua_rawgeti(L, LCURL_LUA_REGISTRY, rbuffer->ref); + data = luaL_checklstring(L, -1, &data_size); + lua_pop(L, 1); + + data = data + rbuffer->off; + data_size -= rbuffer->off; + + if(data_size > ret){ + data_size = ret; + memcpy(buffer, data, data_size); + rbuffer->off += data_size; + } + else{ + memcpy(buffer, data, data_size); + luaL_unref(L, LCURL_LUA_REGISTRY, rbuffer->ref); + rbuffer->ref = LUA_NOREF; + } + + lua_settop(L, top); + return data_size; + } + + // buffer is clean + assert(rbuffer->ref == LUA_NOREF); + + n = lcurl_util_push_cb(L, rd); + lua_pushinteger(L, ret); + if(lua_pcall(L, n, LUA_MULTRET, 0)){ + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top+1); + return CURL_READFUNC_ABORT; + } + + if(lua_gettop(L) == top){ + return 0; + } + + assert(lua_gettop(L) >= top); + + if(lua_type(L, top + 1) != LUA_TSTRING){ + if(lua_isnil(L, top + 1)){ + if(lua_gettop(L) == (top+1)){// only nil -> EOF + lua_settop(L, top); + return 0; + } + } + else{ + if(lua_type(L, top + 1) == LUA_TNUMBER){ + size_t ret = lua_tointeger(L, top + 1); + if(ret == (size_t)CURL_READFUNC_PAUSE){ + lua_settop(L, top); + return CURL_READFUNC_PAUSE; + } + } + lua_settop(L, top); + } + return CURL_READFUNC_ABORT; + } + + data = lua_tolstring(L, top + 1, &data_size); + assert(data); + if(data_size > ret){ + data_size = ret; + rbuffer->ref = luaL_ref(L, LCURL_LUA_REGISTRY); + rbuffer->off = data_size; + } + memcpy(buffer, data, data_size); + + lua_settop(L, top); + return data_size; +} + +static size_t lcurl_easy_read_callback(char *buffer, size_t size, size_t nitems, void *arg){ + lcurl_easy_t *p = arg; + if(p->magic == LCURL_HPOST_STREAM_MAGIC){ + return lcurl_hpost_read_callback(buffer, size, nitems, arg); + } + assert(NULL != p->L); + return lcurl_read_callback(p->L, &p->rd, &p->rbuffer, buffer, size, nitems); +} + +static size_t lcurl_hpost_read_callback(char *buffer, size_t size, size_t nitems, void *arg){ + lcurl_hpost_stream_t *p = arg; + assert(NULL != p->L); + return lcurl_read_callback(*p->L, &p->rd, &p->rbuffer, buffer, size, nitems); +} + +static int lcurl_easy_set_READFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->rd, + CURLOPT_READFUNCTION, CURLOPT_READDATA, + "read", lcurl_easy_read_callback + ); +} + +//} + +//{ Header + +static size_t lcurl_header_callback(char *ptr, size_t size, size_t nmemb, void *arg){ + lcurl_easy_t *p = arg; + assert(NULL != p->L); + return lcurl_write_callback_(p->L, p, &p->hd, ptr, size, nmemb); +} + +static int lcurl_easy_set_HEADERFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->hd, + CURLOPT_HEADERFUNCTION, CURLOPT_HEADERDATA, + "header", lcurl_header_callback + ); +} + +//} + +//{ Progress + +static int lcurl_xferinfo_callback(void *arg, curl_off_t dltotal, curl_off_t dlnow, + curl_off_t ultotal, curl_off_t ulnow) +{ + lcurl_easy_t *p = arg; + lua_State *L = p->L; + int n, top, ret = 0; + + assert(NULL != p->L); + + top = lua_gettop(L); + n = lcurl_util_push_cb(L, &p->pr); + + lua_pushnumber( L, (lua_Number)dltotal ); + lua_pushnumber( L, (lua_Number)dlnow ); + lua_pushnumber( L, (lua_Number)ultotal ); + lua_pushnumber( L, (lua_Number)ulnow ); + + if(lua_pcall(L, n+3, LUA_MULTRET, 0)){ + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top+1); + return 1; + } + + if(lua_gettop(L) > top){ + if(lua_isnil(L, top + 1)){ + if(lua_gettop(L) == (top+1)) lua_settop(L, top); + return 1; + } + if(lua_isboolean(L, top + 1)) + ret = lua_toboolean(L, top + 1)?0:1; + else{ + ret = lua_tonumber(L, top + 1); + #if LCURL_CURL_VER_GE(7,68,0) + if(ret != (size_t)CURL_PROGRESSFUNC_CONTINUE) + #endif + if(ret == 0) ret = 1; else ret = 0; + } + } + + lua_settop(L, top); + return ret; +} + +static int lcurl_progress_callback(void *arg, double dltotal, double dlnow, + double ultotal, double ulnow) +{ + return lcurl_xferinfo_callback(arg, + (curl_off_t)dltotal, + (curl_off_t)dlnow, + (curl_off_t)ultotal, + (curl_off_t)ulnow + ); +} + +static int lcurl_easy_set_PROGRESSFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + int n = lcurl_easy_set_callback(L, p, &p->pr, + CURLOPT_PROGRESSFUNCTION, CURLOPT_PROGRESSDATA, + "progress", lcurl_progress_callback + ); + +#if LCURL_CURL_VER_GE(7,32,0) + if(p->pr.cb_ref != LUA_NOREF){ + curl_easy_setopt(p->curl, CURLOPT_XFERINFOFUNCTION, lcurl_xferinfo_callback); + curl_easy_setopt(p->curl, CURLOPT_XFERINFODATA, p); + } +#endif + + return n; +} + +//} + +//{ Seek + +static int lcurl_seek_callback(void *arg, curl_off_t offset, int origin){ + lcurl_easy_t *p = arg; + lua_State *L = p->L; + int ret = CURL_SEEKFUNC_OK; + int top = lua_gettop(L); + int n = lcurl_util_push_cb(L, &p->seek); + + assert(NULL != p->L); + + if (SEEK_SET == origin) lua_pushliteral(L, "set"); + else if (SEEK_CUR == origin) lua_pushliteral(L, "cur"); + else if (SEEK_END == origin) lua_pushliteral(L, "end"); + else lua_pushinteger(L, origin); + lutil_pushint64(L, offset); + + if (lua_pcall(L, n+1, LUA_MULTRET, 0)) { + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURL_SEEKFUNC_FAIL; + } + + if(lua_gettop(L) > top){ + if(lua_isnil(L, top + 1) && (!lua_isnoneornil(L, top + 2))){ + lua_settop(L, top + 2); + lua_remove(L, top + 1); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURL_SEEKFUNC_FAIL; + } + ret = lua_toboolean(L, top + 1) ? CURL_SEEKFUNC_OK : CURL_SEEKFUNC_CANTSEEK; + } + + lua_settop(L, top); + return ret; +} + +static int lcurl_easy_set_SEEKFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->seek, + CURLOPT_SEEKFUNCTION, CURLOPT_SEEKDATA, + "seek", lcurl_seek_callback + ); +} + +//} + +//{ Debug + +static int lcurl_debug_callback(CURL *handle, curl_infotype type, char *data, size_t size, void *arg){ + lcurl_easy_t *p = arg; + lua_State *L = p->L; + int top = lua_gettop(L); + int n = lcurl_util_push_cb(L, &p->debug); + + assert(NULL != p->L); + assert(handle == p->curl); + + lua_pushinteger(L, type); + lua_pushlstring(L, data, size); + + // just ignore all errors from Lua callback + lua_pcall(L, n + 1, LUA_MULTRET, 0); + lua_settop(L, top); + + return 0; +} + +static int lcurl_easy_set_DEBUGFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->debug, + CURLOPT_DEBUGFUNCTION, CURLOPT_DEBUGDATA, + "debug", lcurl_debug_callback + ); +} + +//} + +//{ Match + +#if LCURL_CURL_VER_GE(7,21,0) + +static int lcurl_match_callback(void *arg, const char *pattern, const char *string) { + lcurl_easy_t *p = arg; + lua_State *L = p->L; + int ret = CURL_FNMATCHFUNC_NOMATCH; + int top = lua_gettop(L); + int n = lcurl_util_push_cb(L, &p->match); + + assert(NULL != p->L); + + lua_pushstring(L, pattern); + lua_pushstring(L, string); + + if (lua_pcall(L, n+1, LUA_MULTRET, 0)) { + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURL_FNMATCHFUNC_FAIL; + } + + if(lua_gettop(L) > top){ + if(lua_isnil(L, top + 1) && (!lua_isnoneornil(L, top + 2))){ + lua_settop(L, top + 2); + lua_remove(L, top + 1); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURL_FNMATCHFUNC_FAIL; + } + ret = lua_toboolean(L, top + 1) ? CURL_FNMATCHFUNC_MATCH : CURL_FNMATCHFUNC_NOMATCH; + } + + lua_settop(L, top); + return ret; +} + +static int lcurl_easy_set_FNMATCH_FUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->match, + CURLOPT_FNMATCH_FUNCTION, CURLOPT_FNMATCH_DATA, + "match", lcurl_match_callback + ); +} + +#endif + +//} + +//{ Chunk begin/end + +#if LCURL_CURL_VER_GE(7,21,0) + +static int lcurl_chunk_bgn_callback(struct curl_fileinfo *info, void *arg, int remains) { + lcurl_easy_t *p = arg; + lua_State *L = p->L; + int ret = CURL_CHUNK_BGN_FUNC_OK; + int top = lua_gettop(L); + int n = lcurl_util_push_cb(L, &p->chunk_bgn); + + assert(NULL != p->L); + + lua_newtable(L); + lua_pushstring (L, info->filename ); lua_setfield(L, -2, "filename" ); + lua_pushinteger(L, info->filetype ); lua_setfield(L, -2, "filetype" ); + lutil_pushint64(L, info->time ); lua_setfield(L, -2, "time" ); + lutil_pushint64(L, info->perm ); lua_setfield(L, -2, "perm" ); + lua_pushinteger(L, info->uid ); lua_setfield(L, -2, "uid" ); + lua_pushinteger(L, info->gid ); lua_setfield(L, -2, "gid" ); + lutil_pushint64(L, info->size ); lua_setfield(L, -2, "size" ); + lutil_pushint64(L, info->hardlinks ); lua_setfield(L, -2, "hardlinks" ); + lutil_pushint64(L, info->flags ); lua_setfield(L, -2, "flags" ); + + lua_newtable(L); + if(info->strings.time) { lua_pushstring (L, info->strings.time ); lua_setfield(L, -2, "time" ); } + if(info->strings.perm) { lua_pushstring (L, info->strings.perm ); lua_setfield(L, -2, "perm" ); } + if(info->strings.user) { lua_pushstring (L, info->strings.user ); lua_setfield(L, -2, "user" ); } + if(info->strings.group) { lua_pushstring (L, info->strings.group ); lua_setfield(L, -2, "group" ); } + if(info->strings.target){ lua_pushstring (L, info->strings.target); lua_setfield(L, -2, "target"); } + lua_setfield(L, -2, "strings"); + + lua_pushinteger(L, remains); + + if (lua_pcall(L, n+1, LUA_MULTRET, 0)) { + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURL_CHUNK_BGN_FUNC_FAIL; + } + + if(lua_gettop(L) > top){ + if(lua_isnil(L, top + 1) && (!lua_isnoneornil(L, top + 2))){ + lua_settop(L, top + 2); + lua_remove(L, top + 1); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURL_CHUNK_BGN_FUNC_FAIL; + } + ret = lua_toboolean(L, top + 1) ? CURL_CHUNK_BGN_FUNC_OK : CURL_CHUNK_BGN_FUNC_SKIP; + } + + lua_settop(L, top); + return ret; +} + +static int lcurl_chunk_end_callback(void *arg) { + lcurl_easy_t *p = arg; + lua_State *L = p->L; + int ret = CURL_CHUNK_END_FUNC_OK; + int top = lua_gettop(L); + int n = lcurl_util_push_cb(L, &p->chunk_end); + + assert(NULL != p->L); + + if (lua_pcall(L, n-1, LUA_MULTRET, 0)) { + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURL_CHUNK_END_FUNC_FAIL; + } + + if(lua_gettop(L) > top){ + if(lua_isnil(L, top + 1) && (!lua_isnoneornil(L, top + 2))){ + lua_settop(L, top + 2); + lua_remove(L, top + 1); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURL_CHUNK_END_FUNC_FAIL; + } + ret = lua_toboolean(L, top + 1) ? CURL_CHUNK_END_FUNC_OK : CURL_CHUNK_END_FUNC_FAIL; + } + + lua_settop(L, top); + return ret; +} + +static int lcurl_easy_set_CHUNK_BGN_FUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->chunk_bgn, + CURLOPT_CHUNK_BGN_FUNCTION, CURLOPT_CHUNK_DATA, + "chunk_bgn", lcurl_chunk_bgn_callback + ); +} + +static int lcurl_easy_set_CHUNK_END_FUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->chunk_end, + CURLOPT_CHUNK_END_FUNCTION, CURLOPT_CHUNK_DATA, + "chunk_end", lcurl_chunk_end_callback + ); +} + +#endif + +//} + +//{ Trailer + +#if LCURL_CURL_VER_GE(7,64,0) + +static int lcurl_trailer_callback(struct curl_slist **list, void *arg) { + lcurl_easy_t *p = arg; + lua_State *L = p->L; + int top = lua_gettop(L); + int n = lcurl_util_push_cb(L, &p->trailer); + + if (lua_pcall(L, n - 1, LUA_MULTRET, 0)) { + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURL_TRAILERFUNC_ABORT; + } + + n = lua_gettop(L); + + if (n == top) { + return CURL_TRAILERFUNC_OK; + } + + /* libcurl will free the list */ + *list = lcurl_util_to_slist(L, top + 1); + if (*list) { + lua_settop(L, top); + return CURL_TRAILERFUNC_OK; + } + + // empty array or NULL + if (lua_istable(L, top + 1) || lutil_is_null(L, top + 1)) { + lua_settop(L, top); + return CURL_TRAILERFUNC_OK; + } + + // true + if((lua_type(L, top + 1) == LUA_TBOOLEAN) && (lua_toboolean(L, top + 1))){ + lua_settop(L, top); + return CURL_TRAILERFUNC_OK; + } + + // single nil + if((n == (top + 1)) && lua_isnil(L, top + 1)){ + lua_settop(L, top); + return CURL_TRAILERFUNC_OK; + } + + lua_settop(L, top); + return CURL_TRAILERFUNC_ABORT; +} + +static int lcurl_easy_set_TRAILERFUNCTION (lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->trailer, + CURLOPT_TRAILERFUNCTION, CURLOPT_TRAILERDATA, + "trailer", lcurl_trailer_callback + ); +} + +#endif + +//} + +//{ HSTS Reader + +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + +#define LCURL_HSTS_EXPIRE_LEN 18 + +static int lcurl_hstsread_callback(CURL *easy, struct curl_hstsentry *sts, void *arg) { + lcurl_easy_t *p = arg; + lua_State *L = p->L; + int top = lua_gettop(L); + int n = lcurl_util_push_cb(L, &p->hstsread); + const char *name; size_t namelen; + const char *expire; size_t expirelen; + int type; + + assert(NULL != p->L); + + lua_pushinteger(L, sts->namelen); + if (lua_pcall(L, n, LUA_MULTRET, 0)) { + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURLSTS_FAIL; + } + + if (lua_gettop(L) == top) { + return CURLSTS_DONE; + } + + assert(lua_gettop(L) >= top); + + type = lua_type(L, top + 1); + if (type == LUA_TNIL) { + lua_settop(L, top); + return CURLSTS_DONE; + } + + if (type != LUA_TSTRING) { + lua_settop(L, top); + return CURLSTS_FAIL; + } + + name = lua_tolstring(L, top + 1, &namelen); + + if(namelen > sts->namelen) { + lua_settop(L, top); + return CURLSTS_FAIL; + } + + memcpy(sts->name, name, namelen + 1); + + type = lua_type(L, top + 2); + if (type == LUA_TNONE) { + lua_settop(L, top); + return CURLSTS_OK; + } + + if ((type != LUA_TBOOLEAN) && (type != LUA_TNIL)) { + lua_settop(L, top); + return CURLSTS_FAIL; + } + + if (type == LUA_TBOOLEAN) { + sts->includeSubDomains = lua_toboolean(L, top + 2) ? 0 : 1; + } + else if (type != LUA_TNIL) { + lua_settop(L, top); + return CURLSTS_FAIL; + } + + type = lua_type(L, top + 3); + if ((type == LUA_TNONE) || (type == LUA_TNIL)) { + lua_settop(L, top); + return CURLSTS_OK; + } + + if(type != LUA_TSTRING) { + lua_settop(L, top); + return CURLSTS_FAIL; + } + + expire = lua_tolstring(L, top + 3, &expirelen); + + if (expirelen != LCURL_HSTS_EXPIRE_LEN - 1) { + lua_settop(L, top); + return CURLSTS_FAIL; + } + + memcpy(sts->expire, expire, expirelen + 1); + + lua_settop(L, top); + return CURLSTS_OK; +} + +static int lcurl_easy_set_HSTSREADFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->hstsread, + CURLOPT_HSTSREADFUNCTION, CURLOPT_HSTSREADDATA, + "hstsread", lcurl_hstsread_callback + ); +} + +#endif + +//} + +//{ HSTS Writer + +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + +static int lcurl_hstswrite_callback(CURL *easy, struct curl_hstsentry *sts, struct curl_index *count, void *arg) { + lcurl_easy_t *p = arg; + lua_State *L = p->L; + int top = lua_gettop(L); + int n = lcurl_util_push_cb(L, &p->hstswrite); + int type; + + assert(NULL != p->L); + + lua_pushstring(L, sts->name); + lua_pushboolean(L, sts->includeSubDomains ? 1 : 0); + if (sts->expire[0]) { + lua_pushstring(L, sts->expire); + } else { + lua_pushnil(L); + } + lua_pushinteger(L, count->index); + lua_pushinteger(L, count->total); + + if (lua_pcall(L, n + 4, LUA_MULTRET, 0)) { + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURLSTS_FAIL; + } + + if (lua_gettop(L) == top) { + return CURLSTS_OK; + } + + assert(lua_gettop(L) >= top); + + type = lua_type(L, top + 1); + if (type == LUA_TNIL) { + type = lua_type(L, top + 2); + lua_settop(L, top); + if(type == LUA_TNONE){ + return CURLSTS_OK; + } + return CURLSTS_FAIL; + } + + if (type == LUA_TNUMBER) { + int ret = lua_tointeger(L, top + 1); + lua_settop(L, top); + return ret; + } + + if (type == LUA_TBOOLEAN) { + int ret = lua_toboolean(L, top + 1); + lua_settop(L, top); + return ret ? CURLSTS_OK : CURLSTS_DONE; + } + + lua_settop(L, top); + return CURLSTS_FAIL; +} + +static int lcurl_easy_set_HSTSWRITEFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->hstswrite, + CURLOPT_HSTSWRITEFUNCTION, CURLOPT_HSTSWRITEDATA, + "hstswrite", lcurl_hstswrite_callback + ); +} + +#endif + +//} + +//{ SSH key + +#if LCURL_CURL_VER_GE(7,19,6) + +static void lcurl_ssh_key_push(lua_State *L, const struct curl_khkey *key){ + if (!key) { + lua_pushnil(L); + return; + } + + lua_newtable(L); + + if(key->len){ + lua_pushliteral(L, "raw"); + lua_pushlstring(L, key->key, key->len); + } else { + lua_pushliteral(L, "base64"); + lua_pushstring(L, key->key); + } + lua_rawset(L, -3); + + lua_pushliteral(L, "type"); + lutil_pushuint(L, key->keytype); + lua_rawset(L, -3); +} + +static int lcurl_ssh_key_callback( + CURL *easy, + const struct curl_khkey *knownkey, + const struct curl_khkey *foundkey, + enum curl_khmatch khmatch, + void *arg +) { + lcurl_easy_t *p = arg; + lua_State *L = p->L; + int top = lua_gettop(L); + int n = lcurl_util_push_cb(L, &p->ssh_key); + + assert(NULL != p->L); + + lcurl_ssh_key_push(L, knownkey); + lcurl_ssh_key_push(L, foundkey); + lutil_pushuint(L, khmatch); + + if (lua_pcall(L, n + 2, LUA_MULTRET, 0)) { + assert(lua_gettop(L) >= top); + lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + lua_insert(L, top + 1); + return CURLKHSTAT_REJECT; + } + + if (lua_gettop(L) > top) { + int ret = lua_tointeger(L, top + 1); + lua_settop(L, top); + + switch (ret) +#if LCURL_CURL_VER_GE(7,73,0) + case CURLKHSTAT_FINE_REPLACE: +#endif + case CURLKHSTAT_FINE_ADD_TO_FILE: + case CURLKHSTAT_FINE: + case CURLKHSTAT_REJECT: + case CURLKHSTAT_DEFER: + return ret; + } + + return CURLKHSTAT_REJECT; +} + +static int lcurl_easy_set_SSH_KEYFUNCTION(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + return lcurl_easy_set_callback(L, p, &p->ssh_key, + CURLOPT_SSH_KEYFUNCTION, CURLOPT_SSH_KEYDATA, + "ssh_key", lcurl_ssh_key_callback + ); +} + +#endif + +//} + +static int lcurl_easy_setopt(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + long opt; + + luaL_checkany(L, 2); + if(lua_type(L, 2) == LUA_TTABLE){ + int ret = lcurl_utils_apply_options(L, 2, 1, 0, p->err_mode, LCURL_ERROR_EASY, LCURL_E_UNKNOWN_OPTION); + if(ret) return ret; + lua_settop(L, 1); + return 1; + } + + opt = luaL_checklong(L, 2); + lua_remove(L, 2); + +#define OPT_ENTRY(l, N, T, S, D) case CURLOPT_##N: return lcurl_easy_set_##N(L); + switch(opt){ + #include "lcopteasy.h" + OPT_ENTRY(postfields, POSTFIELDS, TTT, 0, 0) + OPT_ENTRY(httppost, HTTPPOST, TTT, 0, 0) + OPT_ENTRY(share, SHARE, TTT, 0, 0) + OPT_ENTRY(writefunction, WRITEFUNCTION, TTT, 0, 0) + OPT_ENTRY(readfunction, READFUNCTION, TTT, 0, 0) + OPT_ENTRY(headerfunction, HEADERFUNCTION, TTT, 0, 0) + OPT_ENTRY(progressfunction, PROGRESSFUNCTION, TTT, 0, 0) + OPT_ENTRY(seekfunction, SEEKFUNCTION, TTT, 0, 0) + OPT_ENTRY(debugfunction, DEBUGFUNCTION, TTT, 0, 0) +#if LCURL_CURL_VER_GE(7,19,6) + OPT_ENTRY(ssh_keyfunction, SSH_KEYFUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,21,0) + OPT_ENTRY(fnmatch_function, FNMATCH_FUNCTION, TTT, 0, 0) + OPT_ENTRY(chunk_bgn_function, CHUNK_BGN_FUNCTION, TTT, 0, 0) + OPT_ENTRY(chunk_end_function, CHUNK_END_FUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,46,0) + OPT_ENTRY(stream_depends, STREAM_DEPENDS, TTT, 0, 0) + OPT_ENTRY(stream_depends_e, STREAM_DEPENDS_E, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,56,0) + OPT_ENTRY(mimepost, MIMEPOST, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,63,0) + OPT_ENTRY(curlu, CURLU, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,64,0) + OPT_ENTRY(trailerfunction, TRAILERFUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + OPT_ENTRY(hstsreadfunction, HSTSREADFUNCTION, TTT, 0, 0) + OPT_ENTRY(hstswritefunction, HSTSWRITEFUNCTION,TTT, 0, 0) +#endif + } +#undef OPT_ENTRY + + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, LCURL_E_UNKNOWN_OPTION); +} + +static int lcurl_easy_unsetopt(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + long opt; + + opt = luaL_checklong(L, 2); + lua_remove(L, 2); + +#define OPT_ENTRY(l, N, T, S, D) case CURLOPT_##N: return lcurl_easy_unset_##N(L); + switch(opt){ + #include "lcopteasy.h" + OPT_ENTRY(postfields, POSTFIELDS, TTT, 0, 0) + OPT_ENTRY(httppost, HTTPPOST, TTT, 0, 0) + OPT_ENTRY(share, SHARE, TTT, 0, 0) + OPT_ENTRY(writefunction, WRITEFUNCTION, TTT, 0, 0) + OPT_ENTRY(readfunction, READFUNCTION, TTT, 0, 0) + OPT_ENTRY(headerfunction, HEADERFUNCTION, TTT, 0, 0) + OPT_ENTRY(progressfunction, PROGRESSFUNCTION, TTT, 0, 0) + OPT_ENTRY(seekfunction, SEEKFUNCTION, TTT, 0, 0) + OPT_ENTRY(debugfunction, DEBUGFUNCTION, TTT, 0, 0) +#if LCURL_CURL_VER_GE(7,19,6) + OPT_ENTRY(ssh_keyfunction, SSH_KEYFUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,21,0) + OPT_ENTRY(fnmatch_function, FNMATCH_FUNCTION, TTT, 0, 0) + OPT_ENTRY(chunk_bgn_function, CHUNK_BGN_FUNCTION, TTT, 0, 0) + OPT_ENTRY(chunk_end_function, CHUNK_END_FUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,46,0) + OPT_ENTRY(stream_depends, STREAM_DEPENDS, TTT, 0, 0) + OPT_ENTRY(stream_depends_e, STREAM_DEPENDS_E, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,56,0) + OPT_ENTRY(mimepost, MIMEPOST, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,63,0) + OPT_ENTRY(curlu, CURLU, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,64,0) + OPT_ENTRY(trailerfunction, TRAILERFUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + OPT_ENTRY(hstsreadfunction, HSTSREADFUNCTION, TTT, 0, 0) + OPT_ENTRY(hstswritefunction, HSTSWRITEFUNCTION,TTT, 0, 0) +#endif + } +#undef OPT_ENTRY + + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, LCURL_E_UNKNOWN_OPTION); +} + +static int lcurl_easy_getinfo(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + long opt = luaL_checklong(L, 2); + lua_remove(L, 2); + +#define OPT_ENTRY(l, N, T, S) case CURLINFO_##N: return lcurl_easy_get_##N(L); + switch(opt){ + #include "lcinfoeasy.h" + } +#undef OPT_ENTRY + + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, LCURL_E_UNKNOWN_OPTION); +} + +static int lcurl_easy_pause(lua_State *L){ + lcurl_easy_t *p = lcurl_geteasy(L); + lua_State *curL; + int mask = luaL_checkint(L, 2); + CURLcode code; + + curL = p->L; lcurl__easy_assign_lua(L, p, L, 1); + code = curl_easy_pause(p->curl, mask); +#ifndef LCURL_RESET_NULL_LUA + if(curL != NULL) +#endif + lcurl__easy_assign_lua(L, p, curL, 1); + + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, code); + } + lua_settop(L, 1); + return 1; +} + +static int lcurl_easy_setdata(lua_State *L){ + lua_settop(L, 2); + lua_pushvalue(L, 1); + lua_insert(L, 2); + lua_rawset(L, LCURL_USERVALUES); + return 1; +} + +static int lcurl_easy_getdata(lua_State *L){ + lua_settop(L, 1); + lua_rawget(L, LCURL_USERVALUES); + return 1; +} + +//} + +static const struct luaL_Reg lcurl_easy_methods[] = { + +#define OPT_ENTRY(L, N, T, S, D) { "setopt_"#L, lcurl_easy_set_##N }, + #include "lcopteasy.h" + OPT_ENTRY(postfields, POSTFIELDS, TTT, 0, 0) + OPT_ENTRY(httppost, HTTPPOST, TTT, 0, 0) + OPT_ENTRY(share, SHARE, TTT, 0, 0) + OPT_ENTRY(writefunction, WRITEFUNCTION, TTT, 0, 0) + OPT_ENTRY(readfunction, READFUNCTION, TTT, 0, 0) + OPT_ENTRY(headerfunction, HEADERFUNCTION, TTT, 0, 0) + OPT_ENTRY(progressfunction, PROGRESSFUNCTION, TTT, 0, 0) + OPT_ENTRY(seekfunction, SEEKFUNCTION, TTT, 0, 0) + OPT_ENTRY(debugfunction, DEBUGFUNCTION, TTT, 0, 0) +#if LCURL_CURL_VER_GE(7,19,6) + OPT_ENTRY(ssh_keyfunction, SSH_KEYFUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,21,0) + OPT_ENTRY(fnmatch_function, FNMATCH_FUNCTION, TTT, 0, 0) + OPT_ENTRY(chunk_bgn_function, CHUNK_BGN_FUNCTION, TTT, 0, 0) + OPT_ENTRY(chunk_end_function, CHUNK_END_FUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,46,0) + OPT_ENTRY(stream_depends, STREAM_DEPENDS, TTT, 0, 0) + OPT_ENTRY(stream_depends_e, STREAM_DEPENDS_E, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,56,0) + OPT_ENTRY(mimepost, MIMEPOST, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,63,0) + OPT_ENTRY(curlu, CURLU, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,64,0) + OPT_ENTRY(trailerfunction, TRAILERFUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + OPT_ENTRY(hstsreadfunction, HSTSREADFUNCTION, TTT, 0, 0) + OPT_ENTRY(hstswritefunction, HSTSWRITEFUNCTION,TTT, 0, 0) +#endif +#undef OPT_ENTRY + +#define OPT_ENTRY(L, N, T, S, D) { "unsetopt_"#L, lcurl_easy_unset_##N }, + #include "lcopteasy.h" + OPT_ENTRY(postfields, POSTFIELDS, TTT, 0, 0) + OPT_ENTRY(httppost, HTTPPOST, TTT, 0, 0) + OPT_ENTRY(share, SHARE, TTT, 0, 0) + OPT_ENTRY(writefunction, WRITEFUNCTION, TTT, 0, 0) + OPT_ENTRY(readfunction, READFUNCTION, TTT, 0, 0) + OPT_ENTRY(headerfunction, HEADERFUNCTION, TTT, 0, 0) + OPT_ENTRY(progressfunction, PROGRESSFUNCTION, TTT, 0, 0) + OPT_ENTRY(seekfunction, SEEKFUNCTION, TTT, 0, 0) + OPT_ENTRY(debugfunction, DEBUGFUNCTION, TTT, 0, 0) +#if LCURL_CURL_VER_GE(7,19,6) + OPT_ENTRY(ssh_keyfunction, SSH_KEYFUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,21,0) + OPT_ENTRY(fnmatch_function, FNMATCH_FUNCTION, TTT, 0, 0) + OPT_ENTRY(chunk_bgn_function, CHUNK_BGN_FUNCTION, TTT, 0, 0) + OPT_ENTRY(chunk_end_function, CHUNK_END_FUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,46,0) + OPT_ENTRY(stream_depends, STREAM_DEPENDS, TTT, 0, 0) + OPT_ENTRY(stream_depends_e, STREAM_DEPENDS_E, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,56,0) + OPT_ENTRY(mimepost, MIMEPOST, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,63,0) + OPT_ENTRY(curlu, CURLU, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,64,0) + OPT_ENTRY(trailerfunction, TRAILERFUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + OPT_ENTRY(hstsreadfunction, HSTSREADFUNCTION, TTT, 0, 0) + OPT_ENTRY(hstswritefunction, HSTSWRITEFUNCTION,TTT, 0, 0) +#endif +#undef OPT_ENTRY + +#define OPT_ENTRY(L, N, T, S) { "getinfo_"#L, lcurl_easy_get_##N }, + #include "lcinfoeasy.h" +#undef OPT_ENTRY + +#if LCURL_CURL_VER_GE(7,56,0) + { "mime", lcurl_easy_mime }, +#endif + + { "pause", lcurl_easy_pause }, + { "reset", lcurl_easy_reset }, + { "setopt", lcurl_easy_setopt }, + { "getinfo", lcurl_easy_getinfo }, + { "unsetopt", lcurl_easy_unsetopt }, + { "escape", lcurl_easy_escape }, + { "unescape", lcurl_easy_unescape }, + { "perform", lcurl_easy_perform }, +#if LCURL_CURL_VER_GE(7,62,0) + { "upkeep", lcurl_easy_upkeep }, +#endif + { "close", lcurl_easy_cleanup }, + { "__gc", lcurl_easy_cleanup }, + { "__tostring", lcurl_easy_to_s }, + + { "setdata", lcurl_easy_setdata }, + { "getdata", lcurl_easy_getdata }, + + {NULL,NULL} +}; + +static const lcurl_const_t lcurl_easy_opt[] = { + +#define OPT_ENTRY(L, N, T, S, D) { "OPT_"#N, CURLOPT_##N }, +#define FLG_ENTRY(N) { #N, CURL_##N }, +#include "lcopteasy.h" + OPT_ENTRY(postfields, POSTFIELDS, TTT, 0, 0) + OPT_ENTRY(httppost, HTTPPOST, TTT, 0, 0) + OPT_ENTRY(share, SHARE, TTT, 0, 0) + OPT_ENTRY(writefunction, WRITEFUNCTION, TTT, 0, 0) + OPT_ENTRY(readfunction, READFUNCTION, TTT, 0, 0) + OPT_ENTRY(headerfunction, HEADERFUNCTION, TTT, 0, 0) + OPT_ENTRY(progressfunction, PROGRESSFUNCTION, TTT, 0, 0) + OPT_ENTRY(seekfunction, SEEKFUNCTION, TTT, 0, 0) + OPT_ENTRY(debugfunction, DEBUGFUNCTION, TTT, 0, 0) +#if LCURL_CURL_VER_GE(7,19,6) + OPT_ENTRY(ssh_keyfunction, SSH_KEYFUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,21,0) + OPT_ENTRY(fnmatch_function, FNMATCH_FUNCTION, TTT, 0, 0) + OPT_ENTRY(chunk_bgn_function, CHUNK_BGN_FUNCTION, TTT, 0, 0) + OPT_ENTRY(chunk_end_function, CHUNK_END_FUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,46,0) + OPT_ENTRY(stream_depends, STREAM_DEPENDS, TTT, 0, 0) + OPT_ENTRY(stream_depends_e, STREAM_DEPENDS_E, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,56,0) + OPT_ENTRY(mimepost, MIMEPOST, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,63,0) + OPT_ENTRY(curlu, CURLU, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,64,0) + OPT_ENTRY(trailerfunction, TRAILERFUNCTION, TTT, 0, 0) +#endif +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + OPT_ENTRY(hstsreadfunction, HSTSREADFUNCTION, TTT, 0, 0) + OPT_ENTRY(hstswritefunction, HSTSWRITEFUNCTION,TTT, 0, 0) +#endif +#undef OPT_ENTRY +#undef FLG_ENTRY + +#define OPT_ENTRY(L, N, T, S) { "INFO_"#N, CURLINFO_##N }, +#include "lcinfoeasy.h" +#undef OPT_ENTRY + +#define OPT_ENTRY(N) { #N, CURL##N }, + // Debug message types not easy info + OPT_ENTRY(INFO_TEXT ) + OPT_ENTRY(INFO_HEADER_IN ) + OPT_ENTRY(INFO_HEADER_OUT ) + OPT_ENTRY(INFO_DATA_IN ) + OPT_ENTRY(INFO_DATA_OUT ) + OPT_ENTRY(INFO_SSL_DATA_OUT ) + OPT_ENTRY(INFO_SSL_DATA_IN ) + + // File types for CURL_CHUNK_BGN_FUNCTION +#if LCURL_CURL_VER_GE(7,21,0) + OPT_ENTRY(FILETYPE_DEVICE_BLOCK ) + OPT_ENTRY(FILETYPE_DEVICE_CHAR ) + OPT_ENTRY(FILETYPE_DIRECTORY ) + OPT_ENTRY(FILETYPE_DOOR ) + OPT_ENTRY(FILETYPE_FILE ) + OPT_ENTRY(FILETYPE_NAMEDPIPE ) + OPT_ENTRY(FILETYPE_SOCKET ) + OPT_ENTRY(FILETYPE_SYMLINK ) + OPT_ENTRY(FILETYPE_UNKNOWN ) +#endif + +#undef OPT_ENTRY + + {NULL, 0} +}; + +void lcurl_easy_initlib(lua_State *L, int nup){ + + /* Hack. We ensure that lcurl_easy_t and lcurl_hpost_stream_t + compatiable for readfunction + */ + LCURL_ASSERT_SAME_OFFSET(lcurl_easy_t, magic, lcurl_hpost_stream_t, magic); + LCURL_ASSERT_SAME_OFFSET(lcurl_easy_t, L, lcurl_hpost_stream_t, L); + LCURL_ASSERT_SAME_OFFSET(lcurl_easy_t, rd, lcurl_hpost_stream_t, rd); + LCURL_ASSERT_SAME_OFFSET(lcurl_easy_t, rbuffer, lcurl_hpost_stream_t, rbuffer); + LCURL_ASSERT_SAME_FIELD_SIZE(lcurl_easy_t, rbuffer, lcurl_hpost_stream_t, rbuffer); + + if(!lutil_createmetap(L, LCURL_EASY, lcurl_easy_methods, nup)) + lua_pop(L, nup); + lua_pop(L, 1); + + lcurl_util_set_const(L, lcurl_easy_opt); +} + +//} diff --git a/watchdog/third_party/lua-curl/src/lceasy.h b/watchdog/third_party/lua-curl/src/lceasy.h new file mode 100644 index 0000000..c03617c --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lceasy.h @@ -0,0 +1,127 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2021 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#ifndef _LCEASY_H_ +#define _LCEASY_H_ + +#include "lcurl.h" +#include "lcutils.h" +#include "lchttppost.h" + +#define LCURL_LST_INDEX(N) LCURL_##N##_LIST, +#define LCURL_STR_INDEX(N) +#define LCURL_LNG_INDEX(N) +#define LCURL_OFF_INDEX(N) +#define LCURL_BLB_INDEX(N) +#define OPT_ENTRY(L, N, T, S, D) LCURL_##T##_INDEX(N) + +enum { + LCURL_LIST_DUMMY = -1, + +#include"lcopteasy.h" + + LCURL_LIST_COUNT, +}; + +#undef LCURL_BLB_INDEX +#undef LCURL_OFF_INDEX +#undef LCURL_LST_INDEX +#undef LCURL_STR_INDEX +#undef LCURL_LNG_INDEX +#undef OPT_ENTRY + +#define LCURL_EASY_MAGIC 0xEA + +#if LCURL_CC_SUPPORT_FORWARD_TYPEDEF + typedef struct lcurl_multi_tag lcurl_multi_t; + #if LCURL_CURL_VER_GE(7,56,0) + typedef struct lcurl_mime_tag lcurl_mime_t; + #endif + #if LCURL_CURL_VER_GE(7,63,0) + typedef struct lcurl_url_tag lcurl_url_t; + #endif +#else + struct lcurl_multi_tag; + #define lcurl_multi_t struct lcurl_multi_tag + #if LCURL_CURL_VER_GE(7,56,0) + struct lcurl_mime_tag; + #define lcurl_mime_t struct lcurl_mime_tag + #endif + #if LCURL_CURL_VER_GE(7,63,0) + struct lcurl_url_tag; + #define lcurl_url_t struct lcurl_url_tag + #endif +#endif + +typedef struct lcurl_easy_tag{ + unsigned char magic; + + lua_State *L; + lcurl_callback_t rd; + lcurl_read_buffer_t rbuffer; + + lcurl_hpost_t *post; + + lcurl_multi_t *multi; + +#if LCURL_CURL_VER_GE(7,56,0) + lcurl_mime_t *mime; +#endif + + CURL *curl; + int storage; + int lists[LCURL_LIST_COUNT]; + int err_mode; + lcurl_callback_t wr; + lcurl_callback_t hd; + lcurl_callback_t pr; + lcurl_callback_t seek; + lcurl_callback_t debug; + lcurl_callback_t match; + lcurl_callback_t chunk_bgn; + lcurl_callback_t chunk_end; +#if LCURL_CURL_VER_GE(7,19,6) + lcurl_callback_t ssh_key; +#endif +#if LCURL_CURL_VER_GE(7,64,0) + lcurl_callback_t trailer; +#endif +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS + lcurl_callback_t hstsread; + lcurl_callback_t hstswrite; +#endif +}lcurl_easy_t; + +int lcurl_easy_create(lua_State *L, int error_mode); + +lcurl_easy_t *lcurl_geteasy_at(lua_State *L, int i); + +#define lcurl_geteasy(L) lcurl_geteasy_at((L),1) + +void lcurl_easy_initlib(lua_State *L, int nup); + +void lcurl__easy_assign_lua(lua_State *L, lcurl_easy_t *p, lua_State *value, int assign_multi); + +size_t lcurl_read_callback(lua_State *L, + lcurl_callback_t *rd, lcurl_read_buffer_t *rbuffer, + char *buffer, size_t size, size_t nitems +); + +#if !LCURL_CC_SUPPORT_FORWARD_TYPEDEF +#undef lcurl_multi_t +#ifdef lcurl_mime_t +#undef lcurl_mime_t +#endif +#ifdef lcurl_url_t +#undef lcurl_url_t +#endif +#endif + +#endif diff --git a/watchdog/third_party/lua-curl/src/lcerr_easy.h b/watchdog/third_party/lua-curl/src/lcerr_easy.h new file mode 100644 index 0000000..986026a --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcerr_easy.h @@ -0,0 +1,146 @@ +ERR_ENTRY ( OK ) +ERR_ENTRY ( UNSUPPORTED_PROTOCOL ) +ERR_ENTRY ( FAILED_INIT ) +ERR_ENTRY ( URL_MALFORMAT ) +#if LCURL_CURL_VER_GE(7,21,5) +ERR_ENTRY ( NOT_BUILT_IN ) +#endif +ERR_ENTRY ( COULDNT_RESOLVE_PROXY ) +ERR_ENTRY ( COULDNT_RESOLVE_HOST ) +ERR_ENTRY ( COULDNT_CONNECT ) +#if LCURL_CURL_VER_GE(7,51,0) +ERR_ENTRY ( WEIRD_SERVER_REPLY ) +#else +ERR_ENTRY ( FTP_WEIRD_SERVER_REPLY ) +#endif +ERR_ENTRY ( REMOTE_ACCESS_DENIED ) +#if LCURL_CURL_VER_GE(7,31,0) +ERR_ENTRY ( FTP_ACCEPT_FAILED ) +#endif +ERR_ENTRY ( FTP_WEIRD_PASS_REPLY ) +#if LCURL_CURL_VER_GE(7,24,0) +ERR_ENTRY ( FTP_ACCEPT_TIMEOUT ) +#endif +ERR_ENTRY ( FTP_WEIRD_PASV_REPLY ) +ERR_ENTRY ( FTP_WEIRD_227_FORMAT ) +ERR_ENTRY ( FTP_CANT_GET_HOST ) +ERR_ENTRY ( FTP_COULDNT_SET_TYPE ) +ERR_ENTRY ( PARTIAL_FILE ) +ERR_ENTRY ( FTP_COULDNT_RETR_FILE ) +ERR_ENTRY ( OBSOLETE20 ) +ERR_ENTRY ( QUOTE_ERROR ) +ERR_ENTRY ( HTTP_RETURNED_ERROR ) +ERR_ENTRY ( WRITE_ERROR ) +ERR_ENTRY ( OBSOLETE24 ) +ERR_ENTRY ( UPLOAD_FAILED ) +ERR_ENTRY ( READ_ERROR ) +ERR_ENTRY ( OUT_OF_MEMORY ) +ERR_ENTRY ( OPERATION_TIMEDOUT ) +ERR_ENTRY ( OBSOLETE29 ) +ERR_ENTRY ( FTP_PORT_FAILED ) +ERR_ENTRY ( FTP_COULDNT_USE_REST ) +ERR_ENTRY ( OBSOLETE32 ) +ERR_ENTRY ( RANGE_ERROR ) +ERR_ENTRY ( HTTP_POST_ERROR ) +ERR_ENTRY ( SSL_CONNECT_ERROR ) +ERR_ENTRY ( BAD_DOWNLOAD_RESUME ) +ERR_ENTRY ( FILE_COULDNT_READ_FILE ) +ERR_ENTRY ( LDAP_CANNOT_BIND ) +ERR_ENTRY ( LDAP_SEARCH_FAILED ) +ERR_ENTRY ( OBSOLETE40 ) +ERR_ENTRY ( FUNCTION_NOT_FOUND ) +ERR_ENTRY ( ABORTED_BY_CALLBACK ) +ERR_ENTRY ( BAD_FUNCTION_ARGUMENT ) +ERR_ENTRY ( OBSOLETE44 ) +ERR_ENTRY ( INTERFACE_FAILED ) +ERR_ENTRY ( OBSOLETE46 ) +ERR_ENTRY ( TOO_MANY_REDIRECTS ) +#if LCURL_CURL_VER_GE(7,21,5) +ERR_ENTRY ( UNKNOWN_OPTION ) +#else +ERR_ENTRY ( UNKNOWN_TELNET_OPTION ) /* User specified an unknown option */ +#endif +ERR_ENTRY ( TELNET_OPTION_SYNTAX ) +ERR_ENTRY ( OBSOLETE50 ) +ERR_ENTRY ( PEER_FAILED_VERIFICATION ) +ERR_ENTRY ( GOT_NOTHING ) +ERR_ENTRY ( SSL_ENGINE_NOTFOUND ) +ERR_ENTRY ( SSL_ENGINE_SETFAILED ) +ERR_ENTRY ( SEND_ERROR ) +ERR_ENTRY ( RECV_ERROR ) +ERR_ENTRY ( OBSOLETE57 ) +ERR_ENTRY ( SSL_CERTPROBLEM ) +ERR_ENTRY ( SSL_CIPHER ) +#if LCURL_CURL_VER_GE(7,62,0) +ERR_ENTRY ( OBSOLETE51 ) +#else +ERR_ENTRY ( SSL_CACERT ) +#endif +ERR_ENTRY ( BAD_CONTENT_ENCODING ) +ERR_ENTRY ( LDAP_INVALID_URL ) +ERR_ENTRY ( FILESIZE_EXCEEDED ) +ERR_ENTRY ( USE_SSL_FAILED ) +ERR_ENTRY ( SEND_FAIL_REWIND ) +ERR_ENTRY ( SSL_ENGINE_INITFAILED ) +ERR_ENTRY ( LOGIN_DENIED ) +ERR_ENTRY ( TFTP_NOTFOUND ) +ERR_ENTRY ( TFTP_PERM ) +ERR_ENTRY ( REMOTE_DISK_FULL ) +ERR_ENTRY ( TFTP_ILLEGAL ) +ERR_ENTRY ( TFTP_UNKNOWNID ) +ERR_ENTRY ( REMOTE_FILE_EXISTS ) +ERR_ENTRY ( TFTP_NOSUCHUSER ) +ERR_ENTRY ( CONV_FAILED ) +ERR_ENTRY ( CONV_REQD ) +ERR_ENTRY ( SSL_CACERT_BADFILE ) +ERR_ENTRY ( REMOTE_FILE_NOT_FOUND ) +ERR_ENTRY ( SSH ) +ERR_ENTRY ( SSL_SHUTDOWN_FAILED ) +ERR_ENTRY ( AGAIN ) +ERR_ENTRY ( SSL_CRL_BADFILE ) +ERR_ENTRY ( SSL_ISSUER_ERROR ) +#if LCURL_CURL_VER_GE(7,20,0) +ERR_ENTRY ( FTP_PRET_FAILED ) +#endif +#if LCURL_CURL_VER_GE(7,21,0) +ERR_ENTRY ( FTP_BAD_FILE_LIST ) +#endif +#if LCURL_CURL_VER_GE(7,20,0) +ERR_ENTRY ( RTSP_CSEQ_ERROR ) +ERR_ENTRY ( RTSP_SESSION_ERROR ) +#endif +#if LCURL_CURL_VER_GE(7,21,0) +ERR_ENTRY ( CHUNK_FAILED ) +#endif +#if LCURL_CURL_VER_GE(7,30,0) +ERR_ENTRY ( NO_CONNECTION_AVAILABLE ) +#endif +#if LCURL_CURL_VER_GE(7,38,0) +ERR_ENTRY ( HTTP2 ) +#else +ERR_ENTRY ( OBSOLETE16 ) +#endif +#if LCURL_CURL_VER_GE(7,39,0) +ERR_ENTRY ( SSL_PINNEDPUBKEYNOTMATCH ) +#endif +#if LCURL_CURL_VER_GE(7,41,0) +ERR_ENTRY ( SSL_INVALIDCERTSTATUS ) +#endif +#if LCURL_CURL_VER_GE(7,49,0) +ERR_ENTRY ( HTTP2_STREAM ) +#endif +#if LCURL_CURL_VER_GE(7,59,0) +ERR_ENTRY ( RECURSIVE_API_CALL ) +#endif +#if LCURL_CURL_VER_GE(7,66,0) +ERR_ENTRY ( AUTH_ERROR ) +#endif +#if LCURL_CURL_VER_GE(7,68,0) +ERR_ENTRY ( HTTP3 ) +#endif +#if LCURL_CURL_VER_GE(7,69,0) +ERR_ENTRY ( QUIC_CONNECT_ERROR ) +#endif +#if LCURL_CURL_VER_GE(7,73,0) +ERR_ENTRY ( PROXY ) +#endif diff --git a/watchdog/third_party/lua-curl/src/lcerr_form.h b/watchdog/third_party/lua-curl/src/lcerr_form.h new file mode 100644 index 0000000..60ca8ce --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcerr_form.h @@ -0,0 +1,8 @@ +ERR_ENTRY ( OK ) +ERR_ENTRY ( MEMORY ) +ERR_ENTRY ( OPTION_TWICE ) +ERR_ENTRY ( NULL ) +ERR_ENTRY ( UNKNOWN_OPTION ) +ERR_ENTRY ( INCOMPLETE ) +ERR_ENTRY ( ILLEGAL_ARRAY ) +ERR_ENTRY ( DISABLED ) diff --git a/watchdog/third_party/lua-curl/src/lcerr_multi.h b/watchdog/third_party/lua-curl/src/lcerr_multi.h new file mode 100644 index 0000000..45322b7 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcerr_multi.h @@ -0,0 +1,14 @@ +ERR_ENTRY ( OK ) +ERR_ENTRY ( CALL_MULTI_PERFORM ) +ERR_ENTRY ( BAD_HANDLE ) +ERR_ENTRY ( BAD_EASY_HANDLE ) +ERR_ENTRY ( OUT_OF_MEMORY ) +ERR_ENTRY ( INTERNAL_ERROR ) +ERR_ENTRY ( BAD_SOCKET ) +ERR_ENTRY ( UNKNOWN_OPTION ) +#if LCURL_CURL_VER_GE(7,32,1) +ERR_ENTRY ( ADDED_ALREADY ) +#endif +#if LCURL_CURL_VER_GE(7,59,0) +ERR_ENTRY ( RECURSIVE_API_CALL ) +#endif diff --git a/watchdog/third_party/lua-curl/src/lcerr_share.h b/watchdog/third_party/lua-curl/src/lcerr_share.h new file mode 100644 index 0000000..7027c2a --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcerr_share.h @@ -0,0 +1,8 @@ +ERR_ENTRY ( OK ) +ERR_ENTRY ( BAD_OPTION ) +ERR_ENTRY ( IN_USE ) +ERR_ENTRY ( INVALID ) +ERR_ENTRY ( NOMEM ) +#if LCURL_CURL_VER_GE(7,23,0) +ERR_ENTRY ( NOT_BUILT_IN ) +#endif diff --git a/watchdog/third_party/lua-curl/src/lcerr_url.h b/watchdog/third_party/lua-curl/src/lcerr_url.h new file mode 100644 index 0000000..f1002a1 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcerr_url.h @@ -0,0 +1,20 @@ +#if LCURL_CURL_VER_GE(7,61,0) +ERR_ENTRY ( BAD_HANDLE ) +ERR_ENTRY ( BAD_PARTPOINTER ) +ERR_ENTRY ( BAD_PORT_NUMBER ) +ERR_ENTRY ( MALFORMED_INPUT ) +ERR_ENTRY ( NO_FRAGMENT ) +ERR_ENTRY ( NO_HOST ) +ERR_ENTRY ( NO_OPTIONS ) +ERR_ENTRY ( NO_PASSWORD ) +ERR_ENTRY ( NO_PORT ) +ERR_ENTRY ( NO_QUERY ) +ERR_ENTRY ( NO_SCHEME ) +ERR_ENTRY ( NO_USER ) +ERR_ENTRY ( OK ) +ERR_ENTRY ( OUT_OF_MEMORY ) +ERR_ENTRY ( UNKNOWN_PART ) +ERR_ENTRY ( UNSUPPORTED_SCHEME) +ERR_ENTRY ( URLDECODE ) +ERR_ENTRY ( USER_NOT_ALLOWED ) +#endif \ No newline at end of file diff --git a/watchdog/third_party/lua-curl/src/lcerror.c b/watchdog/third_party/lua-curl/src/lcerror.c new file mode 100644 index 0000000..11eea55 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcerror.c @@ -0,0 +1,342 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#include "lcurl.h" +#include "lcerror.h" +#include +#include "lcutils.h" + +#define LCURL_ERROR_NAME LCURL_PREFIX" Error" +static const char *LCURL_ERROR = LCURL_ERROR_NAME; + +#define LCURL_ERROR_EASY_NAME "CURL-EASY" +#define LCURL_ERROR_MULTI_NAME "CURL-MULTI" +#define LCURL_ERROR_SHARE_NAME "CURL-SHARE" +#define LCURL_ERROR_FORM_NAME "CURL-FORM" +#define LCURL_ERROR_URL_NAME "CURL-URL" + +typedef struct lcurl_error_tag{ + int tp; + int no; +}lcurl_error_t; + +//{ + +static const char* lcurl_err_easy_mnemo(int err){ +#define ERR_ENTRY(E) case CURLE_##E: return #E; + + switch (err){ + #include "lcerr_easy.h" + } + return "UNKNOWN"; + +#undef ERR_ENTRY +} + +static const char* lcurl_err_multi_mnemo(int err){ +#define ERR_ENTRY(E) case CURLM_##E: return #E; + + switch (err){ + #include "lcerr_multi.h" + } + return "UNKNOWN"; + +#undef ERR_ENTRY +} + +static const char* lcurl_err_share_mnemo(int err){ +#define ERR_ENTRY(E) case CURLSHE_##E: return #E; + + switch (err){ + #include "lcerr_share.h" + } + return "UNKNOWN"; + +#undef ERR_ENTRY +} + +static const char* lcurl_err_form_mnemo(int err){ +#define ERR_ENTRY(E) case CURL_FORMADD_##E: return #E; + + switch (err){ + #include "lcerr_form.h" + } + return "UNKNOWN"; + +#undef ERR_ENTRY +} + +static const char* lcurl_err_url_mnemo(int err){ +#define ERR_ENTRY(E) case CURLUE_##E: return #E; + + switch (err){ + #include "lcerr_url.h" + } + return "UNKNOWN"; + +#undef ERR_ENTRY +} + +static const char* _lcurl_err_mnemo(int tp, int err){ + switch(tp){ + case LCURL_ERROR_EASY : return lcurl_err_easy_mnemo (err); + case LCURL_ERROR_MULTI: return lcurl_err_multi_mnemo(err); + case LCURL_ERROR_SHARE: return lcurl_err_share_mnemo(err); + case LCURL_ERROR_FORM : return lcurl_err_form_mnemo (err); + case LCURL_ERROR_URL : return lcurl_err_url_mnemo (err); + } + assert(0); + return ""; +} + +static const char* _lcurl_err_msg(int tp, int err){ + switch(tp){ + case LCURL_ERROR_EASY : return curl_easy_strerror (err); + case LCURL_ERROR_MULTI: return curl_multi_strerror(err); + case LCURL_ERROR_SHARE: return curl_share_strerror(err); + case LCURL_ERROR_FORM : return lcurl_err_form_mnemo(err); + case LCURL_ERROR_URL : return lcurl_err_url_mnemo(err); + } + assert(0); + return ""; +} + +static const char* _lcurl_err_category_name(int tp){ + assert( + (tp == LCURL_ERROR_EASY ) || + (tp == LCURL_ERROR_MULTI) || + (tp == LCURL_ERROR_SHARE) || + (tp == LCURL_ERROR_FORM ) || + (tp == LCURL_ERROR_URL ) || + 0 + ); + + switch(tp){ + case LCURL_ERROR_EASY: { + static const char *name = LCURL_ERROR_EASY_NAME; + return name; + } + case LCURL_ERROR_MULTI: { + static const char *name = LCURL_ERROR_MULTI_NAME; + return name; + } + case LCURL_ERROR_SHARE: { + static const char *name = LCURL_ERROR_SHARE_NAME; + return name; + } + case LCURL_ERROR_FORM: { + static const char *name = LCURL_ERROR_FORM_NAME; + return name; + } + case LCURL_ERROR_URL: { + static const char *name = LCURL_ERROR_URL_NAME; + return name; + } + } + + assert(0); + return NULL; +} + +static void _lcurl_err_pushstring(lua_State *L, int tp, int err){ + lua_pushfstring(L, "[%s][%s] %s (%d)", + _lcurl_err_category_name(tp), + _lcurl_err_mnemo(tp, err), + _lcurl_err_msg(tp, err), + err + ); +} + +//} + +//{ + +int lcurl_error_create(lua_State *L, int error_type, int no){ + lcurl_error_t *err = lutil_newudatap(L, lcurl_error_t, LCURL_ERROR); + + assert( + (error_type == LCURL_ERROR_EASY ) || + (error_type == LCURL_ERROR_MULTI) || + (error_type == LCURL_ERROR_SHARE) || + (error_type == LCURL_ERROR_FORM ) || + (error_type == LCURL_ERROR_URL ) || + 0 + ); + + err->tp = error_type; + err->no = no; + return 1; +} + +static lcurl_error_t *lcurl_geterror_at(lua_State *L, int i){ + lcurl_error_t *err = (lcurl_error_t *)lutil_checkudatap (L, i, LCURL_ERROR); + luaL_argcheck (L, err != NULL, 1, LCURL_PREFIX"error object expected"); + return err; +} + +#define lcurl_geterror(L) lcurl_geterror_at((L),1) + +static int lcurl_err_no(lua_State *L){ + lcurl_error_t *err = lcurl_geterror(L); + lua_pushinteger(L, err->no); + return 1; +} + +static int lcurl_err_msg(lua_State *L){ + lcurl_error_t *err = lcurl_geterror(L); + lua_pushstring(L, _lcurl_err_msg(err->tp, err->no)); + return 1; +} + +static int lcurl_err_mnemo(lua_State *L){ + lcurl_error_t *err = lcurl_geterror(L); + lua_pushstring(L, _lcurl_err_mnemo(err->tp, err->no)); + return 1; +} + +static int lcurl_err_tostring(lua_State *L){ + lcurl_error_t *err = lcurl_geterror(L); + _lcurl_err_pushstring(L, err->tp, err->no); + return 1; +} + +static int lcurl_err_equal(lua_State *L){ + lcurl_error_t *lhs = lcurl_geterror_at(L, 1); + lcurl_error_t *rhs = lcurl_geterror_at(L, 2); + lua_pushboolean(L, ((lhs->no == rhs->no)&&(lhs->tp == rhs->tp))?1:0); + return 1; +} + +static int lcurl_err_category(lua_State *L){ + lcurl_error_t *err = lcurl_geterror(L); + lua_pushstring(L, _lcurl_err_category_name(err->tp)); + return 1; +} + +//} + +//{ + +int lcurl_fail_ex(lua_State *L, int mode, int error_type, int code){ + if(mode == LCURL_ERROR_RETURN){ + lua_pushnil(L); + lcurl_error_create(L, error_type, code); + return 2; + } + +#if LUA_VERSION_NUM >= 502 // lua 5.2 + lcurl_error_create(L, error_type, code); +#else + _lcurl_err_pushstring(L, error_type, code); +#endif + + assert(LCURL_ERROR_RAISE == mode); + + return lua_error(L); +} + +int lcurl_fail(lua_State *L, int error_type, int code){ + return lcurl_fail_ex(L, LCURL_ERROR_RETURN, error_type, code); +} + +//} + +static const int ERROR_CATEGORIES[] = { + LCURL_ERROR_EASY, + LCURL_ERROR_MULTI, + LCURL_ERROR_SHARE, + LCURL_ERROR_FORM, + LCURL_ERROR_URL, +}; + +static const char* ERROR_CATEGORIES_NAME[] = { + LCURL_ERROR_EASY_NAME, + LCURL_ERROR_MULTI_NAME, + LCURL_ERROR_SHARE_NAME, + LCURL_ERROR_FORM_NAME, + LCURL_ERROR_URL_NAME, + NULL +}; + +int lcurl_error_new(lua_State *L){ + int tp, no = luaL_checkint(L, 2); + if (lua_isnumber(L, 1)){ + tp = luaL_checkint(L, 2); + } + else{ + tp = luaL_checkoption(L, 1, NULL, ERROR_CATEGORIES_NAME); + tp = ERROR_CATEGORIES[tp]; + } + + //! @todo checks error type value + + lcurl_error_create(L, tp, no); + return 1; +} + +static const struct luaL_Reg lcurl_err_methods[] = { + {"no", lcurl_err_no }, + {"msg", lcurl_err_msg }, + {"name", lcurl_err_mnemo }, + {"mnemo", lcurl_err_mnemo }, + {"cat", lcurl_err_category }, + {"category", lcurl_err_category }, + {"__tostring", lcurl_err_tostring }, + {"__eq", lcurl_err_equal }, + + {NULL,NULL} +}; + +static const lcurl_const_t lcurl_error_codes[] = { + +#define ERR_ENTRY(N) { "E_"#N, CURLE_##N }, +#include "lcerr_easy.h" +#undef ERR_ENTRY + +/* libcurl rename CURLE_FTP_WEIRD_SERVER_REPLY to CURLE_WEIRD_SERVER_REPLY in version 7.51.0*/ +/* we can not have both codes in general because we have to be able convern error number to error name*/ +/* so we use newest name but add error code as alias.*/ +#if LCURL_CURL_VER_GE(7,51,0) + { "E_FTP_WEIRD_SERVER_REPLY", CURLE_FTP_WEIRD_SERVER_REPLY }, +#else + { "E_WEIRD_SERVER_REPLY", CURLE_FTP_WEIRD_SERVER_REPLY }, +#endif + +#define ERR_ENTRY(N) { "E_MULTI_"#N, CURLM_##N }, +#include "lcerr_multi.h" +#undef ERR_ENTRY + +#define ERR_ENTRY(N) { "E_SHARE_"#N, CURLSHE_##N }, +#include "lcerr_share.h" +#undef ERR_ENTRY + +#define ERR_ENTRY(N) { "E_FORM_"#N, CURL_FORMADD_##N }, +#include "lcerr_form.h" +#undef ERR_ENTRY + +#define ERR_ENTRY(N) { "E_URL_"#N, CURLUE_##N }, +#include "lcerr_url.h" +#undef ERR_ENTRY + + {NULL, 0} +}; + +void lcurl_error_initlib(lua_State *L, int nup){ + if(!lutil_createmetap(L, LCURL_ERROR, lcurl_err_methods, nup)) + lua_pop(L, nup); + lua_pop(L, 1); + + lcurl_util_set_const(L, lcurl_error_codes); + + lua_pushstring(L, _lcurl_err_category_name(LCURL_ERROR_EASY ));lua_setfield(L, -2, "ERROR_EASY" ); + lua_pushstring(L, _lcurl_err_category_name(LCURL_ERROR_MULTI ));lua_setfield(L, -2, "ERROR_MULTI"); + lua_pushstring(L, _lcurl_err_category_name(LCURL_ERROR_SHARE ));lua_setfield(L, -2, "ERROR_SHARE"); + lua_pushstring(L, _lcurl_err_category_name(LCURL_ERROR_FORM ));lua_setfield(L, -2, "ERROR_FORM" ); +} diff --git a/watchdog/third_party/lua-curl/src/lcerror.h b/watchdog/third_party/lua-curl/src/lcerror.h new file mode 100644 index 0000000..da4e1c5 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcerror.h @@ -0,0 +1,34 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#ifndef _LCERROR_H_ +#define _LCERROR_H_ + +#include "lcurl.h" + +#define LCURL_ERROR_CURL 1 +#define LCURL_ERROR_EASY 1 +#define LCURL_ERROR_MULTI 2 +#define LCURL_ERROR_SHARE 3 +#define LCURL_ERROR_FORM 4 +#define LCURL_ERROR_URL 5 + +#define LCURL_ERROR_RETURN 1 +#define LCURL_ERROR_RAISE 2 + +int lcurl_fail(lua_State *L, int error_type, int code); + +int lcurl_fail_ex(lua_State *L, int mode, int error_type, int code); + +int lcurl_error_new(lua_State *L); + +void lcurl_error_initlib(lua_State *L, int nup); + +#endif diff --git a/watchdog/third_party/lua-curl/src/lcflags.h b/watchdog/third_party/lua-curl/src/lcflags.h new file mode 100644 index 0000000..0fbeff2 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcflags.h @@ -0,0 +1,283 @@ +/* Bitmasks for CURLOPT_HTTPAUTH and CURLOPT_PROXYAUTH options */ +FLG_ENTRY(AUTH_NONE ) +FLG_ENTRY(AUTH_BASIC ) +FLG_ENTRY(AUTH_DIGEST ) +FLG_ENTRY(AUTH_GSSNEGOTIATE ) +#if LCURL_CURL_VER_GE(7,38,0) +FLG_ENTRY(AUTH_NEGOTIATE ) +#endif +FLG_ENTRY(AUTH_NTLM ) +#if LCURL_CURL_VER_GE(7,19,3) +FLG_ENTRY(AUTH_DIGEST_IE ) +#endif +#if LCURL_CURL_VER_GE(7,19,6) +FLG_ENTRY(KHSTAT_FINE_ADD_TO_FILE ) +FLG_ENTRY(KHSTAT_FINE ) +FLG_ENTRY(KHSTAT_REJECT ) +FLG_ENTRY(KHSTAT_DEFER ) +FLG_ENTRY(KHMATCH_OK ) +FLG_ENTRY(KHMATCH_MISMATCH ) +FLG_ENTRY(KHMATCH_MISSING ) +FLG_ENTRY(KHTYPE_RSA1 ) +FLG_ENTRY(KHTYPE_RSA ) +FLG_ENTRY(KHTYPE_DSS ) +#endif +#if LCURL_CURL_VER_GE(7,58,0) +FLG_ENTRY(KHTYPE_ECDSA ) +FLG_ENTRY(KHTYPE_ED25519 ) +#endif +#if LCURL_CURL_VER_GE(7,73,0) +FLG_ENTRY(KHSTAT_FINE_REPLACE ) +#endif + +#if LCURL_CURL_VER_GE(7,22,0) +FLG_ENTRY(AUTH_NTLM_WB ) +#endif +#if LCURL_CURL_VER_GE(7,21,3) +FLG_ENTRY(AUTH_ONLY ) +#endif +FLG_ENTRY(AUTH_ANY ) +FLG_ENTRY(AUTH_ANYSAFE ) +#if LCURL_CURL_VER_GE(7,55,0) +FLG_ENTRY(AUTH_GSSAPI ) +#endif +#if LCURL_CURL_VER_GE(7,61,0) +FLG_ENTRY(AUTH_BEARER ) +#endif + +#ifdef CURLSSH_AUTH_ANY +FLG_ENTRY(SSH_AUTH_ANY ) +#endif +#ifdef CURLSSH_AUTH_NONE +FLG_ENTRY(SSH_AUTH_NONE ) +#endif +#ifdef CURLSSH_AUTH_PUBLICKEY +FLG_ENTRY(SSH_AUTH_PUBLICKEY ) +#endif +#ifdef CURLSSH_AUTH_PASSWORD +FLG_ENTRY(SSH_AUTH_PASSWORD ) +#endif +#ifdef CURLSSH_AUTH_HOST +FLG_ENTRY(SSH_AUTH_HOST ) +#endif +#ifdef CURLSSH_AUTH_GSSAPI +FLG_ENTRY(SSH_AUTH_GSSAPI ) +#endif +#ifdef CURLSSH_AUTH_KEYBOARD +FLG_ENTRY(SSH_AUTH_KEYBOARD ) +#endif +#ifdef CURLSSH_AUTH_AGENT +FLG_ENTRY(SSH_AUTH_AGENT ) +#endif +#ifdef CURLSSH_AUTH_DEFAULT +FLG_ENTRY(SSH_AUTH_DEFAULT ) +#endif + +#ifdef CURLGSSAPI_DELEGATION_NONE +FLG_ENTRY(GSSAPI_DELEGATION_NONE ) +#endif +#ifdef CURLGSSAPI_DELEGATION_POLICY_FLAG +FLG_ENTRY(GSSAPI_DELEGATION_POLICY_FLAG ) +#endif +#ifdef CURLGSSAPI_DELEGATION_FLAG +FLG_ENTRY(GSSAPI_DELEGATION_FLAG ) +#endif + +/* Bitmasks for CURLOPT_HTTPAUTH and CURLOPT_PROXYAUTH options */ +FLG_ENTRY(USESSL_NONE ) +FLG_ENTRY(USESSL_TRY ) +FLG_ENTRY(USESSL_CONTROL ) +FLG_ENTRY(USESSL_ALL ) + +/* Definition of bits for the CURLOPT_SSL_OPTIONS argument: */ +#ifdef CURLSSLOPT_ALLOW_BEAST +FLG_ENTRY(SSLOPT_ALLOW_BEAST ) +#endif +#ifdef CURLSSLOPT_NO_REVOKE +FLG_ENTRY(SSLOPT_NO_REVOKE ) +#endif +#ifdef CURLSSLOPT_NO_PARTIALCHAIN +FLG_ENTRY(SSLOPT_NO_PARTIALCHAIN ) +#endif +#ifdef CURLSSLOPT_REVOKE_BEST_EFFORT +FLG_ENTRY(SSLOPT_REVOKE_BEST_EFFORT ) +#endif +#ifdef CURLSSLOPT_NATIVE_CA +FLG_ENTRY(SSLOPT_NATIVE_CA ) +#endif + +/* parameter for the CURLOPT_FTP_SSL_CCC option */ +FLG_ENTRY(FTPSSL_CCC_NONE ) +FLG_ENTRY(FTPSSL_CCC_PASSIVE ) +FLG_ENTRY(FTPSSL_CCC_ACTIVE ) + +/* parameter for the CURLOPT_FTPSSLAUTH option */ +FLG_ENTRY(FTPAUTH_DEFAULT ) +FLG_ENTRY(FTPAUTH_SSL ) +FLG_ENTRY(FTPAUTH_TLS ) + +/* parameter for the CURLOPT_FTP_CREATE_MISSING_DIRS option */ +FLG_ENTRY(FTP_CREATE_DIR_NONE ) +FLG_ENTRY(FTP_CREATE_DIR ) +FLG_ENTRY(FTP_CREATE_DIR_RETRY ) +FLG_ENTRY(FTP_CREATE_DIR_LAST ) + +/* parameter for the CURLOPT_FTP_FILEMETHOD option */ +FLG_ENTRY(FTPMETHOD_DEFAULT ) +FLG_ENTRY(FTPMETHOD_MULTICWD ) +FLG_ENTRY(FTPMETHOD_NOCWD ) +FLG_ENTRY(FTPMETHOD_SINGLECWD ) + +/* bitmask defines for CURLOPT_HEADEROPT */ +#if LCURL_CURL_VER_GE(7,37,0) +FLG_ENTRY(HEADER_UNIFIED ) +FLG_ENTRY(HEADER_SEPARATE ) +#endif + +/* CURLPROTO_ defines are for the CURLOPT_*PROTOCOLS options */ +FLG_ENTRY(PROTO_HTTP ) +FLG_ENTRY(PROTO_HTTPS ) +FLG_ENTRY(PROTO_FTP ) +FLG_ENTRY(PROTO_FTPS ) +FLG_ENTRY(PROTO_SCP ) +FLG_ENTRY(PROTO_SFTP ) +FLG_ENTRY(PROTO_TELNET ) +FLG_ENTRY(PROTO_LDAP ) +FLG_ENTRY(PROTO_LDAPS ) +FLG_ENTRY(PROTO_DICT ) +FLG_ENTRY(PROTO_FILE ) +FLG_ENTRY(PROTO_TFTP ) +#ifdef CURLPROTO_IMAP +FLG_ENTRY(PROTO_IMAP ) +#endif +#ifdef CURLPROTO_IMAPS +FLG_ENTRY(PROTO_IMAPS ) +#endif +#ifdef CURLPROTO_POP3 +FLG_ENTRY(PROTO_POP3 ) +#endif +#ifdef CURLPROTO_POP3S +FLG_ENTRY(PROTO_POP3S ) +#endif +#ifdef CURLPROTO_SMTP +FLG_ENTRY(PROTO_SMTP ) +#endif +#ifdef CURLPROTO_SMTPS +FLG_ENTRY(PROTO_SMTPS ) +#endif +#ifdef CURLPROTO_RTSP +FLG_ENTRY(PROTO_RTSP ) +#endif +#ifdef CURLPROTO_RTMP +FLG_ENTRY(PROTO_RTMP ) +#endif +#ifdef CURLPROTO_RTMPT +FLG_ENTRY(PROTO_RTMPT ) +#endif +#ifdef CURLPROTO_RTMPE +FLG_ENTRY(PROTO_RTMPE ) +#endif +#ifdef CURLPROTO_RTMPTE +FLG_ENTRY(PROTO_RTMPTE ) +#endif +#ifdef CURLPROTO_RTMPS +FLG_ENTRY(PROTO_RTMPS ) +#endif +#ifdef CURLPROTO_RTMPTS +FLG_ENTRY(PROTO_RTMPTS ) +#endif +#ifdef CURLPROTO_GOPHER +FLG_ENTRY(PROTO_GOPHER ) +#endif +#ifdef CURLPROTO_SMB +FLG_ENTRY(PROTO_SMB ) +#endif +#ifdef CURLPROTO_SMBS +FLG_ENTRY(PROTO_SMBS ) +#endif +#ifdef CURLPROTO_MQTT +FLG_ENTRY(PROTO_MQTT ) +#endif +FLG_ENTRY(PROTO_ALL ) + +FLG_ENTRY(PROXY_HTTP ) /* added in 7.10.0 */ +FLG_ENTRY(PROXY_HTTP_1_0 ) /* added in 7.19.4 */ +FLG_ENTRY(PROXY_SOCKS4 ) /* added in 7.15.2 */ +FLG_ENTRY(PROXY_SOCKS5 ) /* added in 7.10.0 */ +FLG_ENTRY(PROXY_SOCKS4A ) /* added in 7.18.0 */ +FLG_ENTRY(PROXY_SOCKS5_HOSTNAME ) /* added in 7.18.0 */ +#if LCURL_CURL_VER_GE(7,52,0) +FLG_ENTRY(PROXY_HTTPS ) +#endif + +FLG_ENTRY(PAUSE_ALL ) /* added in 7.18.0 */ +FLG_ENTRY(PAUSE_CONT ) /* added in 7.18.0 */ +FLG_ENTRY(PAUSE_RECV ) /* added in 7.18.0 */ +FLG_ENTRY(PAUSE_RECV_CONT ) /* added in 7.18.0 */ +FLG_ENTRY(PAUSE_SEND ) /* added in 7.18.0 */ +FLG_ENTRY(PAUSE_SEND_CONT ) /* added in 7.18.0 */ + +#if LCURL_CURL_VER_GE(7,64,1) +FLG_ENTRY(ALTSVC_H1) +FLG_ENTRY(ALTSVC_H2) +FLG_ENTRY(ALTSVC_H3) +FLG_ENTRY(ALTSVC_READONLYFILE) +#endif + +#if LCURL_CURL_VER_GE(7,73,0) +FLG_ENTRY(PX_OK) +FLG_ENTRY(PX_BAD_ADDRESS_TYPE) +FLG_ENTRY(PX_BAD_VERSION) +FLG_ENTRY(PX_CLOSED) +FLG_ENTRY(PX_GSSAPI) +FLG_ENTRY(PX_GSSAPI_PERMSG) +FLG_ENTRY(PX_GSSAPI_PROTECTION) +FLG_ENTRY(PX_IDENTD) +FLG_ENTRY(PX_IDENTD_DIFFER) +FLG_ENTRY(PX_LONG_HOSTNAME) +FLG_ENTRY(PX_LONG_PASSWD) +FLG_ENTRY(PX_LONG_USER) +FLG_ENTRY(PX_NO_AUTH) +FLG_ENTRY(PX_RECV_ADDRESS) +FLG_ENTRY(PX_RECV_AUTH) +FLG_ENTRY(PX_RECV_CONNECT) +FLG_ENTRY(PX_RECV_REQACK) +FLG_ENTRY(PX_REPLY_ADDRESS_TYPE_NOT_SUPPORTED) +FLG_ENTRY(PX_REPLY_COMMAND_NOT_SUPPORTED) +FLG_ENTRY(PX_REPLY_CONNECTION_REFUSED) +FLG_ENTRY(PX_REPLY_GENERAL_SERVER_FAILURE) +FLG_ENTRY(PX_REPLY_HOST_UNREACHABLE) +FLG_ENTRY(PX_REPLY_NETWORK_UNREACHABLE) +FLG_ENTRY(PX_REPLY_NOT_ALLOWED) +FLG_ENTRY(PX_REPLY_TTL_EXPIRED) +FLG_ENTRY(PX_REPLY_UNASSIGNED) +FLG_ENTRY(PX_REQUEST_FAILED) +FLG_ENTRY(PX_RESOLVE_HOST) +FLG_ENTRY(PX_SEND_AUTH) +FLG_ENTRY(PX_SEND_CONNECT) +FLG_ENTRY(PX_SEND_REQUEST) +FLG_ENTRY(PX_UNKNOWN_FAIL) +FLG_ENTRY(PX_UNKNOWN_MODE) +FLG_ENTRY(PX_USER_REJECTED) +#endif + +#if LCURL_CURL_VER_GE(7,73,0) +FLG_ENTRY(OT_LONG) +FLG_ENTRY(OT_VALUES) +FLG_ENTRY(OT_OFF_T) +FLG_ENTRY(OT_OBJECT) +FLG_ENTRY(OT_STRING) +FLG_ENTRY(OT_SLIST) +FLG_ENTRY(OT_CBPTR) +FLG_ENTRY(OT_BLOB) +FLG_ENTRY(OT_FUNCTION) +FLG_ENTRY(OT_FLAG_ALIAS) +#endif + +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS +FLG_ENTRY(HSTS_ENABLE) +FLG_ENTRY(HSTS_READONLYFILE) +FLG_ENTRY(STS_OK) +FLG_ENTRY(STS_DONE) +FLG_ENTRY(STS_FAIL) +#endif diff --git a/watchdog/third_party/lua-curl/src/lchttppost.c b/watchdog/third_party/lua-curl/src/lchttppost.c new file mode 100644 index 0000000..2d49323 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lchttppost.c @@ -0,0 +1,595 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#include "lcurl.h" +#include "lchttppost.h" +#include "lcerror.h" +#include "lcutils.h" + +#define LCURL_HTTPPOST_NAME LCURL_PREFIX" HTTPPost" +static const char *LCURL_HTTPPOST = LCURL_HTTPPOST_NAME; + + +#if LUA_VERSION_NUM >= 503 /* Lua 5.3 */ + +/*! @fixme detect real types (e.g. float/int32_t) */ + +# define LCURL_USE_INTEGER + +#endif + +#ifdef LCURL_USE_INTEGER +# ifdef LUA_32BITS +# define LCURL_INT_SIZE_16 +# define LCURL_INT_SIZE_32 +# else +# define LCURL_INT_SIZE_16 +# define LCURL_INT_SIZE_32 +# define LCURL_INT_SIZE_64 +# endif +#endif + +#if LCURL_CURL_VER_GE(7,46,0) +# define LCURL_FORM_CONTENTLEN CURLFORM_CONTENTLEN +# define LCURL_LEN_TYPE curl_off_t +#else +# define LCURL_FORM_CONTENTLEN CURLFORM_CONTENTSLENGTH +# define LCURL_LEN_TYPE long +#endif + +/* 7.56.0 changed code for `curl_formget` if callback abort write. + * + * https://github.com/curl/curl/issues/1987#issuecomment-336139060 + * ... not sure its worth the effort to document its return codes to + * any further extent then it currently is. This function is very + * rarely used, and the new mime API doesn't even have a version of it. + **/ +#if LCURL_CURL_VER_GE(7,56,0) +# define LCURL_GET_CB_ERROR CURLE_READ_ERROR +#else +# define LCURL_GET_CB_ERROR (CURLcode)-1 +#endif + +//{ stream + +static lcurl_hpost_stream_t *lcurl_hpost_stream_add(lua_State *L, lcurl_hpost_t *p){ + lcurl_hpost_stream_t *ptr = p->stream; + lcurl_hpost_stream_t *stream = malloc(sizeof(lcurl_hpost_stream_t)); + if(!stream) return NULL; + + stream->magic = LCURL_HPOST_STREAM_MAGIC; + stream->L = &p->L; + stream->rbuffer.ref = LUA_NOREF; + stream->rd.cb_ref = stream->rd.ud_ref = LUA_NOREF; + stream->next = NULL; + if(!p->stream) p->stream = stream; + else{ + while(ptr->next) ptr = ptr->next; + ptr->next = stream; + } + return stream; +} + +static void lcurl_hpost_stream_free(lua_State *L, lcurl_hpost_stream_t *ptr){ + if(ptr){ + luaL_unref(L, LCURL_LUA_REGISTRY, ptr->rbuffer.ref); + luaL_unref(L, LCURL_LUA_REGISTRY, ptr->rd.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, ptr->rd.ud_ref); + free(ptr); + } +} + +static void lcurl_hpost_stream_free_last(lua_State *L, lcurl_hpost_t *p){ + lcurl_hpost_stream_t *ptr = p->stream; + if(!ptr) return; + if(!ptr->next){ + lcurl_hpost_stream_free(L, ptr); + p->stream = 0; + } + + while(ptr->next->next) ptr = ptr->next; + lcurl_hpost_stream_free(L, ptr->next); + ptr->next = NULL; +} + +static void lcurl_hpost_stream_free_all(lua_State *L, lcurl_hpost_t *p){ + lcurl_hpost_stream_t *ptr = p->stream; + while(ptr){ + lcurl_hpost_stream_t *next = ptr->next; + lcurl_hpost_stream_free(L, ptr); + ptr = next; + } + p->stream = 0; +} + +//} + +//{ HTTPPost + +int lcurl_hpost_create(lua_State *L, int error_mode){ + lcurl_hpost_t *p = lutil_newudatap(L, lcurl_hpost_t, LCURL_HTTPPOST); + p->post = p->last = 0; + p->storage = lcurl_storage_init(L); + p->err_mode = error_mode; + p->stream = 0; + + return 1; +} + +lcurl_hpost_t *lcurl_gethpost_at(lua_State *L, int i){ + lcurl_hpost_t *p = (lcurl_hpost_t *)lutil_checkudatap (L, i, LCURL_HTTPPOST); + luaL_argcheck (L, p != NULL, 1, LCURL_HTTPPOST_NAME" object expected"); + return p; +} + +static int lcurl_hpost_to_s(lua_State *L){ + lcurl_hpost_t *p = (lcurl_hpost_t *)lutil_checkudatap (L, 1, LCURL_HTTPPOST); + lua_pushfstring(L, LCURL_HTTPPOST_NAME" (%p)", (void*)p); + return 1; +} + +static int lcurl_hpost_add_content(lua_State *L){ + // add_buffer(name, data, [type,] [headers]) + lcurl_hpost_t *p = lcurl_gethpost(L); + size_t name_len; const char *name = luaL_checklstring(L, 2, &name_len); + size_t cont_len; const char *cont = luaL_checklstring(L, 3, &cont_len); + const char *type = lua_tostring(L, 4); + struct curl_slist *list = lcurl_util_to_slist(L, type?5:4); + struct curl_forms forms[3]; + CURLFORMcode code; + + int i = 0; + if(type){ forms[i].option = CURLFORM_CONTENTTYPE; forms[i++].value = type; } + if(list){ forms[i].option = CURLFORM_CONTENTHEADER; forms[i++].value = (char*)list; } + forms[i].option = CURLFORM_END; + + code = curl_formadd(&p->post, &p->last, + CURLFORM_PTRNAME, name, CURLFORM_NAMELENGTH, (long)name_len, + CURLFORM_PTRCONTENTS, cont, LCURL_FORM_CONTENTLEN, (LCURL_LEN_TYPE)cont_len, + CURLFORM_ARRAY, forms, + CURLFORM_END); + + if(code != CURL_FORMADD_OK){ + if(list) curl_slist_free_all(list); + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_FORM, code); + } + + lcurl_storage_preserve_value(L, p->storage, 2); + lcurl_storage_preserve_value(L, p->storage, 3); + if(list) lcurl_storage_preserve_slist (L, p->storage, list); + + lua_settop(L, 1); + return 1; +} + +static int lcurl_hpost_add_buffer(lua_State *L){ + // add_buffer(name, filename, data, [type,] [headers]) + lcurl_hpost_t *p = lcurl_gethpost(L); + size_t name_len; const char *name = luaL_checklstring(L, 2, &name_len); + const char *buff = luaL_checkstring(L, 3); + size_t cont_len; const char *cont = luaL_checklstring(L, 4, &cont_len); + const char *type = lua_tostring(L, 5); + struct curl_slist *list = lcurl_util_to_slist(L, ((!type)&&(lua_isnone(L,6)))?5:6); + struct curl_forms forms[3]; + CURLFORMcode code; + + int i = 0; + if(type){ forms[i].option = CURLFORM_CONTENTTYPE; forms[i++].value = type; } + if(list){ forms[i].option = CURLFORM_CONTENTHEADER; forms[i++].value = (char*)list; } + forms[i].option = CURLFORM_END; + + code = curl_formadd(&p->post, &p->last, + CURLFORM_PTRNAME, name, CURLFORM_NAMELENGTH, (long)name_len, + CURLFORM_BUFFER, buff, + CURLFORM_BUFFERPTR, cont, CURLFORM_BUFFERLENGTH, cont_len, + CURLFORM_ARRAY, forms, + CURLFORM_END); + + if(code != CURL_FORMADD_OK){ + if(list) curl_slist_free_all(list); + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_FORM, code); + } + + lcurl_storage_preserve_value(L, p->storage, 2); + lcurl_storage_preserve_value(L, p->storage, 4); + if(list) lcurl_storage_preserve_slist (L, p->storage, list); + + lua_settop(L, 1); + return 1; +} + +static int lcurl_hpost_add_file(lua_State *L){ + // add_file(name, path, [type, [fname,]] [headers]) + // add_file("Picture", "c:\\image.jpg") + // add_file("Picture", "c:\\image.jpg", "image/jpeg") + // add_file("Picture", "c:\\image.jpg", "image/jpeg", {"XDescript: my image"}) + // add_file("Picture", "c:\\image.jpg", "image/jpeg", "avatar.jpeg", {"XDescript: my image"}) + // add_file("Picture", "c:\\image.jpg", nil, "avatar.jpeg", {"XDescript: my image"}) + + int top = lua_gettop(L); + lcurl_hpost_t *p = lcurl_gethpost(L); + size_t name_len; const char *name = luaL_checklstring(L, 2, &name_len); + const char *path = luaL_checkstring(L, 3); + const char *type = 0, *fname = 0; + struct curl_slist *list = NULL; + struct curl_forms forms[4]; + CURLFORMcode code; + int i = 0; + + if(top == 4){ /* name, path, type | headers */ + if(lua_istable(L, 4)) + list = lcurl_util_to_slist(L, 4); + else + type = lua_tostring(L, 4); + } + else if(top > 4){ /* name, path, type, fname | [fname, headers] */ + type = lua_tostring(L, 4); + if(top == 5){ /* name, path, type, fname | headers */ + if(lua_istable(L, 5)) + list = lcurl_util_to_slist(L, 5); + else + fname = lua_tostring(L, 5); + } + else{ /* name, path, type, fname, headers */ + fname = lua_tostring(L, 5); + list = lcurl_util_to_slist(L, 6); + } + } + + if(fname){ forms[i].option = CURLFORM_FILENAME; forms[i++].value = fname; } + if(type) { forms[i].option = CURLFORM_CONTENTTYPE; forms[i++].value = type; } + if(list) { forms[i].option = CURLFORM_CONTENTHEADER; forms[i++].value = (char*)list; } + forms[i].option = CURLFORM_END; + + code = curl_formadd(&p->post, &p->last, + CURLFORM_PTRNAME, name, CURLFORM_NAMELENGTH, (long)name_len, + CURLFORM_FILE, path, + CURLFORM_ARRAY, forms, + CURLFORM_END); + + if(code != CURL_FORMADD_OK){ + if(list) curl_slist_free_all(list); + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_FORM, code); + } + + lcurl_storage_preserve_value(L, p->storage, 2); + if(list) lcurl_storage_preserve_slist (L, p->storage, list); + + lua_settop(L, 1); + return 1; +} + +static int lcurl_hpost_add_stream(lua_State *L){ + static const char *EMPTY = ""; + + // add_stream(name, [filename, [type,]] [headers,] size, reader [,context]) + lcurl_hpost_t *p = lcurl_gethpost(L); + size_t name_len; const char *name = luaL_checklstring(L, 2, &name_len); + struct curl_slist *list = NULL; int ilist = 0; + const char *type = 0, *fname = 0; + size_t len; + CURLFORMcode code; + lcurl_callback_t rd = {LUA_NOREF, LUA_NOREF}; + lcurl_hpost_stream_t *stream; + int n = 0, i = 3; + struct curl_forms forms[4]; + + while(1){ // [filename, [type,]] [headers,] + if(lua_isnone(L, i)){ + lua_pushliteral(L, "stream size required"); + lua_error(L); + } + if(lua_type(L, i) == LUA_TNUMBER){ + break; + } + if(lua_type(L, i) == LUA_TTABLE){ + ilist = i++; + break; + } + else if(!fname){ + if(lua_isnil(L, i)) fname = EMPTY; + else fname = luaL_checkstring(L, i); + } + else if(!type){ + if(lua_isnil(L, i)) type = EMPTY; + else type = luaL_checkstring(L, i); + } + else{ + if(lua_isnil(L, i) && (!ilist)){ + ++i; // empty headers + break; + } + lua_pushliteral(L, "stream size required"); + lua_error(L); + } + ++i; + } + +#if defined(LCURL_INT_SIZE_64) && LCURL_CURL_VER_GE(7,46,0) + len = luaL_checkinteger(L, i); +#else + len = luaL_checklong(L, i); +#endif + + lcurl_set_callback(L, &rd, i + 1, "read"); + + luaL_argcheck(L, rd.cb_ref != LUA_NOREF, i + 1, "function expected"); + + if(ilist) list = lcurl_util_to_slist(L, ilist); + if(fname == EMPTY) fname = NULL; + if(type == EMPTY) type = NULL; + + n = 0; + if(fname){ forms[n].option = CURLFORM_FILENAME; forms[n++].value = fname; } + if(type) { forms[n].option = CURLFORM_CONTENTTYPE; forms[n++].value = type; } + if(list) { forms[n].option = CURLFORM_CONTENTHEADER; forms[n++].value = (char*)list; } + forms[n].option = CURLFORM_END; + + stream = lcurl_hpost_stream_add(L, p); + if(!stream){ + if(list) curl_slist_free_all(list); + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_FORM, CURL_FORMADD_MEMORY); + } + + stream->rd = rd; + + code = curl_formadd(&p->post, &p->last, + CURLFORM_PTRNAME, name, CURLFORM_NAMELENGTH, (long)name_len, + CURLFORM_STREAM, stream, LCURL_FORM_CONTENTLEN, (LCURL_LEN_TYPE)len, + CURLFORM_ARRAY, forms, + CURLFORM_END + ); + + if(code != CURL_FORMADD_OK){ + lcurl_hpost_stream_free_last(L, p); + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_FORM, code); + } + + lcurl_storage_preserve_value(L, p->storage, 2); + if(list) lcurl_storage_preserve_slist (L, p->storage, list); + + lua_settop(L, 1); + return 1; +} + +static int lcurl_hpost_add_files(lua_State *L){ + lcurl_hpost_t *p = lcurl_gethpost(L); + size_t name_len; const char *name = luaL_checklstring(L, 2, &name_len); + int i; int opt_count = 0; + int arr_count = lua_rawlen(L, 3); + struct curl_forms *forms; + CURLFORMcode code; + + lua_settop(L, 3); + if(lua_type(L, -1) != LUA_TTABLE){ + //! @fixme use library specific error codes + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_FORM, CURL_FORMADD_ILLEGAL_ARRAY); + } + + for(i = 1; i <= arr_count; ++i){ + int n; + lua_rawgeti(L, 3, i); + + if((lua_type(L, -1) != LUA_TTABLE) && (lua_type(L, -1) != LUA_TSTRING)){ + //! @fixme use library specific error codes + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_FORM, CURL_FORMADD_ILLEGAL_ARRAY); + } + + n = (lua_type(L, -1) == LUA_TSTRING) ? 1: lua_rawlen(L, -1); + if(n == 1) opt_count += 1; // name + else if(n == 2) opt_count += 2; // name and type + else if(n == 3) opt_count += 3; // name, type and filename + else{ + //! @fixme use library specific error codes + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_FORM, CURL_FORMADD_ILLEGAL_ARRAY); + } + + lua_pop(L, 1); + } + + if(opt_count == 0){ + lua_settop(L, 1); + return 1; + } + + forms = calloc(opt_count + 1, sizeof(struct curl_forms)); + if(!forms){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_FORM, CURL_FORMADD_MEMORY); + } + forms[opt_count].option = CURLFORM_END; + + opt_count = 0; + for(i = 1; i <= arr_count; ++i){ + int n; + + lua_rawgeti(L, 3, i); + if (lua_type(L, -1) == LUA_TSTRING){ + forms[opt_count].option = CURLFORM_FILE; forms[opt_count++].value = luaL_checkstring(L, -1); + } + else{ + n = lua_rawlen(L, -1); + lua_rawgeti(L, -1, 1); + forms[opt_count].option = CURLFORM_FILE; forms[opt_count++].value = luaL_checkstring(L, -1); + lua_pop(L, 1); + if(n > 1){ + lua_rawgeti(L, -1, 2); + forms[opt_count].option = CURLFORM_CONTENTTYPE; forms[opt_count++].value = luaL_checkstring(L, -1); + lua_pop(L, 1); + } + if(n > 2){ + lua_rawgeti(L, -1, 3); + forms[opt_count].option = CURLFORM_FILENAME; forms[opt_count++].value = luaL_checkstring(L, -1); + lua_pop(L, 1); + } + } + + lua_pop(L, 1); + } + + code = curl_formadd(&p->post, &p->last, + CURLFORM_PTRNAME, name, CURLFORM_NAMELENGTH, (long)name_len, + CURLFORM_ARRAY, forms, + CURLFORM_END); + + free(forms); + + if(code != CURL_FORMADD_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_FORM, code); + } + + lua_settop(L, 1); + return 1; +} + +static size_t lcurl_hpost_getter_by_buffer(void *arg, const char *buf, size_t len){ + luaL_Buffer *b = arg; + luaL_addlstring(b, buf, len); + return len; +} + +static size_t call_writer(lua_State *L, int fn, int ctx, const char *buf, size_t len){ + int top = lua_gettop(L); + int n = 1; // number of args + lua_Number ret = (lua_Number)len; + + lua_pushvalue(L, fn); + if(ctx){ + lua_pushvalue(L, ctx); + n += 1; + } + lua_pushlstring(L, buf, len); + + if(lua_pcall(L, n, LUA_MULTRET, 0)) return 0; + + if(lua_gettop(L) > top){ + if(lua_isnil(L, top + 1)) return 0; + if(lua_isboolean(L, top + 1)){ + if(!lua_toboolean(L, top + 1)) ret = 0; + } + else ret = lua_tonumber(L, top + 1); + } + lua_settop(L, top); + + return (size_t)ret; +} + +static size_t lcurl_hpost_getter_by_callback1(void *arg, const char *buf, size_t len){ + lua_State *L = arg; + assert(2 == lua_gettop(L)); + return call_writer(L, 2, 0, buf, len); +} + +static size_t lcurl_hpost_getter_by_callback2(void *arg, const char *buf, size_t len){ + lua_State *L = arg; + assert(3 == lua_gettop(L)); + return call_writer(L, 2, 3, buf, len); +} + +static int lcurl_hpost_get(lua_State *L){ + // get() + // get(fn [, ctx]) + // get(object) + lcurl_hpost_t *p = lcurl_gethpost(L); + CURLcode code; + int top; + + if(lua_isnoneornil(L, 2)){ + luaL_Buffer b; + luaL_buffinit(L, &b); + + code = curl_formget(p->post, &b, lcurl_hpost_getter_by_buffer); + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_CURL, code); + } + + luaL_pushresult(&b); + return 1; + } + + if(lua_isfunction(L, 2)){ + if(lua_gettop(L) == 2){ + top = 2; + code = curl_formget(p->post, L, lcurl_hpost_getter_by_callback1); + } + else{ + top = 3; + lua_settop(L, 3); + code = curl_formget(p->post, L, lcurl_hpost_getter_by_callback2); + } + } + else if(lua_isuserdata(L, 2) || lua_istable(L, 2)){ + lua_settop(L, 2); + lua_getfield(L, 2, "write"); + luaL_argcheck(L, lua_isfunction(L, -1), 2, "write method not found in object"); + assert(3 == lua_gettop(L)); + lua_insert(L, -2); + top = 3; + code = curl_formget(p->post, L, lcurl_hpost_getter_by_callback2); + } + else{ + lua_pushliteral(L, "invalid writer type"); + return lua_error(L); + } + + if(LCURL_GET_CB_ERROR == code){ + if(((lua_gettop(L) == top+1))&&(lua_isstring(L, -1))){ + return lua_error(L); + } + return lua_gettop(L) - top; + } + + if(code != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_CURL, code); + } + + lua_settop(L, 1); + return 1; +} + +static int lcurl_hpost_free(lua_State *L){ + lcurl_hpost_t *p = lcurl_gethpost(L); + if(p->post){ + curl_formfree(p->post); + p->post = p->last = 0; + } + + if(p->storage != LUA_NOREF){ + p->storage = lcurl_storage_free(L, p->storage); + } + + lcurl_hpost_stream_free_all(L, p); + + return 0; +} + +//} + +static const struct luaL_Reg lcurl_hpost_methods[] = { + {"add_content", lcurl_hpost_add_content }, + {"add_buffer", lcurl_hpost_add_buffer }, + {"add_file", lcurl_hpost_add_file }, + {"add_stream", lcurl_hpost_add_stream }, + + {"add_files", lcurl_hpost_add_files }, + + {"get", lcurl_hpost_get }, + {"free", lcurl_hpost_free }, + {"__gc", lcurl_hpost_free }, + {"__tostring", lcurl_hpost_to_s }, + + {NULL,NULL} +}; + +void lcurl_hpost_initlib(lua_State *L, int nup){ + if(!lutil_createmetap(L, LCURL_HTTPPOST, lcurl_hpost_methods, nup)) + lua_pop(L, nup); + lua_pop(L, 1); +} + diff --git a/watchdog/third_party/lua-curl/src/lchttppost.h b/watchdog/third_party/lua-curl/src/lchttppost.h new file mode 100644 index 0000000..3299069 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lchttppost.h @@ -0,0 +1,47 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#ifndef _LCHTTPPOST_H_ +#define _LCHTTPPOST_H_ + +#include "lcurl.h" +#include "lcutils.h" +#include + +#define LCURL_HPOST_STREAM_MAGIC 0xAA + +typedef struct lcurl_hpost_stream_tag{ + unsigned char magic; + + lua_State **L; + lcurl_callback_t rd; + lcurl_read_buffer_t rbuffer; + struct lcurl_hpost_stream_tag *next; +}lcurl_hpost_stream_t; + +typedef struct lcurl_hpost_tag{ + lua_State *L; + struct curl_httppost *post; + struct curl_httppost *last; + int storage; + int err_mode; + lcurl_hpost_stream_t *stream; +}lcurl_hpost_t; + +int lcurl_hpost_create(lua_State *L, int error_mode); + +void lcurl_hpost_initlib(lua_State *L, int nup); + +lcurl_hpost_t *lcurl_gethpost_at(lua_State *L, int i); + +#define lcurl_gethpost(L) lcurl_gethpost_at((L),1) + + +#endif diff --git a/watchdog/third_party/lua-curl/src/lcinfoeasy.h b/watchdog/third_party/lua-curl/src/lcinfoeasy.h new file mode 100644 index 0000000..6cbf8a9 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcinfoeasy.h @@ -0,0 +1,94 @@ +OPT_ENTRY( effective_url, EFFECTIVE_URL, STR, 0) +OPT_ENTRY( response_code, RESPONSE_CODE, LNG, 0) +OPT_ENTRY( http_connectcode, HTTP_CONNECTCODE, LNG, 0) +OPT_ENTRY( filetime, FILETIME, LNG, 0) +OPT_ENTRY( total_time, TOTAL_TIME, DBL, 0) +OPT_ENTRY( namelookup_time, NAMELOOKUP_TIME, DBL, 0) +OPT_ENTRY( connect_time, CONNECT_TIME, DBL, 0) +OPT_ENTRY( appconnect_time, APPCONNECT_TIME, DBL, 0) +OPT_ENTRY( pretransfer_time, PRETRANSFER_TIME, DBL, 0) +OPT_ENTRY( starttransfer_time, STARTTRANSFER_TIME, DBL, 0) +OPT_ENTRY( redirect_time, REDIRECT_TIME, DBL, 0) +OPT_ENTRY( redirect_count, REDIRECT_COUNT, LNG, 0) +OPT_ENTRY( redirect_url, REDIRECT_URL, STR, 0) +OPT_ENTRY( size_upload, SIZE_UPLOAD, DBL, 0) +OPT_ENTRY( size_download, SIZE_DOWNLOAD, DBL, 0) +OPT_ENTRY( speed_download, SPEED_DOWNLOAD, DBL, 0) +OPT_ENTRY( speed_upload, SPEED_UPLOAD, DBL, 0) +OPT_ENTRY( header_size, HEADER_SIZE, LNG, 0) +OPT_ENTRY( request_size, REQUEST_SIZE, LNG, 0) +OPT_ENTRY( ssl_verifyresult, SSL_VERIFYRESULT, LNG, 0) +OPT_ENTRY( ssl_engines, SSL_ENGINES, LST, 0) +OPT_ENTRY( content_length_download, CONTENT_LENGTH_DOWNLOAD, DBL, 0) +OPT_ENTRY( content_length_upload, CONTENT_LENGTH_UPLOAD, DBL, 0) +OPT_ENTRY( content_type, CONTENT_TYPE, STR, 0) +OPT_ENTRY( httpauth_avail, HTTPAUTH_AVAIL, LNG, 0) +OPT_ENTRY( proxyauth_avail, PROXYAUTH_AVAIL, LNG, 0) +OPT_ENTRY( os_errno, OS_ERRNO, LNG, 0) +OPT_ENTRY( num_connects, NUM_CONNECTS, LNG, 0) +OPT_ENTRY( primary_ip, PRIMARY_IP, STR, 0) +OPT_ENTRY( certinfo, CERTINFO, CERTINFO, 0) +#if LCURL_CURL_VER_GE(7,21,0) +OPT_ENTRY( primary_port, PRIMARY_PORT, LNG, 0) +OPT_ENTRY( local_ip, LOCAL_IP, STR, 0) +OPT_ENTRY( local_port, LOCAL_PORT, LNG, 0) +#endif +OPT_ENTRY( cookielist, COOKIELIST, LST, 0) +OPT_ENTRY( lastsocket, LASTSOCKET, LNG, 0) +OPT_ENTRY( ftp_entry_path, FTP_ENTRY_PATH, STR, 0) +OPT_ENTRY( condition_unmet, CONDITION_UNMET, LNG, 0) +#if LCURL_CURL_VER_GE(7,20,0) +OPT_ENTRY( rtsp_session_id, RTSP_SESSION_ID, STR, 0) +OPT_ENTRY( rtsp_client_cseq, RTSP_CLIENT_CSEQ, LNG, 0) +OPT_ENTRY( rtsp_server_cseq, RTSP_SERVER_CSEQ, LNG, 0) +OPT_ENTRY( rtsp_cseq_recv, RTSP_CSEQ_RECV, LNG, 0) +#endif + +#if LCURL_CURL_VER_GE(7,50,1) +OPT_ENTRY( http_version, HTTP_VERSION, LNG, 0) +#endif + +#if LCURL_CURL_VER_GE(7,52,0) +OPT_ENTRY( proxy_ssl_verifyresult, PROXY_SSL_VERIFYRESULT, LNG, 0) +OPT_ENTRY( protocol, PROTOCOL, LNG, 0) +OPT_ENTRY( scheme, SCHEME, STR, 0) +#endif + +#if LCURL_CURL_VER_GE(7,55,0) +OPT_ENTRY( content_length_download_t, CONTENT_LENGTH_DOWNLOAD_T, OFF, 0) +OPT_ENTRY( content_length_upload_t, CONTENT_LENGTH_UPLOAD_T, OFF, 0) +OPT_ENTRY( size_download_t, SIZE_DOWNLOAD_T, OFF, 0) +OPT_ENTRY( size_upload_t, SIZE_UPLOAD_T, OFF, 0) +OPT_ENTRY( speed_download_t, SPEED_DOWNLOAD_T, OFF, 0) +OPT_ENTRY( speed_upload_t, SPEED_UPLOAD_T, OFF, 0) +#endif + +#if LCURL_CURL_VER_GE(7,59,0) +OPT_ENTRY( filetime_t, FILETIME_T, OFF, 0) +#endif + +#if LCURL_CURL_VER_GE(7,61,0) +OPT_ENTRY(appconnect_time_t, APPCONNECT_TIME_T, OFF, 0) +OPT_ENTRY(connect_time_t, CONNECT_TIME_T, OFF, 0) +OPT_ENTRY(namelookup_time_t, NAMELOOKUP_TIME_T, OFF, 0) +OPT_ENTRY(pretransfer_time_t, PRETRANSFER_TIME_T, OFF, 0) +OPT_ENTRY(redirect_time_t, REDIRECT_TIME_T, OFF, 0) +OPT_ENTRY(starttransfer_time_t, STARTTRANSFER_TIME_T, OFF, 0) +OPT_ENTRY(total_time_t, TOTAL_TIME_T, OFF, 0) +#endif + +#if LCURL_CURL_VER_GE(7,66,0) +OPT_ENTRY(retry_after, RETRY_AFTER, OFF, 0) +#endif + +#if LCURL_CURL_VER_GE(7,72,0) +OPT_ENTRY(effective_method, EFFECTIVE_METHOD, STR, 0) +#endif + +#if LCURL_CURL_VER_GE(7,73,0) +OPT_ENTRY(proxy_error, PROXY_ERROR, LNG, 0) +#endif + +// OPT_ENTRY( PRIVATE, void ) +// OPT_ENTRY( TLS_SSL_PTR, struct curl_tlssessioninfo ** +// OPT_ENTRY( TLS_SESSION, struct curl_tlssessioninfo * diff --git a/watchdog/third_party/lua-curl/src/lcmime.c b/watchdog/third_party/lua-curl/src/lcmime.c new file mode 100644 index 0000000..90fd6b4 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcmime.c @@ -0,0 +1,686 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2017-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#include "lcurl.h" +#include "lcmime.h" +#include "lceasy.h" +#include "lcerror.h" +#include "lcutils.h" + +/* API Notes. + * 1. Each mime can be root or child. If mime is a child (subpart) then curl free it + * when parent mime is freed or when remove this part from parent. There no way reuse same mime. + * Its not clear is it possible use mime created by one easy handle when do preform in another. + * `m=e1:mime() e2:setopt_httpmime(m) e1:close() e2:perform()` + * + * // Attach child to root (root also can have parent) + * curl_mime_subparts(root, child); + * + * // curl free `child` and all its childs + * curl_mime_subparts(root, other_child_or_null); + * + * // forbidden + * curl_mime_free(child); + */ + +#if LCURL_CURL_VER_GE(7,56,0) + +#define LCURL_MIME_NAME LCURL_PREFIX" MIME" +static const char *LCURL_MIME = LCURL_MIME_NAME; + +#define LCURL_MIME_PART_NAME LCURL_PREFIX" MIME Part" +static const char *LCURL_MIME_PART = LCURL_MIME_PART_NAME; + +//{ Free mime and subparts + +static void lcurl_mime_part_remove_subparts(lua_State *L, lcurl_mime_part_t *p, int free_it); + +static lcurl_mime_t* lcurl_mime_part_get_subparts(lua_State *L, lcurl_mime_part_t *part){ + lcurl_mime_t *sub = NULL; + + if(LUA_NOREF != part->subpart_ref){ + lua_rawgeti(L, LCURL_LUA_REGISTRY, part->subpart_ref); + sub = lcurl_getmime_at(L, -1); + lua_pop(L, 1); + } + + return sub; +} + +static int lcurl_mime_part_reset(lua_State *L, lcurl_mime_part_t *p){ + p->part = NULL; + + luaL_unref(L, LCURL_LUA_REGISTRY, p->rd.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->rd.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->rbuffer.ref); + + p->headers_ref = p->rbuffer.ref = p->rd.cb_ref = p->rd.ud_ref = LUA_NOREF; + + /*free only if we have no parents*/ + lcurl_mime_part_remove_subparts(L, p, 0); + + return 0; +} + +static int lcurl_mime_reset(lua_State *L, lcurl_mime_t *p){ + lcurl_mime_part_t *ptr; + + /* reset all parts*/ + for(ptr = p->parts; ptr; ptr=ptr->next){ + lcurl_mime_part_reset(L, ptr); + } + + if(LUA_NOREF != p->storage){ + p->storage = lcurl_storage_free(L, p->storage); + } + + p->parts = p->parent = NULL; + p->mime = NULL; + + /* remove weak reference to easy */ + lua_pushnil(L); + lua_rawsetp(L, LCURL_MIME_EASY, p); + + return 0; +} + +static void lcurl_mime_part_remove_subparts(lua_State *L, lcurl_mime_part_t *p, int free_it){ + lcurl_mime_t *sub = lcurl_mime_part_get_subparts(L, p); + if(sub){ + assert(LUA_NOREF != p->subpart_ref); + /* detach `subpart` mime from current mime part */ + /* if set `sub->parent = NULL` then gc for mime will try free curl_mime_free. */ + + luaL_unref(L, LCURL_LUA_REGISTRY, p->subpart_ref); + p->subpart_ref = LUA_NOREF; + + if(p->part && free_it){ + curl_mime_subparts(p->part, NULL); + } + + /* seems curl_mime_subparts(h, NULL) free asubparts. + so we have to invalidate all reference to all nested objects (part/mime). + NOTE. All resources already feed. So just need set all pointers to NULL + and free all Lua resources (like references and storages) + */ + { + lcurl_mime_part_t *ptr; + /* reset all parts*/ + for(ptr = sub->parts; ptr; ptr=ptr->next){ + lcurl_mime_part_remove_subparts(L, p, 0); + } + lcurl_mime_reset(L, sub); + } + } +} + +//} + +int lcurl_mime_set_lua(lua_State *L, lcurl_mime_t *p, lua_State *v){ + lcurl_mime_part_t *part; + for(part = p->parts; part; part=part->next){ + lcurl_mime_t *sub = lcurl_mime_part_get_subparts(L, part); + if(sub) lcurl_mime_set_lua(L, sub, v); + part->L = v; + } + return 0; +} + +#define IS_NILORSTR(L, i) (lua_type(L, i) == LUA_TSTRING) || (lua_type(L, i) == LUA_TNIL) +#define IS_TABLE(L, i) lua_type(L, i) == LUA_TTABLE +#define IS_FALSE(L, i) ((lua_type(L, i) == LUA_TBOOLEAN) && (!lua_toboolean(L, i))) || lutil_is_null(L,i) +#define IS_OPTSTR(L, i) (IS_FALSE(L, i)) || (IS_NILORSTR(L, i)) + +static int lutil_isarray(lua_State *L, int i){ + int ret = 0; + i = lua_absindex(L, i); + lua_pushnil(L); + if(lua_next(L, i)){ + ret = lua_isnumber(L, -2); + lua_pop(L, 2); + } + return ret; +} + +static int lcurl_mime_part_assign(lua_State *L, int part, const char *method){ + int top = lua_gettop(L); + + lua_pushvalue(L, part); + lua_insert(L, -2); + lua_getfield(L, -2, method); + lua_insert(L, -3); + lua_call(L, 2, LUA_MULTRET); + + return lua_gettop(L) - top + 1; +} + +static const char *lcurl_mime_part_fields[] = { + "data", "filedata", "name", "filename", "headers", "encoder", "type", NULL +}; + +static int lcurl_mime_part_assing_table(lua_State *L, int part, int t){ + int top = lua_gettop(L); + const char *method; int i; + + part = lua_absindex(L, part); + t = lua_absindex(L, t); + + if(lutil_isarray(L, t)){ + int ret; + lua_pushvalue(L, t); + ret = lcurl_mime_part_assign(L, part, "headers"); + if(ret != 1) return ret; + + lua_pop(L, 1); + + assert(top == lua_gettop(L)); + } + else{ + for(i=0;method = lcurl_mime_part_fields[i]; ++i){ + lua_getfield(L, t, method); + if(!lua_isnil(L, -1)){ + int ret = lcurl_mime_part_assign(L, part, method); + if(ret != 1) return ret; + } + lua_pop(L, 1); + + assert(top == lua_gettop(L)); + } + + lua_getfield(L, t, "subparts"); + if(!lua_isnil(L, -1)){ + if(IS_FALSE(L, -1) || lcurl_getmime_at(L, -1)){ + int ret = lcurl_mime_part_assign(L, part, "subparts"); + if(ret != 1) return ret; + } + } + lua_pop(L, 1); + assert(top == lua_gettop(L)); + } + + return 0; +} + +//{ MIME + +static lcurl_mime_part_t* lcurl_mime_parts_append(lcurl_mime_t *m, lcurl_mime_part_t *p){ + if(!m->parts) m->parts = p; + else{ + lcurl_mime_part_t *ptr = m->parts; + while(ptr->next)ptr = ptr->next; + ptr->next = p; + } + return p; +} + +static lcurl_mime_part_t* lcurl_mime_parts_find(lcurl_mime_t *m, lcurl_mime_part_t *p){ + lcurl_mime_part_t *ptr; + + for(ptr = m->parts; ptr; ptr = ptr->next){ + if(ptr == p) return p; + } + + return NULL; +} + +int lcurl_mime_create(lua_State *L, int error_mode){ + //! @todo make this function as method of easy handle + lcurl_easy_t *e = lcurl_geteasy(L); + + lcurl_mime_t *p = lutil_newudatap(L, lcurl_mime_t, LCURL_MIME); + + p->mime = curl_mime_init(e->curl); + + //! @todo return more accurate error category/code + if(!p->mime) return lcurl_fail_ex(L, error_mode, LCURL_ERROR_EASY, CURLE_FAILED_INIT); + + p->storage = lcurl_storage_init(L); + p->err_mode = error_mode; + p->parts = p->parent = NULL; + + /* weak reference from mime to easy handle */ + lua_pushvalue(L, 1); + lua_rawsetp(L, LCURL_MIME_EASY, (void*)p); + + return 1; +} + +lcurl_mime_t *lcurl_getmime_at(lua_State *L, int i){ + lcurl_mime_t *p = (lcurl_mime_t *)lutil_checkudatap (L, i, LCURL_MIME); + luaL_argcheck (L, p != NULL, i, LCURL_MIME_NAME" object expected"); + luaL_argcheck (L, p->mime != NULL, i, LCURL_MIME_NAME" object freed"); + return p; +} + +static int lcurl_mime_to_s(lua_State *L){ + lcurl_mime_t *p = (lcurl_mime_t *)lutil_checkudatap (L, 1, LCURL_MIME); + luaL_argcheck (L, p != NULL, 1, LCURL_MIME_NAME" object expected"); + + lua_pushfstring(L, LCURL_MIME_NAME" (%p)%s", (void*)p, + p->mime ? (p->parent ? " (subpart)" : "") : " (freed)" + ); + return 1; +} + +static int lcurl_mime_free(lua_State *L){ + lcurl_mime_t *p = (lcurl_mime_t *)lutil_checkudatap (L, 1, LCURL_MIME); + luaL_argcheck (L, p != NULL, 1, LCURL_MIME_NAME" object expected"); + + if((p->mime) && (NULL == p->parent)){ + curl_mime_free(p->mime); + } + + return lcurl_mime_reset(L, p); +} + +static int lcurl_mime_addpart(lua_State *L){ + lcurl_mime_t *p = lcurl_getmime(L); + int ret; + + lua_settop(L, 2); + + ret = lcurl_mime_part_create(L, p->err_mode); + if(ret != 1) return ret; + + /* store mime part in storage */ + lcurl_storage_preserve_value(L, p->storage, lua_absindex(L, -1)); + lcurl_mime_parts_append(p, lcurl_getmimepart_at(L, -1)); + + if(lua_istable(L, 2)){ + ret = lcurl_mime_part_assing_table(L, 3, 2); + if(ret) return ret; + } + + return 1; +} + +static int lcurl_mime_easy(lua_State *L){ + lcurl_mime_t *p = lcurl_getmime(L); + lua_rawgetp(L, LCURL_MIME_EASY, p); + return 1; +} + +//} + +//{ MIME Part + +int lcurl_mime_part_create(lua_State *L, int error_mode){ + //! @todo make this function as method of mime handle + lcurl_mime_t *m = lcurl_getmime(L); + + lcurl_mime_part_t *p = lutil_newudatap(L, lcurl_mime_part_t, LCURL_MIME_PART); + + p->part = curl_mime_addpart(m->mime); + + //! @todo return more accurate error category/code + if(!p->part) return lcurl_fail_ex(L, error_mode, LCURL_ERROR_EASY, CURLE_FAILED_INIT); + + p->rbuffer.ref = p->rd.cb_ref = p->rd.ud_ref = LUA_NOREF; + p->err_mode = error_mode; + p->subpart_ref = p->headers_ref = LUA_NOREF; + p->parent = m; + + return 1; +} + +lcurl_mime_part_t *lcurl_getmimepart_at(lua_State *L, int i){ + lcurl_mime_part_t *p = (lcurl_mime_part_t *)lutil_checkudatap (L, i, LCURL_MIME_PART); + luaL_argcheck (L, p != NULL, i, LCURL_MIME_PART_NAME" object expected"); + luaL_argcheck (L, p->part != NULL, i, LCURL_MIME_PART_NAME" object freed"); + return p; +} + +static int lcurl_mime_part_to_s(lua_State *L){ + lcurl_mime_part_t *p = (lcurl_mime_part_t *)lutil_checkudatap (L, 1, LCURL_MIME_PART); + luaL_argcheck (L, p != NULL, 1, LCURL_MIME_PART_NAME" object expected"); + + lua_pushfstring(L, LCURL_MIME_PART_NAME" (%p)%s", (void*)p, p->part ? "" : " (freed)"); + return 1; +} + +static int lcurl_mime_part_free(lua_State *L){ + lcurl_mime_part_t *p = (lcurl_mime_part_t *)lutil_checkudatap (L, 1, LCURL_MIME_PART); + luaL_argcheck (L, p != NULL, 1, LCURL_MIME_PART_NAME" object expected"); + + lcurl_mime_part_reset(L, p); + + return 0; +} + +static int lcurl_mime_part_assing_ext(lua_State *L, int part, int i){ +#define UNSET_VALUE (const char*)-1 + + const char *mime_type = NULL, *mime_name = NULL, *mime_fname = NULL; + int headers = 0; + CURLcode ret; + lcurl_mime_part_t *p = lcurl_getmimepart_at(L, part); + + if(IS_TABLE(L, i)) headers = i; + else if (IS_OPTSTR(L, i)) { + mime_type = IS_FALSE(L, i) ? UNSET_VALUE : lua_tostring(L, i); + if(IS_TABLE(L, i+1)) headers = i+1; + else if(IS_OPTSTR(L, i+1)){ + mime_name = IS_FALSE(L, i+1) ? UNSET_VALUE : lua_tostring(L, i+1); + if(IS_TABLE(L, i+2)) headers = i+2; + else if(IS_OPTSTR(L, i+2)){ + mime_fname = IS_FALSE(L, i+2) ? UNSET_VALUE : lua_tostring(L, i+2); + if(IS_TABLE(L, i+3)) headers = i+3; + else if(IS_FALSE(L, i+3)){ + headers = -1; + } + } + } + } + + if(mime_type){ + ret = curl_mime_type(p->part, mime_type == UNSET_VALUE ? NULL : mime_type); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + } + + if(mime_name){ + ret = curl_mime_name(p->part, mime_name == UNSET_VALUE ? NULL : mime_name); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + } + + if(mime_fname){ + ret = curl_mime_filename(p->part, mime_fname == UNSET_VALUE ? NULL : mime_fname); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + } + + if(headers){ + if(-1 == headers){ + ret = curl_mime_headers(p->part, NULL, 0); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + } + else + return lcurl_mime_part_assing_table(L, part, headers); + } + + return 0; + +#undef UNSET_VALUE +} + +// part:data(str[, type[, name[, filename]]][, headers]) +static int lcurl_mime_part_data(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + size_t len; const char *data; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + data = NULL; + len = 0; + } + else{ + data = luaL_checklstring(L, 2, &len); + /*string too long*/ + if(len == CURL_ZERO_TERMINATED){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, CURLE_BAD_FUNCTION_ARGUMENT); + } + } + + /* curl_mime_data copies data */ + ret = curl_mime_data(p->part, data, len); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + if (lua_gettop(L) > 2){ + int res = lcurl_mime_part_assing_ext(L, 1, 3); + if (res) return res; + } + + lua_settop(L, 1); + return 1; +} + +// part:subparts(mime[, type[, name]][, headers]) +static int lcurl_mime_part_subparts(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + lcurl_mime_t *mime = lcurl_getmime_at(L, 2); + CURLcode ret; + + /* we can attach mime to only one part */ + if(mime->parent){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, CURLE_BAD_FUNCTION_ARGUMENT); + } + + /* if we already have one subpart then libcurl free it so we can not use any references to it */ + lcurl_mime_part_remove_subparts(L, p, 1); + + ret = curl_mime_subparts(p->part, mime->mime); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_pushvalue(L, 2); + p->subpart_ref = luaL_ref(L, LCURL_LUA_REGISTRY); + mime->parent = p; + + if (lua_gettop(L) > 2){ + int res = lcurl_mime_part_assing_ext(L, 1, 3); + if (res) return res; + } + + lua_settop(L, 1); + return 1; +} + +// part:filedata(path[, type[, name[, filename]]][, headers]) +static int lcurl_mime_part_filedata(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + const char *data = luaL_checkstring(L, 2); + CURLcode ret; + + ret = curl_mime_filedata(p->part, data); + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + if (lua_gettop(L) > 2){ + int res = lcurl_mime_part_assing_ext(L, 1, 3); + if (res) return res; + } + + lua_settop(L, 1); + return 1; +} + +// part:headers(t) +static int lcurl_mime_part_headers(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + struct curl_slist *list; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + list = NULL; + } + else{ + list = lcurl_util_to_slist(L, 2); + luaL_argcheck(L, list || IS_TABLE(L, 2), 2, "array or null expected"); + } + + ret = curl_mime_headers(p->part, list, 1); + + if(ret != CURLE_OK){ + if(list) curl_slist_free_all(list); + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_settop(L, 1); + return 1; +} + +// part:type(t) +static int lcurl_mime_part_type(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + const char *mime_type; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + mime_type = NULL; + } + else{ + mime_type = luaL_checkstring(L, 2); + } + + ret = curl_mime_type(p->part, mime_type); + + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_settop(L, 1); + return 1; +} + +// part:name(t) +static int lcurl_mime_part_name(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + const char *mime_name; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + mime_name = NULL; + } + else{ + mime_name = luaL_checkstring(L, 2); + } + ret = curl_mime_name(p->part, mime_name); + + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_settop(L, 1); + return 1; +} + +// part:filename(t) +static int lcurl_mime_part_filename(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + const char *mime_name; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + mime_name = NULL; + } + else{ + mime_name = luaL_checkstring(L, 2); + } + ret = curl_mime_filename(p->part, mime_name); + + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_settop(L, 1); + return 1; +} + +// part:encoder(t) +static int lcurl_mime_part_encoder(lua_State *L){ + lcurl_mime_part_t *p = lcurl_getmimepart(L); + const char *mime_encode; + CURLcode ret; + + if(IS_FALSE(L, 2)){ + mime_encode = NULL; + } + else{ + mime_encode = luaL_checkstring(L, 2); + } + ret = curl_mime_encoder(p->part, mime_encode); + + if(ret != CURLE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_EASY, ret); + } + + lua_settop(L, 1); + return 1; +} + +//} + +static const struct luaL_Reg lcurl_mime_methods[] = { + + {"addpart", lcurl_mime_addpart }, + {"easy", lcurl_mime_easy }, + + {"free", lcurl_mime_free }, + {"__gc", lcurl_mime_free }, + {"__tostring", lcurl_mime_to_s }, + + {NULL,NULL} +}; + +static const struct luaL_Reg lcurl_mime_part_methods[] = { + + {"subparts", lcurl_mime_part_subparts }, + {"data", lcurl_mime_part_data }, + {"filedata", lcurl_mime_part_filedata }, + {"headers", lcurl_mime_part_headers }, + {"name", lcurl_mime_part_name }, + {"filename", lcurl_mime_part_filename }, + {"type", lcurl_mime_part_type }, + {"encoder", lcurl_mime_part_encoder }, + + + {"free", lcurl_mime_part_free }, + {"__gc", lcurl_mime_part_free }, + {"__tostring", lcurl_mime_part_to_s }, + + {NULL,NULL} +}; + +static int lcurl_pushvalues(lua_State *L, int nup) { + assert(lua_gettop(L) >= nup); + + if (nup > 0) { + int b = lua_absindex(L, -nup); + int e = lua_absindex(L, -1); + int i; + + lua_checkstack(L, nup); + + for(i = b; i <= e; ++i) + lua_pushvalue(L, i); + } + + return nup; +} + +#endif + +void lcurl_mime_initlib(lua_State *L, int nup){ +#if LCURL_CURL_VER_GE(7,56,0) + lcurl_pushvalues(L, nup); + + if(!lutil_createmetap(L, LCURL_MIME, lcurl_mime_methods, nup)) + lua_pop(L, nup); + lua_pop(L, 1); + + if(!lutil_createmetap(L, LCURL_MIME_PART, lcurl_mime_part_methods, nup)) + lua_pop(L, nup); + lua_pop(L, 1); + +#else + lua_pop(L, nup); +#endif +} + diff --git a/watchdog/third_party/lua-curl/src/lcmime.h b/watchdog/third_party/lua-curl/src/lcmime.h new file mode 100644 index 0000000..6dbe9c5 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcmime.h @@ -0,0 +1,66 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2017-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#ifndef _LCMIME_H_ +#define _LCMIME_H_ + +#include "lcurl.h" +#include "lcutils.h" +#include + +void lcurl_mime_initlib(lua_State *L, int nup); + +#if LCURL_CURL_VER_GE(7,56,0) + +typedef struct lcurl_mime_part_tag{ + lua_State *L; + + lcurl_callback_t rd; + lcurl_read_buffer_t rbuffer; + + curl_mimepart *part; + + struct lcurl_mime_tag *parent; /*always set and can not be changed*/ + + int subpart_ref; + int headers_ref; + + int err_mode; + + struct lcurl_mime_part_tag *next; +}lcurl_mime_part_t; + +typedef struct lcurl_mime_tag{ + curl_mime *mime; + + int storage; + int err_mode; + + lcurl_mime_part_t *parts; + lcurl_mime_part_t *parent; /*after set there no way change it*/ +}lcurl_mime_t; + +int lcurl_mime_create(lua_State *L, int error_mode); + +lcurl_mime_t *lcurl_getmime_at(lua_State *L, int i); + +#define lcurl_getmime(L) lcurl_getmime_at((L), 1) + +int lcurl_mime_part_create(lua_State *L, int error_mode); + +lcurl_mime_part_t *lcurl_getmimepart_at(lua_State *L, int i); + +#define lcurl_getmimepart(L) lcurl_getmimepart_at((L), 1) + +int lcurl_mime_set_lua(lua_State *L, lcurl_mime_t *p, lua_State *v); + +#endif + +#endif \ No newline at end of file diff --git a/watchdog/third_party/lua-curl/src/lcmulti.c b/watchdog/third_party/lua-curl/src/lcmulti.c new file mode 100644 index 0000000..b040083 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcmulti.c @@ -0,0 +1,670 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#if defined(_WINDOWS) || defined(_WIN32) +# define LCURL_WINDOWS +#endif + +#ifdef LCURL_WINDOWS +# include +#else +# include +#endif + +#include "lcurl.h" +#include "lceasy.h" +#include "lcmulti.h" +#include "lcerror.h" +#include "lcutils.h" +#include "lchttppost.h" + +#define LCURL_MULTI_NAME LCURL_PREFIX" Multi" +static const char *LCURL_MULTI = LCURL_MULTI_NAME; + +#if defined(DEBUG) || defined(_DEBUG) +static void lcurl__multi_validate_sate(lua_State *L, lcurl_multi_t *p){ + int top = lua_gettop(L); + + lua_rawgeti(L, LCURL_LUA_REGISTRY, p->h_ref); + assert(lua_istable(L, -1)); + + lua_pushnil(L); + while(lua_next(L, -2)){ + lcurl_easy_t *e = lcurl_geteasy_at(L, -1); + void *ptr = lua_touserdata(L, -2); + + assert(e->curl == ptr); + assert(e->multi == p); + assert(e->L == p->L); + + lua_pop(L, 1); + } + + lua_pop(L, 1); + assert(lua_gettop(L) == top); +} +#else +# define lcurl__multi_validate_sate(L, p) (void*)(0) +#endif + +void lcurl__multi_assign_lua(lua_State *L, lcurl_multi_t *p, lua_State *value, int assign_easy){ + lcurl__multi_validate_sate(L, p); + + if((assign_easy)&&(p->L != value)){ + lua_rawgeti(L, LCURL_LUA_REGISTRY, p->h_ref); + lua_pushnil(L); + while(lua_next(L, -2)){ + lcurl_easy_t *e = lcurl_geteasy_at(L, -1); + lcurl__easy_assign_lua(L, e, value, 0); + lua_pop(L, 1); + } + lua_pop(L, 1); + } + + p->L = value; +} + +//{ + +int lcurl_multi_create(lua_State *L, int error_mode){ + lcurl_multi_t *p; + + lua_settop(L, 1); + + p = lutil_newudatap(L, lcurl_multi_t, LCURL_MULTI); + p->curl = curl_multi_init(); + p->err_mode = error_mode; + if(!p->curl) return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, CURLM_INTERNAL_ERROR); + p->L = NULL; + lcurl_util_new_weak_table(L, "v"); + p->h_ref = luaL_ref(L, LCURL_LUA_REGISTRY); + p->tm.cb_ref = p->tm.ud_ref = LUA_NOREF; + p->sc.cb_ref = p->sc.ud_ref = LUA_NOREF; + + if(lua_type(L, 1) == LUA_TTABLE){ + int ret = lcurl_utils_apply_options(L, 1, 2, 1, p->err_mode, LCURL_ERROR_MULTI, CURLM_UNKNOWN_OPTION); + if(ret) return ret; + assert(lua_gettop(L) == 2); + } + + return 1; +} + +lcurl_multi_t *lcurl_getmulti_at(lua_State *L, int i){ + lcurl_multi_t *p = (lcurl_multi_t *)lutil_checkudatap (L, i, LCURL_MULTI); + luaL_argcheck (L, p != NULL, 1, LCURL_MULTI_NAME" object expected"); + return p; +} + +static int lcurl_multi_to_s(lua_State *L){ + lcurl_multi_t *p = (lcurl_multi_t *)lutil_checkudatap (L, 1, LCURL_MULTI); + lua_pushfstring(L, LCURL_MULTI_NAME" (%p)", (void*)p); + return 1; +} + +static int lcurl_multi_cleanup(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + if(p->curl){ + curl_multi_cleanup(p->curl); + p->curl = NULL; + } + + if(p->h_ref != LUA_NOREF){ + lua_rawgeti(L, LCURL_LUA_REGISTRY, p->h_ref); + lua_pushnil(L); + while(lua_next(L, -2)){ + lcurl_easy_t *e = lcurl_geteasy_at(L, -1); + e->multi = NULL; + lua_pop(L, 1); + } + lua_pop(L, 1); + luaL_unref(L, LCURL_LUA_REGISTRY, p->h_ref); + p->h_ref = LUA_NOREF; + } + + luaL_unref(L, LCURL_LUA_REGISTRY, p->tm.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->tm.ud_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->sc.cb_ref); + luaL_unref(L, LCURL_LUA_REGISTRY, p->sc.ud_ref); + p->tm.cb_ref = p->tm.ud_ref = LUA_NOREF; + p->sc.cb_ref = p->sc.ud_ref = LUA_NOREF; + + lua_settop(L, 1); + lua_pushnil(L); + lua_rawset(L, LCURL_USERVALUES); + + return 0; +} + +static int lcurl_multi_add_handle(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + lcurl_easy_t *e = lcurl_geteasy_at(L, 2); + CURLMcode code; + lua_State *curL; + + if(e->multi){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, +#if LCURL_CURL_VER_GE(7,32,1) + CURLM_ADDED_ALREADY +#else + CURLM_BAD_EASY_HANDLE +#endif + ); + } + + // From doc: + // If you have CURLMOPT_TIMERFUNCTION set in the multi handle, + // that callback will be called from within this function to ask + // for an updated timer so that your main event loop will get + // the activity on this handle to get started. + // + // So we should add easy before this call + // call chain may be like => timerfunction->socket_action->socketfunction + lua_settop(L, 2); + lua_rawgeti(L, LCURL_LUA_REGISTRY, p->h_ref); + lua_pushvalue(L, 2); + lua_rawsetp(L, -2, e->curl); + lua_settop(L, 1); + + // all `esay` handles have to have same L + lcurl__easy_assign_lua(L, e, p->L, 0); + + e->multi = p; + + curL = p->L; lcurl__multi_assign_lua(L, p, L, 1); + code = curl_multi_add_handle(p->curl, e->curl); +#ifndef LCURL_RESET_NULL_LUA + if(curL != NULL) +#endif + lcurl__multi_assign_lua(L, p, curL, 1); + + if(code != CURLM_OK){ + // remove + lua_rawgeti(L, LCURL_LUA_REGISTRY, p->h_ref); + lua_pushnil(L); + lua_rawsetp(L, -2, e->curl); + e->multi = NULL; + + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, code); + } + return 1; +} + +static int lcurl_multi_remove_handle(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + lcurl_easy_t *e = lcurl_geteasy_at(L, 2); + CURLMcode code = lcurl__multi_remove_handle(L, p, e); + + if(code != CURLM_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, code); + } + + lua_settop(L, 1); + return 1; +} + +CURLMcode lcurl__multi_remove_handle(lua_State *L, lcurl_multi_t *p, lcurl_easy_t *e){ + CURLMcode code; + lua_State *curL; + + if(e->multi != p){ + // cURL returns CURLM_OK for such call so we do the same. + // tested on 7.37.1 + return CURLM_OK; + } + + curL = p->L; lcurl__multi_assign_lua(L, p, L, 1); + code = curl_multi_remove_handle(p->curl, e->curl); +#ifndef LCURL_RESET_NULL_LUA + if(curL != NULL) +#endif + lcurl__multi_assign_lua(L, p, curL, 1); + + if(code == CURLM_OK){ + e->multi = NULL; + lua_rawgeti(L, LCURL_LUA_REGISTRY, p->h_ref); + lua_pushnil(L); + lua_rawsetp(L, -2, e->curl); + lua_pop(L, 1); + } + + return code; +} + +static int lcurl_multi_perform(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + int running_handles = 0; + CURLMcode code; + lua_State *curL; + + curL = p->L; lcurl__multi_assign_lua(L, p, L, 1); + while((code = curl_multi_perform(p->curl, &running_handles)) == CURLM_CALL_MULTI_PERFORM); +#ifndef LCURL_RESET_NULL_LUA + if(curL != NULL) +#endif + lcurl__multi_assign_lua(L, p, curL, 1); + + if(code != CURLM_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, code); + } + lua_pushnumber(L, running_handles); + return 1; +} + +static int lcurl_multi_info_read(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + int msgs_in_queue = 0; + CURLMsg *msg = curl_multi_info_read(p->curl, &msgs_in_queue); + int remove = lua_toboolean(L, 2); + + lcurl_easy_t *e; + if(!msg){ + lua_pushnumber(L, msgs_in_queue); + return 1; + } + + if(msg->msg == CURLMSG_DONE){ + lua_rawgeti(L, LCURL_LUA_REGISTRY, p->h_ref); + lua_rawgetp(L, -1, msg->easy_handle); + e = lcurl_geteasy_at(L, -1); + if(remove){ + //! @fixme We ignore any errors + CURLMcode code; + lua_State *curL; + + curL = p->L; lcurl__multi_assign_lua(L, p, L, 1); + code = curl_multi_remove_handle(p->curl, e->curl); +#ifndef LCURL_RESET_NULL_LUA + if(curL != NULL) +#endif + lcurl__multi_assign_lua(L, p, curL, 1); + + if(CURLM_OK == code){ + e->multi = NULL; + lua_pushnil(L); + lua_rawsetp(L, -3, e->curl); + } + } + if(msg->data.result == CURLE_OK){ + lua_pushboolean(L, 1); + return 2; + } + return 1 + lcurl_fail_ex(L, LCURL_ERROR_RETURN, LCURL_ERROR_EASY, msg->data.result); + } + + // @todo handle unknown message + lua_pushboolean(L, 0); + return 1; +} + +static int lcurl_multi_wait(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + CURLMcode code; + int maxfd; long ms; + + if(lua_isnoneornil(L, 2)){ + code = curl_multi_timeout(p->curl, &ms); + if(code != CURLM_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, code); + } + } + else{ + ms = luaL_checklong(L, 2); + } + + if(ms < 0){ + /* if libcurl returns a -1 timeout here, it just means that libcurl + currently has no stored timeout value. You must not wait too long + (more than a few seconds perhaps) before you call + curl_multi_perform() again. + */ + ms = 1000; + } + +#if LCURL_CURL_VER_GE(7,28,0) + //! @todo supports extra_fds + code = curl_multi_wait(p->curl, 0, 0, ms, &maxfd); + if(code != CURLM_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, code); + } + lua_pushnumber(L, maxfd); + return 1; +#else + { + fd_set fdread, fdwrite, fdexcep; + + FD_ZERO(&fdread); + FD_ZERO(&fdwrite); + FD_ZERO(&fdexcep); + + code = curl_multi_fdset(p->curl, &fdread, &fdwrite, &fdexcep, &maxfd); + if(code != CURLM_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, code); + } + + //if(maxfd > 0) + { + struct timeval tv; + tv.tv_sec = ms / 1000; + tv.tv_usec = (ms % 1000) * 1000; + + maxfd = select(maxfd+1, &fdread, &fdwrite, &fdexcep, &tv); + if(maxfd < 0){ + //! @fixme return error + } + } + + lua_pushnumber(L, maxfd); + return 1; + } +#endif +} + +static int lcurl_multi_timeout(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + long n; + CURLMcode code = curl_multi_timeout(p->curl, &n); + if(code != CURLM_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, code); + } + lua_pushnumber(L, n); + return 1; +} + +static int lcurl_multi_socket_action(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + curl_socket_t s = lcurl_opt_os_socket(L, 2, CURL_SOCKET_TIMEOUT); + CURLMcode code; int n, mask; + lua_State *curL; + + if(s == CURL_SOCKET_TIMEOUT) mask = lutil_optint64(L, 3, 0); + else mask = lutil_checkint64(L, 3); + + curL = p->L; lcurl__multi_assign_lua(L, p, L, 1); + code = curl_multi_socket_action(p->curl, s, mask, &n); +#ifndef LCURL_RESET_NULL_LUA + if(curL != NULL) +#endif + lcurl__multi_assign_lua(L, p, curL, 1); + + if(code != CURLM_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, code); + } + lua_pushinteger(L, n); + return 1; +} + +//{ OPTIONS +static int lcurl_opt_set_long_(lua_State *L, int opt){ + lcurl_multi_t *p = lcurl_getmulti(L); + long val; CURLMcode code; + + if(lua_isboolean(L, 2)) val = lua_toboolean(L, 2); + else{ + luaL_argcheck(L, lua_type(L, 2) == LUA_TNUMBER, 2, "number or boolean expected"); + val = luaL_checklong(L, 2); + } + + code = curl_multi_setopt(p->curl, opt, val); + if(code != CURLM_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, code); + } + lua_settop(L, 1); + return 1; +} + +static int lcurl_opt_set_string_array_(lua_State *L, int opt){ + lcurl_multi_t *p = lcurl_getmulti(L); + CURLMcode code; + int n; + + if (lutil_is_null(L, 2)) { + n = 0; + } + else { + luaL_argcheck(L, lua_type(L, 2) == LUA_TTABLE, 2, "array expected"); + n = lua_rawlen(L, 2); + } + + if(n == 0){ + code = curl_multi_setopt(p->curl, opt, 0); + } + else{ + int i; + char const**val = malloc(sizeof(char*) * (n + 1)); + if(!val){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, CURLM_OUT_OF_MEMORY); + } + for(i = 1; i <= n; ++i){ + lua_rawgeti(L, 2, i); + val[i-1] = lua_tostring(L, -1); + lua_pop(L, 1); + } + val[n] = NULL; + code = curl_multi_setopt(p->curl, opt, val); + free((void*)val); + } + + if(code != CURLM_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, code); + } + + lua_settop(L, 1); + return 1; +} + +#define LCURL_LNG_OPT(N, S) static int lcurl_multi_set_##N(lua_State *L){\ + return lcurl_opt_set_long_(L, CURLMOPT_##N);\ +} + +#define LCURL_STR_ARR_OPT(N, S) static int lcurl_multi_set_##N(lua_State *L){\ + return lcurl_opt_set_string_array_(L, CURLMOPT_##N);\ +} + +#define OPT_ENTRY(L, N, T, S) LCURL_##T##_OPT(N, S) + +#include "lcoptmulti.h" + +#undef OPT_ENTRY +#undef LCURL_LNG_OPT +#undef LCURL_STR_ARR_OPT + +//} + +//{ CallBack + +static int lcurl_multi_set_callback(lua_State *L, + lcurl_multi_t *p, lcurl_callback_t *c, + int OPT_CB, int OPT_UD, + const char *method, void *func +) +{ + lcurl_set_callback(L, c, 2, method); + + curl_multi_setopt(p->curl, OPT_CB, (c->cb_ref == LUA_NOREF)?0:func); + curl_multi_setopt(p->curl, OPT_UD, (c->cb_ref == LUA_NOREF)?0:p); + + return 1; +} + +//{ Timer + +int lcurl_multi_timer_callback(CURLM *multi, long ms, void *arg){ + lcurl_multi_t *p = arg; + lua_State *L = p->L; + int n, top, ret = 0; + + assert(NULL != p->L); + + top = lua_gettop(L); + n = lcurl_util_push_cb(L, &p->tm); + + lua_pushnumber(L, ms); + if(lua_pcall(L, n, LUA_MULTRET, 0)){ + assert(lua_gettop(L) >= top); + lua_settop(L, top); //! @todo + // lua_pushlightuserdata(L, (void*)LCURL_ERROR_TAG); + // lua_insert(L, top+1); + return -1; + } + + if(lua_gettop(L) > top){ + if(lua_isnil(L, top + 1)){ + lua_settop(L, top); + return -1; + } + + if(lua_isboolean(L, top + 1)) + ret = lua_toboolean(L, top + 1)?0:-1; + else ret = lua_tointeger(L, top + 1); + } + + lua_settop(L, top); + return ret; +} + +static int lcurl_multi_set_TIMERFUNCTION(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + return lcurl_multi_set_callback(L, p, &p->tm, + CURLMOPT_TIMERFUNCTION, CURLMOPT_TIMERDATA, + "timer", lcurl_multi_timer_callback + ); +} + +//} + +//{ Socket + +static int lcurl_multi_socket_callback(CURL *easy, curl_socket_t s, int what, void *arg, void *socketp){ + lcurl_multi_t *p = arg; + lua_State *L = p->L; + lcurl_easy_t *e; + int n, top; + + assert(NULL != p->L); + + top = lua_gettop(L); + n = lcurl_util_push_cb(L, &p->sc); + + lua_rawgeti(L, LCURL_LUA_REGISTRY, p->h_ref); + lua_rawgetp(L, -1, easy); + e = lcurl_geteasy_at(L, -1); + lua_remove(L, -2); + lcurl_push_os_socket(L, s); + lua_pushinteger(L, what); + + if(lua_pcall(L, n+2, 0, 0)){ + assert(lua_gettop(L) >= top); + lua_settop(L, top); + return -1; //! @todo break perform + } + + lua_settop(L, top); + return 0; +} + +static int lcurl_multi_set_SOCKETFUNCTION(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + return lcurl_multi_set_callback(L, p, &p->sc, + CURLMOPT_SOCKETFUNCTION, CURLMOPT_SOCKETDATA, + "socket", lcurl_multi_socket_callback + ); +} + +//} + +//} + +static int lcurl_multi_setopt(lua_State *L){ + lcurl_multi_t *p = lcurl_getmulti(L); + int opt; + + luaL_checkany(L, 2); + + if(lua_type(L, 2) == LUA_TTABLE){ + int ret = lcurl_utils_apply_options(L, 2, 1, 0, p->err_mode, LCURL_ERROR_MULTI, CURLM_UNKNOWN_OPTION); + if(ret) return ret; + lua_settop(L, 1); + return 1; + } + + opt = luaL_checklong(L, 2); + lua_remove(L, 2); + +#define OPT_ENTRY(l, N, T, S) case CURLMOPT_##N: return lcurl_multi_set_##N(L); + switch(opt){ + #include "lcoptmulti.h" + OPT_ENTRY(timerfunction, TIMERFUNCTION, TTT, 0) + OPT_ENTRY(socketfunction, SOCKETFUNCTION, TTT, 0) + } +#undef OPT_ENTRY + + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_MULTI, CURLM_UNKNOWN_OPTION); +} + +static int lcurl_multi_setdata(lua_State *L){ + lua_settop(L, 2); + lua_pushvalue(L, 1); + lua_insert(L, 2); + lua_rawset(L, LCURL_USERVALUES); + return 1; +} + +static int lcurl_multi_getdata(lua_State *L){ + lua_settop(L, 1); + lua_rawget(L, LCURL_USERVALUES); + return 1; +} + +//} + +static const struct luaL_Reg lcurl_multi_methods[] = { + {"add_handle", lcurl_multi_add_handle }, + {"remove_handle", lcurl_multi_remove_handle }, + {"perform", lcurl_multi_perform }, + {"info_read", lcurl_multi_info_read }, + {"setopt", lcurl_multi_setopt }, + {"wait", lcurl_multi_wait }, + {"timeout", lcurl_multi_timeout }, + {"socket_action", lcurl_multi_socket_action }, + { "__tostring", lcurl_multi_to_s }, + +#define OPT_ENTRY(L, N, T, S) { "setopt_"#L, lcurl_multi_set_##N }, + #include "lcoptmulti.h" + OPT_ENTRY(timerfunction, TIMERFUNCTION, TTT, 0) + OPT_ENTRY(socketfunction, SOCKETFUNCTION, TTT, 0) +#undef OPT_ENTRY + + { "setdata", lcurl_multi_setdata }, + { "getdata", lcurl_multi_getdata }, + + {"close", lcurl_multi_cleanup }, + {"__gc", lcurl_multi_cleanup }, + + {NULL,NULL} +}; + +static const lcurl_const_t lcurl_multi_opt[] = { +#define OPT_ENTRY(L, N, T, S) { "OPT_MULTI_"#N, CURLMOPT_##N }, + #include "lcoptmulti.h" + OPT_ENTRY(timerfunction, TIMERFUNCTION, TTT, 0) + OPT_ENTRY(socketfunction, SOCKETFUNCTION, TTT, 0) +#undef OPT_ENTRY + + {NULL, 0} +}; + +void lcurl_multi_initlib(lua_State *L, int nup){ + if(!lutil_createmetap(L, LCURL_MULTI, lcurl_multi_methods, nup)) + lua_pop(L, nup); + lua_pop(L, 1); + + lcurl_util_set_const(L, lcurl_multi_opt); +} diff --git a/watchdog/third_party/lua-curl/src/lcmulti.h b/watchdog/third_party/lua-curl/src/lcmulti.h new file mode 100644 index 0000000..89c8723 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcmulti.h @@ -0,0 +1,50 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#ifndef _LCMULTI_H_ +#define _LCMULTI_H_ + +#include "lcurl.h" +#include "lcutils.h" + +typedef struct lcurl_multi_tag{ + CURLM *curl; + lua_State *L; + int err_mode; + int h_ref; + lcurl_callback_t tm; + lcurl_callback_t sc; +}lcurl_multi_t; + + +#if LCURL_CC_SUPPORT_FORWARD_TYPEDEF +typedef struct lcurl_multi_tag lcurl_multi_t; +#else +struct lcurl_easy_tag; +#define lcurl_easy_t struct lcurl_easy_tag +#endif + +int lcurl_multi_create(lua_State *L, int error_mode); + +lcurl_multi_t *lcurl_getmulti_at(lua_State *L, int i); + +#define lcurl_getmulti(L) lcurl_getmulti_at((L),1) + +void lcurl_multi_initlib(lua_State *L, int nup); + +void lcurl__multi_assign_lua(lua_State *L, lcurl_multi_t *p, lua_State *value, int assign_easy); + +CURLMcode lcurl__multi_remove_handle(lua_State *L, lcurl_multi_t *p, lcurl_easy_t *e); + +#if !LCURL_CC_SUPPORT_FORWARD_TYPEDEF +#undef lcurl_easy_t +#endif + +#endif diff --git a/watchdog/third_party/lua-curl/src/lcopteasy.h b/watchdog/third_party/lua-curl/src/lcopteasy.h new file mode 100644 index 0000000..8155667 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcopteasy.h @@ -0,0 +1,557 @@ +/* Before version 7.17.0, strings were not copied. + Instead the user was forced keep them available + until libcurl no longer needed them. +*/ + +#ifndef LCURL_STORE_STRING +# if LCURL_CURL_VER_GE(7,17,0) +# define LCURL_STORE_STRING 0 +# else +# define LCURL_STORE_STRING 1 +# endif +#endif + +#ifndef OPT_ENTRY +# define OPT_ENTRY(a,b,c,d,e) +# define OPT_ENTRY_IS_NULL +#endif + +#ifndef FLG_ENTRY +# define FLG_ENTRY(a) +# define FLG_ENTRY_IS_NULL +#endif + +#ifndef LCURL_DEFAULT_VALUE +# define LCURL_DEFAULT_VALUE 0 +#endif + +//{ Reset system macros + +#ifdef TCP_FASTOPEN +# define LCURL__TCP_FASTOPEN TCP_FASTOPEN +# undef TCP_FASTOPEN +#endif + +#ifdef TCP_KEEPIDLE +# define LCURL__TCP_KEEPIDLE TCP_KEEPIDLE +# undef TCP_KEEPIDLE +#endif + +#ifdef TCP_KEEPINTVL +# define LCURL__TCP_KEEPINTVL TCP_KEEPINTVL +# undef TCP_KEEPINTVL +#endif + +#ifdef TCP_NODELAY +# define LCURL__TCP_NODELAY TCP_NODELAY +# undef TCP_NODELAY +#endif + +#ifdef TCP_KEEPALIVE +# define LCURL__TCP_KEEPALIVE TCP_KEEPALIVE +# undef TCP_KEEPALIVE +#endif + +#ifdef BUFFERSIZE +# define LCURL__BUFFERSIZE BUFFERSIZE +# undef BUFFERSIZE +#endif + +#ifdef INTERFACE +# define LCURL__INTERFACE INTERFACE +# undef INTERFACE +#endif + +//} + +OPT_ENTRY( verbose, VERBOSE, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( header, HEADER, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( noprogress, NOPROGRESS, LNG, 0, 1 ) +OPT_ENTRY( nosignal, NOSIGNAL, LNG, 0, LCURL_DEFAULT_VALUE ) +#if LCURL_CURL_VER_GE(7,21,0) +OPT_ENTRY( wildcardmatch, WILDCARDMATCH, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif + +OPT_ENTRY( url, URL, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( failonerror, FAILONERROR, LNG, 0, LCURL_DEFAULT_VALUE ) + +OPT_ENTRY( protocols, PROTOCOLS, LNG, 0, CURLPROTO_ALL ) +OPT_ENTRY( redir_protocols, REDIR_PROTOCOLS, LNG, 0, CURLPROTO_ALL ) /*! @fixme All protocols except for FILE and SCP */ +OPT_ENTRY( proxy, PROXY, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( proxyport, PROXYPORT, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( proxytype, PROXYTYPE, LNG, 0, CURLPROXY_HTTP ) +OPT_ENTRY( noproxy, NOPROXY, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( httpproxytunnel, HTTPPROXYTUNNEL, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( socks5_gssapi_service, SOCKS5_GSSAPI_SERVICE, STR, LCURL_STORE_STRING, "rcmd/server-fqdn" ) +OPT_ENTRY( socks5_gssapi_nec, SOCKS5_GSSAPI_NEC, LNG, 0, LCURL_DEFAULT_VALUE ) /*! @check doc says nothing */ +OPT_ENTRY( interface, INTERFACE, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( localport, LOCALPORT, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( localportrange, LOCALPORTRANGE, LNG, 0, 1 ) +OPT_ENTRY( dns_cache_timeout, DNS_CACHE_TIMEOUT, LNG, 0, 60 ) + +#if !LCURL_CURL_VER_GE(7,65,0) +OPT_ENTRY( dns_use_global_cache, DNS_USE_GLOBAL_CACHE, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif + +#if LCURL_CURL_VER_GE(7,25,0) +OPT_ENTRY( dns_servers, DNS_SERVERS, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +#endif +OPT_ENTRY( buffersize, BUFFERSIZE, LNG, 0, CURL_MAX_WRITE_SIZE ) +OPT_ENTRY( port, PORT, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( tcp_nodelay, TCP_NODELAY, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( address_scope, ADDRESS_SCOPE, LNG, 0, LCURL_DEFAULT_VALUE ) +#if LCURL_CURL_VER_GE(7,25,0) +OPT_ENTRY( tcp_keepalive, TCP_KEEPALIVE, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( tcp_keepidle, TCP_KEEPIDLE, LNG, 0, LCURL_DEFAULT_VALUE ) /*! @check doc says nothing */ +OPT_ENTRY( tcp_keepintvl, TCP_KEEPINTVL, LNG, 0, LCURL_DEFAULT_VALUE ) /*! @check doc says nothing */ +#endif + +OPT_ENTRY( netrc, NETRC, LNG, 0, CURL_NETRC_IGNORED ) +OPT_ENTRY( netrc_file, NETRC_FILE, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( userpwd, USERPWD, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( proxyuserpwd, PROXYUSERPWD, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( username, USERNAME, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( password, PASSWORD, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +#if LCURL_CURL_VER_GE(7,31,0) +OPT_ENTRY( login_options, LOGIN_OPTIONS, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +#endif +OPT_ENTRY( proxyusername, PROXYUSERNAME, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( proxypassword, PROXYPASSWORD, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( httpauth, HTTPAUTH, LNG, 0, CURLAUTH_BASIC ) +#if LCURL_CURL_VER_GE(7,21,4) +OPT_ENTRY( tlsauth_username, TLSAUTH_USERNAME, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( tlsauth_password, TLSAUTH_PASSWORD, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( tlsauth_type, TLSAUTH_TYPE, STR, 0, "" ) +#endif +OPT_ENTRY( proxyauth, PROXYAUTH, LNG, 0, CURLAUTH_BASIC ) +#if LCURL_CURL_VER_GE(7,31,0) +OPT_ENTRY( sasl_ir, SASL_IR, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,33,0) +OPT_ENTRY( xoauth2_bearer, XOAUTH2_BEARER, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +#endif + +OPT_ENTRY( autoreferer, AUTOREFERER, LNG, 0, LCURL_DEFAULT_VALUE ) +#if LCURL_CURL_VER_GE(7,21,6) +OPT_ENTRY( accept_encoding, ACCEPT_ENCODING, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( transfer_encoding, TRANSFER_ENCODING, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif +OPT_ENTRY( followlocation, FOLLOWLOCATION, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( unrestricted_auth, UNRESTRICTED_AUTH, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( maxredirs, MAXREDIRS, LNG, 0, -1 ) +OPT_ENTRY( postredir, POSTREDIR, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( put, PUT, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( post, POST, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( referer, REFERER, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( useragent, USERAGENT, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +#if LCURL_CURL_VER_GE(7,37,0) +OPT_ENTRY( headeropt, HEADEROPT, LNG, 0, CURLHEADER_UNIFIED ) +#endif +OPT_ENTRY( httpheader, HTTPHEADER, LST, 0, LCURL_DEFAULT_VALUE ) +#if LCURL_CURL_VER_GE(7,37,0) +OPT_ENTRY( proxyheader, PROXYHEADER, LST, 0, LCURL_DEFAULT_VALUE ) +#endif +OPT_ENTRY( http200aliases, HTTP200ALIASES, LST, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( cookie, COOKIE, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( cookiefile, COOKIEFILE, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( cookiejar, COOKIEJAR, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( cookiesession, COOKIESESSION, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( cookielist, COOKIELIST, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( httpget, HTTPGET, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( http_version, HTTP_VERSION, LNG, 0, CURL_HTTP_VERSION_NONE ) +OPT_ENTRY( ignore_content_length, IGNORE_CONTENT_LENGTH, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( http_content_decoding, HTTP_CONTENT_DECODING, LNG, 0, 1 ) +OPT_ENTRY( http_transfer_decoding, HTTP_TRANSFER_DECODING, LNG, 0, 1 ) +#if LCURL_CURL_VER_GE(7,36,0) +OPT_ENTRY( expect_100_timeout_ms, EXPECT_100_TIMEOUT_MS, LNG, 0, 1000 ) +#endif + +#if LCURL_CURL_VER_GE(7,20,0) +OPT_ENTRY( mail_from, MAIL_FROM, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) /*! @check doc says `blank` */ +OPT_ENTRY( mail_rcpt, MAIL_RCPT, LST, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,25,0) +OPT_ENTRY( mail_auth, MAIL_AUTH, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +#endif + +OPT_ENTRY( tftp_blksize, TFTP_BLKSIZE, LNG, 0, 512 ) + +OPT_ENTRY( ftpport, FTPPORT, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( quote, QUOTE, LST, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( postquote, POSTQUOTE, LST, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( prequote, PREQUOTE, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( dirlistonly, DIRLISTONLY, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( append, APPEND, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( ftp_use_eprt, FTP_USE_EPRT, LNG, 0, LCURL_DEFAULT_VALUE )/*! @check doc says nothing */ +OPT_ENTRY( ftp_use_epsv, FTP_USE_EPSV, LNG, 0, 1 ) +#if LCURL_CURL_VER_GE(7,20,0) +OPT_ENTRY( ftp_use_pret, FTP_USE_PRET, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif +OPT_ENTRY( ftp_create_missing_dirs, FTP_CREATE_MISSING_DIRS, LNG, 0, CURLFTP_CREATE_DIR_NONE ) +OPT_ENTRY( ftp_response_timeout, FTP_RESPONSE_TIMEOUT, LNG, 0, LCURL_DEFAULT_VALUE ) /*! @fixme doc says `None` */ +OPT_ENTRY( ftp_alternative_to_user, FTP_ALTERNATIVE_TO_USER, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( ftp_skip_pasv_ip, FTP_SKIP_PASV_IP, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( ftpsslauth, FTPSSLAUTH, LNG, 0, CURLFTPAUTH_DEFAULT ) +OPT_ENTRY( ftp_ssl_ccc, FTP_SSL_CCC, LNG, 0, CURLFTPSSL_CCC_NONE ) +OPT_ENTRY( ftp_account, FTP_ACCOUNT, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( ftp_filemethod, FTP_FILEMETHOD, LNG, 0, CURLFTPMETHOD_MULTICWD ) + +OPT_ENTRY( transfertext, TRANSFERTEXT, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( proxy_transfer_mode, PROXY_TRANSFER_MODE, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( crlf, CRLF, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( range, RANGE, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( resume_from, RESUME_FROM, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( resume_from_large, RESUME_FROM_LARGE, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( customrequest, CUSTOMREQUEST, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( filetime, FILETIME, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( nobody, NOBODY, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( infilesize, INFILESIZE, LNG, 0, LCURL_DEFAULT_VALUE )/*! @fixme doc says `Unset` */ +OPT_ENTRY( infilesize_large, INFILESIZE_LARGE, LNG, 0, LCURL_DEFAULT_VALUE )/*! @fixme doc says `Unset` */ +OPT_ENTRY( upload, UPLOAD, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( maxfilesize, MAXFILESIZE, LNG, 0, LCURL_DEFAULT_VALUE ) /*! @fixme doc says `None` */ +OPT_ENTRY( maxfilesize_large, MAXFILESIZE_LARGE, LNG, 0, LCURL_DEFAULT_VALUE ) /*! @fixme doc says `None` */ +OPT_ENTRY( timecondition, TIMECONDITION, LNG, 0, CURL_TIMECOND_NONE ) +OPT_ENTRY( timevalue, TIMEVALUE, LNG, 0, LCURL_DEFAULT_VALUE ) + +OPT_ENTRY( timeout, TIMEOUT, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( timeout_ms, TIMEOUT_MS, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( low_speed_limit, LOW_SPEED_LIMIT, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( low_speed_time, LOW_SPEED_TIME, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( max_send_speed_large, MAX_SEND_SPEED_LARGE, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( max_recv_speed_large, MAX_RECV_SPEED_LARGE, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( maxconnects, MAXCONNECTS, LNG, 0, 5 ) +OPT_ENTRY( fresh_connect, FRESH_CONNECT, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( forbid_reuse, FORBID_REUSE, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( connecttimeout, CONNECTTIMEOUT, LNG, 0, 300 ) +OPT_ENTRY( connecttimeout_ms, CONNECTTIMEOUT_MS, LNG, 0, 300000 ) +OPT_ENTRY( ipresolve, IPRESOLVE, LNG, 0, CURL_IPRESOLVE_WHATEVER ) +OPT_ENTRY( connect_only, CONNECT_ONLY, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( use_ssl, USE_SSL, LNG, 0, CURLUSESSL_NONE ) +#if LCURL_CURL_VER_GE(7,21,3) +OPT_ENTRY( resolve, RESOLVE, LST, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,33,0) +OPT_ENTRY( dns_interface, DNS_INTERFACE, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( dns_local_ip4, DNS_LOCAL_IP4, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( dns_local_ip6, DNS_LOCAL_IP6, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( accepttimeout_ms, ACCEPTTIMEOUT_MS, LNG, 0, 60000 ) +#endif + +OPT_ENTRY( ssh_auth_types, SSH_AUTH_TYPES, LNG, 0, LCURL_DEFAULT_VALUE) /*! @fixme doc says `None` */ +OPT_ENTRY( ssh_host_public_key_md5, SSH_HOST_PUBLIC_KEY_MD5, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( ssh_public_keyfile, SSH_PUBLIC_KEYFILE, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( ssh_private_keyfile, SSH_PRIVATE_KEYFILE, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( ssh_knownhosts, SSH_KNOWNHOSTS, STR, 0, LCURL_DEFAULT_VALUE) + +OPT_ENTRY( new_file_perms, NEW_FILE_PERMS, LNG, 0, 0644) +OPT_ENTRY( new_directory_perms, NEW_DIRECTORY_PERMS, LNG, 0, 0755) + +OPT_ENTRY( telnetoptions, TELNETOPTIONS, LST, 0, LCURL_DEFAULT_VALUE) + +OPT_ENTRY( random_file, RANDOM_FILE, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( egdsocket, EGDSOCKET, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( issuercert, ISSUERCERT, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( krblevel, KRBLEVEL, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) + +OPT_ENTRY( cainfo, CAINFO, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) /*! @fixme doc says `Built-in system specific` */ +OPT_ENTRY( capath, CAPATH, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( certinfo, CERTINFO, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( crlfile, CRLFILE, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) + +OPT_ENTRY( sslcert, SSLCERT, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( sslcerttype, SSLCERTTYPE, STR, LCURL_STORE_STRING, "PEM" ) +OPT_ENTRY( sslengine, SSLENGINE, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( sslengine_default, SSLENGINE_DEFAULT, LNG, 0, LCURL_DEFAULT_VALUE ) /*! @fixme doc says `None` */ +OPT_ENTRY( sslkey, SSLKEY, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( sslkeytype, SSLKEYTYPE, STR, LCURL_STORE_STRING, "PEM" ) +OPT_ENTRY( sslversion, SSLVERSION, LNG, 0, CURL_SSLVERSION_DEFAULT ) +OPT_ENTRY( ssl_cipher_list, SSL_CIPHER_LIST, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +#if LCURL_CURL_VER_GE(7,36,0) +OPT_ENTRY( ssl_enable_alpn, SSL_ENABLE_ALPN, LNG, 0, 1 ) +OPT_ENTRY( ssl_enable_npn, SSL_ENABLE_NPN, LNG, 0, 1 ) +#endif +#if LCURL_CURL_VER_GE(7,25,0) +OPT_ENTRY( ssl_options, SSL_OPTIONS, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif +OPT_ENTRY( ssl_sessionid_cache, SSL_SESSIONID_CACHE, LNG, 0, 1 ) +OPT_ENTRY( ssl_verifyhost, SSL_VERIFYHOST, LNG, 0, 2 ) +OPT_ENTRY( ssl_verifypeer, SSL_VERIFYPEER, LNG, 0, 1 ) +OPT_ENTRY( keypasswd, KEYPASSWD, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) + +#if LCURL_CURL_VER_GE(7,20,0) +OPT_ENTRY( rtsp_client_cseq, RTSP_CLIENT_CSEQ, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( rtsp_request, RTSP_REQUEST, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( rtsp_server_cseq, RTSP_SERVER_CSEQ, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( rtsp_session_id, RTSP_SESSION_ID, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( rtsp_stream_uri, RTSP_STREAM_URI, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( rtsp_transport, RTSP_TRANSPORT, STR, LCURL_STORE_STRING, LCURL_DEFAULT_VALUE ) +#endif + +#if LCURL_CURL_VER_GE(7,22,0) +OPT_ENTRY( gssapi_delegation, GSSAPI_DELEGATION, LNG, 0, CURLGSSAPI_DELEGATION_NONE ) +#endif + +FLG_ENTRY( SSLVERSION_DEFAULT ) +FLG_ENTRY( SSLVERSION_TLSv1 ) +FLG_ENTRY( SSLVERSION_SSLv2 ) +FLG_ENTRY( SSLVERSION_SSLv3 ) +#if LCURL_CURL_VER_GE(7,34,0) +FLG_ENTRY( SSLVERSION_TLSv1_0 ) +FLG_ENTRY( SSLVERSION_TLSv1_1 ) +FLG_ENTRY( SSLVERSION_TLSv1_2 ) +#endif +#if LCURL_CURL_VER_GE(7,52,0) +FLG_ENTRY( SSLVERSION_TLSv1_3 ) +#endif + +#if LCURL_CURL_VER_GE(7,54,0) +FLG_ENTRY( SSLVERSION_MAX_NONE ) +FLG_ENTRY( SSLVERSION_MAX_DEFAULT ) +FLG_ENTRY( SSLVERSION_MAX_TLSv1_0 ) +FLG_ENTRY( SSLVERSION_MAX_TLSv1_1 ) +FLG_ENTRY( SSLVERSION_MAX_TLSv1_2 ) +FLG_ENTRY( SSLVERSION_MAX_TLSv1_3 ) +#endif + +#if LCURL_CURL_VER_GE(7,21,4) +FLG_ENTRY( TLSAUTH_SRP ) +#endif + +FLG_ENTRY( HTTP_VERSION_NONE ) +FLG_ENTRY( HTTP_VERSION_1_0 ) +FLG_ENTRY( HTTP_VERSION_1_1 ) +#if LCURL_CURL_VER_GE(7,33,0) +FLG_ENTRY( HTTP_VERSION_2_0 ) +#endif +#if LCURL_CURL_VER_GE(7,43,0) +FLG_ENTRY( HTTP_VERSION_2 ) +#endif +#if LCURL_CURL_VER_GE(7,47,0) +FLG_ENTRY( HTTP_VERSION_2TLS ) +#endif +#if LCURL_CURL_VER_GE(7,49,0) +FLG_ENTRY( HTTP_VERSION_2_PRIOR_KNOWLEDGE ) +#endif +#if LCURL_CURL_VER_GE(7,66,0) +FLG_ENTRY( HTTP_VERSION_3 ) +#endif + +FLG_ENTRY( READFUNC_PAUSE ) /*7.18.0*/ +FLG_ENTRY( WRITEFUNC_PAUSE ) /*7.18.0*/ + +FLG_ENTRY( POLL_IN ) /*7.14.0*/ +FLG_ENTRY( POLL_INOUT ) /*7.14.0*/ +FLG_ENTRY( POLL_NONE ) /*7.14.0*/ +FLG_ENTRY( POLL_OUT ) /*7.14.0*/ +FLG_ENTRY( POLL_REMOVE ) /*7.14.0*/ +FLG_ENTRY( SOCKET_TIMEOUT ) /*7.14.0*/ + +FLG_ENTRY( CSELECT_ERR ) /*7.16.3*/ +FLG_ENTRY( CSELECT_IN ) /*7.16.3*/ +FLG_ENTRY( CSELECT_OUT ) /*7.16.3*/ + +FLG_ENTRY( IPRESOLVE_WHATEVER ) /*7.10.8*/ +FLG_ENTRY( IPRESOLVE_V4 ) /*7.10.8*/ +FLG_ENTRY( IPRESOLVE_V6 ) /*7.10.8*/ + +#if LCURL_CURL_VER_GE(7,20,0) +FLG_ENTRY( RTSPREQ_OPTIONS ) +FLG_ENTRY( RTSPREQ_DESCRIBE ) +FLG_ENTRY( RTSPREQ_ANNOUNCE ) +FLG_ENTRY( RTSPREQ_SETUP ) +FLG_ENTRY( RTSPREQ_PLAY ) +FLG_ENTRY( RTSPREQ_PAUSE ) +FLG_ENTRY( RTSPREQ_TEARDOWN ) +FLG_ENTRY( RTSPREQ_GET_PARAMETER ) +FLG_ENTRY( RTSPREQ_SET_PARAMETER ) +FLG_ENTRY( RTSPREQ_RECORD ) +FLG_ENTRY( RTSPREQ_RECEIVE ) +#endif + +#if LCURL_CURL_VER_GE(7,39,0) +OPT_ENTRY( pinnedpublickey, PINNEDPUBLICKEY, STR, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,40,0) +OPT_ENTRY( unix_socket_path, UNIX_SOCKET_PATH, STR, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,41,0) +OPT_ENTRY( ssl_verifystatus, SSL_VERIFYSTATUS, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,42,0) +OPT_ENTRY( ssl_falsestart, SSL_FALSESTART, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( path_as_is, PATH_AS_IS, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,43,0) +OPT_ENTRY( proxy_service_name, PROXY_SERVICE_NAME, STR, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( service_name, SERVICE_NAME, STR, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( pipewait, PIPEWAIT, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,45,0) +OPT_ENTRY( default_protocol, DEFAULT_PROTOCOL, STR, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,46,0) +OPT_ENTRY( stream_weight, STREAM_WEIGHT, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,48,0) +OPT_ENTRY( tftp_no_options, TFTP_NO_OPTIONS, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,49,0) +OPT_ENTRY( tcp_fastopen, TCP_FASTOPEN, LNG, 0, LCURL_DEFAULT_VALUE ) +OPT_ENTRY( connect_to, CONNECT_TO, LST, 0, LCURL_DEFAULT_VALUE ) +#endif +#if LCURL_CURL_VER_GE(7,51,0) +OPT_ENTRY( keep_sending_on_error, KEEP_SENDING_ON_ERROR, LNG, 0, LCURL_DEFAULT_VALUE ) +#endif + +#if LCURL_CURL_VER_GE(7,52,0) +OPT_ENTRY( proxy_cainfo, PROXY_CAINFO, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( proxy_capath, PROXY_CAPATH, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( proxy_ssl_verifypeer, PROXY_SSL_VERIFYPEER, LNG, 0, 1) +OPT_ENTRY( proxy_ssl_verifyhost, PROXY_SSL_VERIFYHOST, LNG, 0, 2) +OPT_ENTRY( proxy_sslversion, PROXY_SSLVERSION, LNG, 0, CURL_SSLVERSION_DEFAULT) +OPT_ENTRY( proxy_tlsauth_username, PROXY_TLSAUTH_USERNAME, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( proxy_tlsauth_password, PROXY_TLSAUTH_PASSWORD, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( proxy_tlsauth_type, PROXY_TLSAUTH_TYPE, STR, 0, "") +OPT_ENTRY( proxy_sslcert, PROXY_SSLCERT, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( proxy_sslcerttype, PROXY_SSLCERTTYPE, STR, 0, "PEM") +OPT_ENTRY( proxy_sslkey, PROXY_SSLKEY, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( proxy_sslkeytype, PROXY_SSLKEYTYPE, STR, 0, "PEM") /* default value not defined. Use same as for `SSLKEYTYPE` */ +OPT_ENTRY( proxy_keypasswd, PROXY_KEYPASSWD, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( proxy_ssl_cipher_list, PROXY_SSL_CIPHER_LIST, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( proxy_crlfile, PROXY_CRLFILE, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( proxy_ssl_options, PROXY_SSL_OPTIONS, LNG, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( pre_proxy, PRE_PROXY, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( proxy_pinnedpublickey, PROXY_PINNEDPUBLICKEY, STR, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,53,0) +OPT_ENTRY( abstract_unix_socket, ABSTRACT_UNIX_SOCKET, STR, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,54,0) +OPT_ENTRY( suppress_connect_headers, SUPPRESS_CONNECT_HEADERS, LNG, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,55,0) +OPT_ENTRY( request_target, REQUEST_TARGET, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY( socks5_auth, SOCKS5_AUTH, LNG, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,56,0) +OPT_ENTRY( ssh_compression, SSH_COMPRESSION, LNG, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,59,0) +OPT_ENTRY( happy_eyeballs_timeout_ms,HAPPY_EYEBALLS_TIMEOUT_MS,LNG, 0, CURL_HET_DEFAULT) +OPT_ENTRY( timevalue_large, TIMEVALUE_LARGE ,OFF, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,60,0) +OPT_ENTRY(dns_shuffle_addresses, DNS_SHUFFLE_ADDRESSES, LNG, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY(haproxyprotocol, HAPROXYPROTOCOL, LNG, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,61,0) +OPT_ENTRY(disallow_username_in_url, DISALLOW_USERNAME_IN_URL, LNG, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY(proxy_tls13_ciphers, PROXY_TLS13_CIPHERS, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY(tls13_ciphers, TLS13_CIPHERS, STR, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,62,0) +OPT_ENTRY(upkeep_interval_ms, UPKEEP_INTERVAL_MS, LNG, 0, CURL_UPKEEP_INTERVAL_DEFAULT) +OPT_ENTRY(doh_url, DOH_URL, STR, 0, LCURL_DEFAULT_VALUE) +// thre no named value for default value. It just defined as 64kB in documentation +OPT_ENTRY(upload_buffersize, UPLOAD_BUFFERSIZE, LNG, 0, 64 * 1024) +#endif + +#if LCURL_CURL_VER_GE(7,64,0) +OPT_ENTRY(http09_allowed, HTTP09_ALLOWED, LNG, 0, 0) +#endif + +#if LCURL_CURL_VER_GE(7,64,1) +OPT_ENTRY(altsvc, ALTSVC, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY(altsvc_ctrl, ALTSVC_CTRL, LNG, 0, 0) +#endif + +#if LCURL_CURL_VER_GE(7,65,0) +OPT_ENTRY(maxage_conn, MAXAGE_CONN, LNG, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,66,0) +OPT_ENTRY(sasl_authzid, SASL_AUTHZID, STR, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,68,0) +FLG_ENTRY( PROGRESSFUNC_CONTINUE ) +#endif + +#if LCURL_CURL_VER_GE(7,69,0) +OPT_ENTRY(mail_rcpt_alllowfails, MAIL_RCPT_ALLLOWFAILS, LNG, 0, 1) +#endif + +#if LCURL_CURL_VER_GE(7,71,0) +OPT_ENTRY(sslcert_blob, SSLCERT_BLOB, BLB, 0, 0) +OPT_ENTRY(sslkey_blob, SSLKEY_BLOB, BLB, 0, 0) +OPT_ENTRY(proxy_sslcert_blob, PROXY_SSLCERT_BLOB, BLB, 0, 0) +OPT_ENTRY(proxy_sslkey_blob, PROXY_SSLKEY_BLOB, BLB, 0, 0) +OPT_ENTRY(issuercert_blob, ISSUERCERT_BLOB, BLB, 0, 0) + +OPT_ENTRY(proxy_issuercert, PROXY_ISSUERCERT, STR, 0, LCURL_DEFAULT_VALUE) +OPT_ENTRY(proxy_issuercert_blob, PROXY_ISSUERCERT_BLOB, BLB, 0, 0) +#endif + +#if LCURL_CURL_VER_GE(7,73,0) +OPT_ENTRY(ssl_ec_curves, SSL_EC_CURVES, STR, 0, LCURL_DEFAULT_VALUE) +#endif + +#if LCURL_CURL_VER_GE(7,74,0) && LCURL_USE_HSTS +OPT_ENTRY(hsts_ctrl, HSTS_CTRL, LNG, 0, 0) +OPT_ENTRY(hsts, HSTS, STR, 0, LCURL_DEFAULT_VALUE) +#endif + +//{ Restore system macros + +#ifdef LCURL__TCP_FASTOPEN +# define TCP_FASTOPEN LCURL__TCP_FASTOPEN +# undef LCURL__TCP_FASTOPEN +#endif + +#ifdef LCURL__TCP_KEEPIDLE +# define TCP_KEEPIDLE LCURL__TCP_KEEPIDLE +# undef LCURL__TCP_KEEPIDLE +#endif + +#ifdef LCURL__TCP_KEEPINTVL +# define TCP_KEEPINTVL LCURL__TCP_KEEPINTVL +# undef LCURL__TCP_KEEPINTVL +#endif + +#ifdef LCURL__TCP_NODELAY +# define TCP_NODELAY LCURL__TCP_NODELAY +# undef LCURL__TCP_NODELAY +#endif + +#ifdef LCURL__TCP_KEEPALIVE +# define TCP_KEEPALIVE LCURL__TCP_KEEPALIVE +# undef LCURL__TCP_KEEPALIVE +#endif + +#ifdef LCURL__BUFFERSIZE +# define BUFFERSIZE LCURL__BUFFERSIZE +# undef LCURL__BUFFERSIZE +#endif + +#ifdef LCURL__INTERFACE +# define INTERFACE LCURL__INTERFACE +# undef LCURL__INTERFACE +#endif + +//} + +#ifdef OPT_ENTRY_IS_NULL +# undef OPT_ENTRY +#endif + +#ifdef FLG_ENTRY_IS_NULL +# undef FLG_ENTRY +#endif diff --git a/watchdog/third_party/lua-curl/src/lcoptmulti.h b/watchdog/third_party/lua-curl/src/lcoptmulti.h new file mode 100644 index 0000000..8f7fd71 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcoptmulti.h @@ -0,0 +1,17 @@ + +OPT_ENTRY(pipelining, PIPELINING, LNG, 0 ) +OPT_ENTRY(maxconnects, MAXCONNECTS, LNG, 0 ) + +#if LCURL_CURL_VER_GE(7,30,0) +OPT_ENTRY(max_host_connections, MAX_HOST_CONNECTIONS, LNG, 0 ) +OPT_ENTRY(max_pipeline_length, MAX_PIPELINE_LENGTH, LNG, 0 ) +OPT_ENTRY(content_length_penalty_size, CONTENT_LENGTH_PENALTY_SIZE, LNG, 0 ) +OPT_ENTRY(chunk_length_penalty_size, CHUNK_LENGTH_PENALTY_SIZE, LNG, 0 ) +OPT_ENTRY(pipelining_site_bl, PIPELINING_SITE_BL, STR_ARR, 0 ) +OPT_ENTRY(pipelining_server_bl, PIPELINING_SERVER_BL, STR_ARR, 0 ) +OPT_ENTRY(max_total_connections, MAX_TOTAL_CONNECTIONS, LNG, 0 ) +#endif + +#if LCURL_CURL_VER_GE(7,67,0) +OPT_ENTRY(max_concurrent_streams, MAX_CONCURRENT_STREAMS, LNG, 0 ) +#endif diff --git a/watchdog/third_party/lua-curl/src/lcoptshare.h b/watchdog/third_party/lua-curl/src/lcoptshare.h new file mode 100644 index 0000000..95960bb --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcoptshare.h @@ -0,0 +1,27 @@ +#ifndef OPT_ENTRY +# define OPT_ENTRY(a,b,c,d) +# define OPT_ENTRY_IS_NULL +#endif + +#ifndef FLG_ENTRY +# define FLG_ENTRY(a) +# define FLG_ENTRY_IS_NULL +#endif + +OPT_ENTRY(share, SHARE, LNG, 0 ) +OPT_ENTRY(unshare, UNSHARE, LNG, 0 ) + +FLG_ENTRY( LOCK_DATA_COOKIE ) +FLG_ENTRY( LOCK_DATA_DNS ) +FLG_ENTRY( LOCK_DATA_SSL_SESSION ) +FLG_ENTRY( LOCK_DATA_CONNECT ) + +#ifdef OPT_ENTRY_IS_NULL +# undef OPT_ENTRY +# undef OPT_ENTRY_IS_NULL +#endif + +#ifdef FLG_ENTRY_IS_NULL +# undef FLG_ENTRY +# undef FLG_ENTRY_IS_NULL +#endif diff --git a/watchdog/third_party/lua-curl/src/lcopturl.h b/watchdog/third_party/lua-curl/src/lcopturl.h new file mode 100644 index 0000000..20fee9e --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcopturl.h @@ -0,0 +1,29 @@ +ENTRY_PART(fragment, UPART_FRAGMENT , CURLUE_NO_FRAGMENT ) +ENTRY_PART(host, UPART_HOST , CURLUE_NO_HOST ) +ENTRY_PART(options, UPART_OPTIONS , CURLUE_NO_OPTIONS ) +ENTRY_PART(password, UPART_PASSWORD , CURLUE_NO_PASSWORD ) +ENTRY_PART(path, UPART_PATH , CURLUE_OK ) +ENTRY_PART(port, UPART_PORT , CURLUE_NO_PORT ) +ENTRY_PART(query, UPART_QUERY , CURLUE_NO_QUERY ) +ENTRY_PART(scheme, UPART_SCHEME , CURLUE_NO_SCHEME ) +ENTRY_PART(url, UPART_URL , CURLUE_OK ) +ENTRY_PART(user, UPART_USER , CURLUE_NO_USER ) + +#if LCURL_CURL_VER_GE(7,65,0) +ENTRY_PART(zoneid, UPART_ZONEID , CURLUE_UNKNOWN_PART ) +#endif + +ENTRY_FLAG(DEFAULT_PORT ) +ENTRY_FLAG(NO_DEFAULT_PORT ) +ENTRY_FLAG(DEFAULT_SCHEME ) +ENTRY_FLAG(NON_SUPPORT_SCHEME ) +ENTRY_FLAG(PATH_AS_IS ) +ENTRY_FLAG(DISALLOW_USER ) +ENTRY_FLAG(URLDECODE ) +ENTRY_FLAG(URLENCODE ) +ENTRY_FLAG(APPENDQUERY ) +ENTRY_FLAG(GUESS_SCHEME ) + +#if LCURL_CURL_VER_GE(7,67,0) +ENTRY_FLAG(NO_AUTHORITY ) +#endif diff --git a/watchdog/third_party/lua-curl/src/lcshare.c b/watchdog/third_party/lua-curl/src/lcshare.c new file mode 100644 index 0000000..c7ac489 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcshare.c @@ -0,0 +1,152 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#include "lcurl.h" +#include "lcshare.h" +#include "lcerror.h" +#include "lcutils.h" +#include "lchttppost.h" + +#define LCURL_SHARE_NAME LCURL_PREFIX" Share" +static const char *LCURL_SHARE = LCURL_SHARE_NAME; + +//{ +int lcurl_share_create(lua_State *L, int error_mode){ + lcurl_share_t *p; + + lua_settop(L, 1); + + p = lutil_newudatap(L, lcurl_share_t, LCURL_SHARE); + p->curl = curl_share_init(); + p->err_mode = error_mode; + if(!p->curl) return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_SHARE, CURLSHE_NOMEM); + + if(lua_type(L, 1) == LUA_TTABLE){ + int ret = lcurl_utils_apply_options(L, 1, 2, 1, p->err_mode, LCURL_ERROR_SHARE, CURLSHE_BAD_OPTION); + if(ret) return ret; + assert(lua_gettop(L) == 2); + } + + return 1; +} + +lcurl_share_t *lcurl_getshare_at(lua_State *L, int i){ + lcurl_share_t *p = (lcurl_share_t *)lutil_checkudatap (L, i, LCURL_SHARE); + luaL_argcheck (L, p != NULL, 1, LCURL_SHARE_NAME" object expected"); + return p; +} + +static int lcurl_easy_to_s(lua_State *L){ + lcurl_share_t *p = (lcurl_share_t *)lutil_checkudatap (L, 1, LCURL_SHARE); + lua_pushfstring(L, LCURL_SHARE_NAME" (%p)", (void*)p); + return 1; +} + +static int lcurl_share_cleanup(lua_State *L){ + lcurl_share_t *p = lcurl_getshare(L); + if(p->curl){ + curl_share_cleanup(p->curl); + p->curl = NULL; + } + + return 0; +} + +//{ OPTIONS + +static int lcurl_opt_set_long_(lua_State *L, int opt){ + lcurl_share_t *p = lcurl_getshare(L); + long val; CURLSHcode code; + + if(lua_isboolean(L, 2)) val = lua_toboolean(L, 2); + else{ + luaL_argcheck(L, lua_type(L, 2) == LUA_TNUMBER, 2, "number or boolean expected"); + val = luaL_checklong(L, 2); + } + + code = curl_share_setopt(p->curl, opt, val); + if(code != CURLSHE_OK){ + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_SHARE, code); + } + lua_settop(L, 1); + return 1; +} + +#define LCURL_LNG_OPT(N, S) static int lcurl_share_set_##N(lua_State *L){\ + return lcurl_opt_set_long_(L, CURLSHOPT_##N);\ +} + +#define OPT_ENTRY(L, N, T, S) LCURL_##T##_OPT(N, S) + +#include "lcoptshare.h" + +#undef OPT_ENTRY +#undef LCURL_LNG_OPT + +//} + +static int lcurl_share_setopt(lua_State *L){ + lcurl_share_t *p = lcurl_getshare(L); + int opt; + + luaL_checkany(L, 2); + if(lua_type(L, 2) == LUA_TTABLE){ + int ret = lcurl_utils_apply_options(L, 2, 1, 0, p->err_mode, LCURL_ERROR_SHARE, CURLSHE_BAD_OPTION); + if(ret) return ret; + lua_settop(L, 1); + return 1; + } + + opt = luaL_checklong(L, 2); + lua_remove(L, 2); + +#define OPT_ENTRY(l, N, T, S) case CURLSHOPT_##N: return lcurl_share_set_##N(L); + switch(opt){ + #include "lcoptshare.h" + } +#undef OPT_ENTRY + + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_SHARE, CURLSHE_BAD_OPTION); +} + +//} + +static const struct luaL_Reg lcurl_share_methods[] = { + { "__tostring", lcurl_easy_to_s }, + {"setopt", lcurl_share_setopt }, + +#define OPT_ENTRY(L, N, T, S) { "setopt_"#L, lcurl_share_set_##N }, + #include "lcoptshare.h" +#undef OPT_ENTRY + + {"close", lcurl_share_cleanup }, + {"__gc", lcurl_share_cleanup }, + + {NULL,NULL} +}; + +static const lcurl_const_t lcurl_share_opt[] = { + +#define OPT_ENTRY(L, N, T, S) { "OPT_SHARE_"#N, CURLSHOPT_##N }, +#define FLG_ENTRY(N) { #N, CURL_##N }, +# include "lcoptshare.h" +#undef OPT_ENTRY +#undef FLG_ENTRY + + {NULL, 0} +}; + +void lcurl_share_initlib(lua_State *L, int nup){ + if(!lutil_createmetap(L, LCURL_SHARE, lcurl_share_methods, nup)) + lua_pop(L, nup); + lua_pop(L, 1); + + lcurl_util_set_const(L, lcurl_share_opt); +} diff --git a/watchdog/third_party/lua-curl/src/lcshare.h b/watchdog/third_party/lua-curl/src/lcshare.h new file mode 100644 index 0000000..a1018c9 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcshare.h @@ -0,0 +1,30 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#ifndef _LCSHARE_H_ +#define _LCSHARE_H_ + +#include "lcurl.h" +#include "lcutils.h" + +typedef struct lcurl_share_tag{ + CURLM *curl; + int err_mode; +}lcurl_share_t; + +int lcurl_share_create(lua_State *L, int error_mode); + +lcurl_share_t *lcurl_getshare_at(lua_State *L, int i); + +#define lcurl_getshare(L) lcurl_getshare_at((L),1) + +void lcurl_share_initlib(lua_State *L, int nup); + +#endif diff --git a/watchdog/third_party/lua-curl/src/lcurl.c b/watchdog/third_party/lua-curl/src/lcurl.c new file mode 100644 index 0000000..e70680d --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcurl.c @@ -0,0 +1,487 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2021 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#include "lcurl.h" +#include "lceasy.h" +#include "lcmulti.h" +#include "lcshare.h" +#include "lcerror.h" +#include "lchttppost.h" +#include "lcmime.h" +#include "lcurlapi.h" +#include "lcutils.h" + +/*export*/ +#ifdef _WIN32 +# define LCURL_EXPORT_API __declspec(dllexport) +#else +# define LCURL_EXPORT_API LUALIB_API +#endif + +static const char* LCURL_REGISTRY = "LCURL Registry"; +static const char* LCURL_USERVAL = "LCURL Uservalues"; +#if LCURL_CURL_VER_GE(7,56,0) +static const char* LCURL_MIME_EASY_MAP = "LCURL Mime easy"; +#endif + +#if LCURL_CURL_VER_GE(7,56,0) +#define NUP 3 +#else +#define NUP 2 +#endif + +static volatile int LCURL_INIT = 0; + +static int lcurl_init_in_mode(lua_State *L, long init_mode, int error_mode){ + if(!LCURL_INIT){ + /* Note from libcurl documentation. + * + * The environment it sets up is constant for the life of the program + * and is the same for every program, so multiple calls have the same + * effect as one call. ... This function is not thread safe. + */ + CURLcode code = curl_global_init(init_mode); + if (code != CURLE_OK) { + return lcurl_fail_ex(L, error_mode, LCURL_ERROR_CURL, code); + } + LCURL_INIT = 1; + } + return 0; +} + +static int lcurl_init(lua_State *L, int error_mode){ + long init_mode = CURL_GLOBAL_DEFAULT; + if (L != NULL) { + int type = lua_type(L, 1); + if (type == LUA_TNUMBER) { + init_mode = lua_tonumber(L, 1); + } + } + return lcurl_init_in_mode(L, init_mode, error_mode); +} + +static int lcurl_init_default(lua_State *L){ + return lcurl_init_in_mode(L, CURL_GLOBAL_DEFAULT, LCURL_ERROR_RAISE); +} + +static int lcurl_init_unsafe(lua_State *L){ + return lcurl_init(L, LCURL_ERROR_RAISE); +} + +static int lcurl_init_safe(lua_State *L){ + return lcurl_init(L, LCURL_ERROR_RETURN); +} + +static int lcurl_easy_new_safe(lua_State *L){ + return lcurl_easy_create(L, LCURL_ERROR_RETURN); +} + +static int lcurl_multi_new_safe(lua_State *L){ + return lcurl_multi_create(L, LCURL_ERROR_RETURN); +} + +static int lcurl_share_new_safe(lua_State *L){ + return lcurl_share_create(L, LCURL_ERROR_RETURN); +} + +static int lcurl_hpost_new_safe(lua_State *L) { + return lcurl_hpost_create(L, LCURL_ERROR_RETURN); +} + +#if LCURL_CURL_VER_GE(7,62,0) + +static int lcurl_url_new_safe(lua_State *L) { + return lcurl_url_create(L, LCURL_ERROR_RETURN); +} + +#endif + +static int lcurl_easy_new(lua_State *L){ + return lcurl_easy_create(L, LCURL_ERROR_RAISE); +} + +static int lcurl_multi_new(lua_State *L){ + return lcurl_multi_create(L, LCURL_ERROR_RAISE); +} + +static int lcurl_share_new(lua_State *L){ + return lcurl_share_create(L, LCURL_ERROR_RAISE); +} + +static int lcurl_hpost_new(lua_State *L){ + return lcurl_hpost_create(L, LCURL_ERROR_RAISE); +} + +#if LCURL_CURL_VER_GE(7,62,0) + +static int lcurl_url_new(lua_State *L) { + return lcurl_url_create(L, LCURL_ERROR_RAISE); +} + +#endif + +#if LCURL_CURL_VER_GE(7,73,0) + +static void lcurl_easy_option_push(lua_State *L, const struct curl_easyoption *opt) { + lua_newtable(L); + lua_pushliteral(L, "id"); lutil_pushuint(L, opt->id); lua_rawset(L, -3); + lua_pushliteral(L, "name"); lua_pushstring(L, opt->name); lua_rawset(L, -3); + lua_pushliteral(L, "type"); lutil_pushuint(L, opt->type); lua_rawset(L, -3); + lua_pushliteral(L, "flags"); lutil_pushuint(L, opt->flags); lua_rawset(L, -3); + lua_pushliteral(L, "flags_set"); lua_newtable(L); + lua_pushliteral(L, "alias"); lua_pushboolean(L, opt->flags & CURLOT_FLAG_ALIAS); lua_rawset(L, -3); + lua_rawset(L, -3); + lua_pushliteral(L, "type_name"); + switch(opt->type){ + case CURLOT_LONG : lua_pushliteral(L, "LONG" ); break; + case CURLOT_VALUES : lua_pushliteral(L, "VALUES" ); break; + case CURLOT_OFF_T : lua_pushliteral(L, "OFF_T" ); break; + case CURLOT_OBJECT : lua_pushliteral(L, "OBJECT" ); break; + case CURLOT_STRING : lua_pushliteral(L, "STRING" ); break; + case CURLOT_SLIST : lua_pushliteral(L, "SLIST" ); break; + case CURLOT_CBPTR : lua_pushliteral(L, "CBPTR" ); break; + case CURLOT_BLOB : lua_pushliteral(L, "BLOB" ); break; + case CURLOT_FUNCTION: lua_pushliteral(L, "FUNCTION"); break; + default: lua_pushliteral(L, "UNKNOWN"); + } + lua_rawset(L, -3); +} + +static int lcurl_easy_option_next(lua_State *L) { + const struct curl_easyoption *opt; + + luaL_checktype(L, 1, LUA_TTABLE); + lua_settop(L, 1); + + lua_rawgeti(L, 1, 1); + opt = lua_touserdata(L, -1); + lua_settop(L, 1); + + opt = curl_easy_option_next(opt); + if (!opt) { + return 0; + } + + lcurl_easy_option_push(L, opt); + + lua_pushlightuserdata(L, (void*)opt); + lua_rawseti(L, 1, 1); + + return 1; +} + +static int lcurl_easy_option_by_id(lua_State *L) { + const struct curl_easyoption *opt = NULL; + lua_Integer id = luaL_checkinteger(L, 1); + + lua_settop(L, 0); + opt = curl_easy_option_by_id(id); + if (!opt) { + return 0; + } + + lcurl_easy_option_push(L, opt); + + return 1; +} + +static int lcurl_easy_option_by_name(lua_State *L) { + const struct curl_easyoption *opt = NULL; + const char *name = luaL_checkstring(L, 1); + + lua_settop(L, 0); + opt = curl_easy_option_by_name(name); + if (!opt) { + return 0; + } + + lcurl_easy_option_push(L, opt); + + return 1; +} + +static int lcurl_easy_option_iter(lua_State *L) { + lua_pushcfunction(L, lcurl_easy_option_next); + lua_newtable(L); + return 2; +} + +#endif + +static int lcurl_version(lua_State *L){ + lua_pushstring(L, curl_version()); + return 1; +} + +static int lcurl_debug_getregistry(lua_State *L) { + lua_rawgetp(L, LUA_REGISTRYINDEX, LCURL_REGISTRY); + return 1; +} + +static int push_upper(lua_State *L, const char *str){ + char buffer[128]; + size_t i, n = strlen(str); + char *ptr = (n < sizeof(buffer))?&buffer[0]:malloc(n + 1); + if (!ptr) return 1; + for(i = 0; i < n; ++i){ + if( (str[i] > 96 ) && (str[i] < 123) ) ptr[i] = str[i] - 'a' + 'A'; + else ptr[i] = str[i]; + } + lua_pushlstring(L, ptr, n); + if(ptr != &buffer[0]) free(ptr); + return 0; +} + +static int lcurl_version_info(lua_State *L){ + const char * const*p; + curl_version_info_data *data = curl_version_info(CURLVERSION_NOW); + + lua_newtable(L); + lua_pushstring(L, data->version); lua_setfield(L, -2, "version"); /* LIBCURL_VERSION */ + lutil_pushuint(L, data->version_num); lua_setfield(L, -2, "version_num"); /* LIBCURL_VERSION_NUM */ + lua_pushstring(L, data->host); lua_setfield(L, -2, "host"); /* OS/host/cpu/machine when configured */ + + lua_newtable(L); + lua_pushliteral(L, "IPV6"); lua_pushboolean(L, data->features & CURL_VERSION_IPV6 ); lua_rawset(L, -3); + lua_pushliteral(L, "KERBEROS4"); lua_pushboolean(L, data->features & CURL_VERSION_KERBEROS4 ); lua_rawset(L, -3); + lua_pushliteral(L, "SSL"); lua_pushboolean(L, data->features & CURL_VERSION_SSL ); lua_rawset(L, -3); + lua_pushliteral(L, "LIBZ"); lua_pushboolean(L, data->features & CURL_VERSION_LIBZ ); lua_rawset(L, -3); + lua_pushliteral(L, "NTLM"); lua_pushboolean(L, data->features & CURL_VERSION_NTLM ); lua_rawset(L, -3); + lua_pushliteral(L, "GSSNEGOTIATE"); lua_pushboolean(L, data->features & CURL_VERSION_GSSNEGOTIATE); lua_rawset(L, -3); +#if LCURL_CURL_VER_GE(7,38,0) + lua_pushliteral(L, "GSSAPI"); lua_pushboolean(L, data->features & CURL_VERSION_GSSAPI ); lua_rawset(L, -3); +#endif + lua_pushliteral(L, "DEBUG"); lua_pushboolean(L, data->features & CURL_VERSION_DEBUG ); lua_rawset(L, -3); + lua_pushliteral(L, "ASYNCHDNS"); lua_pushboolean(L, data->features & CURL_VERSION_ASYNCHDNS ); lua_rawset(L, -3); + lua_pushliteral(L, "SPNEGO"); lua_pushboolean(L, data->features & CURL_VERSION_SPNEGO ); lua_rawset(L, -3); + lua_pushliteral(L, "LARGEFILE"); lua_pushboolean(L, data->features & CURL_VERSION_LARGEFILE ); lua_rawset(L, -3); + lua_pushliteral(L, "IDN"); lua_pushboolean(L, data->features & CURL_VERSION_IDN ); lua_rawset(L, -3); + lua_pushliteral(L, "SSPI"); lua_pushboolean(L, data->features & CURL_VERSION_SSPI ); lua_rawset(L, -3); + lua_pushliteral(L, "CONV"); lua_pushboolean(L, data->features & CURL_VERSION_CONV ); lua_rawset(L, -3); + lua_pushliteral(L, "CURLDEBUG"); lua_pushboolean(L, data->features & CURL_VERSION_CURLDEBUG ); lua_rawset(L, -3); +#if LCURL_CURL_VER_GE(7,21,4) + lua_pushliteral(L, "TLSAUTH_SRP"); lua_pushboolean(L, data->features & CURL_VERSION_TLSAUTH_SRP ); lua_rawset(L, -3); +#endif +#if LCURL_CURL_VER_GE(7,22,0) + lua_pushliteral(L, "NTLM_WB"); lua_pushboolean(L, data->features & CURL_VERSION_NTLM_WB ); lua_rawset(L, -3); +#endif +#ifdef CURL_VERSION_HTTP2 + lua_pushliteral(L, "HTTP2"); lua_pushboolean(L, data->features & CURL_VERSION_HTTP2 ); lua_rawset(L, -3); +#endif +#ifdef CURL_VERSION_HTTPS_PROXY + lua_pushliteral(L, "HTTPS_PROXY"); lua_pushboolean(L, data->features & CURL_VERSION_HTTPS_PROXY ); lua_rawset(L, -3); +#endif +#ifdef CURL_VERSION_MULTI_SSL + lua_pushliteral(L, "MULTI_SSL"); lua_pushboolean(L, data->features & CURL_VERSION_MULTI_SSL ); lua_rawset(L, -3); +#endif +#ifdef CURL_VERSION_BROTLI + lua_pushliteral(L, "BROTLI"); lua_pushboolean(L, data->features & CURL_VERSION_BROTLI ); lua_rawset(L, -3); +#endif +#ifdef CURL_VERSION_ALTSVC + lua_pushliteral(L, "ALTSVC"); lua_pushboolean(L, data->features & CURL_VERSION_ALTSVC ); lua_rawset(L, -3); +#endif +#ifdef CURL_VERSION_HTTP3 + lua_pushliteral(L, "HTTP3"); lua_pushboolean(L, data->features & CURL_VERSION_HTTP3 ); lua_rawset(L, -3); +#endif +#ifdef CURL_VERSION_ZSTD + lua_pushliteral(L, "ZSTD"); lua_pushboolean(L, data->features & CURL_VERSION_ZSTD ); lua_rawset(L, -3); +#endif +#ifdef CURL_VERSION_UNICODE + lua_pushliteral(L, "UNICODE"); lua_pushboolean(L, data->features & CURL_VERSION_UNICODE ); lua_rawset(L, -3); +#endif +#ifdef CURL_VERSION_HSTS + lua_pushliteral(L, "HSTS"); lua_pushboolean(L, data->features & CURL_VERSION_HSTS ); lua_rawset(L, -3); +#endif + + lua_setfield(L, -2, "features"); /* bitmask, see defines below */ + + if(data->ssl_version){lua_pushstring(L, data->ssl_version); lua_setfield(L, -2, "ssl_version");} /* human readable string */ + lutil_pushuint(L, data->ssl_version_num); lua_setfield(L, -2, "ssl_version_num"); /* not used anymore, always 0 */ + if(data->libz_version){lua_pushstring(L, data->libz_version); lua_setfield(L, -2, "libz_version");} /* human readable string */ + + /* protocols is terminated by an entry with a NULL protoname */ + lua_newtable(L); + for(p = data->protocols; *p; ++p){ + push_upper(L, *p); lua_pushboolean(L, 1); lua_rawset(L, -3); + } + lua_setfield(L, -2, "protocols"); + + if(data->age >= CURLVERSION_SECOND){ + if(data->ares){lua_pushstring(L, data->ares); lua_setfield(L, -2, "ares");} + lutil_pushuint(L, data->ares_num); lua_setfield(L, -2, "ares_num"); + } + + if(data->age >= CURLVERSION_THIRD){ /* added in 7.12.0 */ + if(data->libidn){lua_pushstring(L, data->libidn); lua_setfield(L, -2, "libidn");} + } + +#if LCURL_CURL_VER_GE(7,16,1) + if(data->age >= CURLVERSION_FOURTH){ + lutil_pushuint(L, data->iconv_ver_num); lua_setfield(L, -2, "iconv_ver_num"); + if(data->libssh_version){lua_pushstring(L, data->libssh_version);lua_setfield(L, -2, "libssh_version");} + } +#endif + +#if LCURL_CURL_VER_GE(7,57,0) + if(data->age >= CURLVERSION_FOURTH){ + lutil_pushuint(L, data->brotli_ver_num); lua_setfield(L, -2, "brotli_ver_num"); + if(data->brotli_version){lua_pushstring(L, data->brotli_version);lua_setfield(L, -2, "brotli_version");} + } +#endif + +#if LCURL_CURL_VER_GE(7,66,0) + if(data->age >= CURLVERSION_SIXTH){ + lutil_pushuint(L, data->nghttp2_ver_num); lua_setfield(L, -2, "nghttp2_ver_num"); + if(data->nghttp2_version){lua_pushstring(L, data->nghttp2_version);lua_setfield(L, -2, "nghttp2_version");} + if(data->quic_version){lua_pushstring(L, data->quic_version);lua_setfield(L, -2, "quic_version");} + } +#endif + +#if LCURL_CURL_VER_GE(7,70,0) + if(data->age >= CURLVERSION_SEVENTH){ + if(data->cainfo){lua_pushstring(L, data->cainfo);lua_setfield(L, -2, "cainfo");} + if(data->capath){lua_pushstring(L, data->capath);lua_setfield(L, -2, "capath");} + } +#endif + +#if LCURL_CURL_VER_GE(7,72,0) + if(data->age >= CURLVERSION_EIGHTH){ + lutil_pushuint(L, data->zstd_ver_num); lua_setfield(L, -2, "zstd_ver_num"); + if(data->zstd_version){lua_pushstring(L, data->zstd_version);lua_setfield(L, -2, "zstd_version");} + } +#endif + + if(lua_isstring(L, 1)){ + lua_pushvalue(L, 1); lua_rawget(L, -2); + } + + return 1; +} + +static const struct luaL_Reg lcurl_functions[] = { + {"init", lcurl_init_unsafe }, + {"error", lcurl_error_new }, + {"form", lcurl_hpost_new }, + {"easy", lcurl_easy_new }, + {"multi", lcurl_multi_new }, + {"share", lcurl_share_new }, +#if LCURL_CURL_VER_GE(7,62,0) + {"url", lcurl_url_new }, +#endif + {"version", lcurl_version }, + {"version_info", lcurl_version_info }, +#if LCURL_CURL_VER_GE(7,73,0) + {"ieasy_options", lcurl_easy_option_iter }, + {"easy_option_by_id", lcurl_easy_option_by_id }, + {"easy_option_by_name", lcurl_easy_option_by_name }, +#endif + + {"__getregistry", lcurl_debug_getregistry}, + + {NULL,NULL} +}; + +static const struct luaL_Reg lcurl_functions_safe[] = { + {"init", lcurl_init_safe }, + {"error", lcurl_error_new }, + {"form", lcurl_hpost_new_safe }, + {"easy", lcurl_easy_new_safe }, + {"multi", lcurl_multi_new_safe }, + {"share", lcurl_share_new_safe }, +#if LCURL_CURL_VER_GE(7,62,0) + {"url", lcurl_url_new_safe }, +#endif + {"version", lcurl_version }, + {"version_info", lcurl_version_info }, +#if LCURL_CURL_VER_GE(7,73,0) + {"ieasy_options", lcurl_easy_option_iter }, + {"easy_option_by_id", lcurl_easy_option_by_id }, + {"easy_option_by_name", lcurl_easy_option_by_name }, +#endif + + { "__getregistry", lcurl_debug_getregistry }, + + {NULL,NULL} +}; + +static const lcurl_const_t lcurl_flags[] = { + +#define FLG_ENTRY(N) { #N, CURL##N }, +#include "lcflags.h" +#undef FLG_ENTRY + + {NULL, 0} +}; + +#if LCURL_CURL_VER_GE(7,56,0) +#define LCURL_PUSH_NUP(L) lua_pushvalue(L, -NUP-1);lua_pushvalue(L, -NUP-1);lua_pushvalue(L, -NUP-1); +#else +#define LCURL_PUSH_NUP(L) lua_pushvalue(L, -NUP-1);lua_pushvalue(L, -NUP-1); +#endif + +static int luaopen_lcurl_(lua_State *L, const struct luaL_Reg *func){ + if (getenv("LCURL_NO_INIT") == NULL) { // do not initialize curl if env variable LCURL_NO_INIT defined + lcurl_init_default(L); + } + + lua_rawgetp(L, LUA_REGISTRYINDEX, LCURL_REGISTRY); + if(!lua_istable(L, -1)){ /* registry */ + lua_pop(L, 1); + lua_newtable(L); + } + + lua_rawgetp(L, LUA_REGISTRYINDEX, LCURL_USERVAL); + if(!lua_istable(L, -1)){ /* usevalues */ + lua_pop(L, 1); + lcurl_util_new_weak_table(L, "k"); + } + +#if LCURL_CURL_VER_GE(7,56,0) + lua_rawgetp(L, LUA_REGISTRYINDEX, LCURL_MIME_EASY_MAP); + if(!lua_istable(L, -1)){ /* Mime->Easy */ + lua_pop(L, 1); + lcurl_util_new_weak_table(L, "v"); + } +#endif + + lua_newtable(L); /* library */ + + LCURL_PUSH_NUP(L); luaL_setfuncs(L, func, NUP); + LCURL_PUSH_NUP(L); lcurl_error_initlib(L, NUP); + LCURL_PUSH_NUP(L); lcurl_hpost_initlib(L, NUP); + LCURL_PUSH_NUP(L); lcurl_easy_initlib (L, NUP); + LCURL_PUSH_NUP(L); lcurl_mime_initlib (L, NUP); + LCURL_PUSH_NUP(L); lcurl_multi_initlib(L, NUP); + LCURL_PUSH_NUP(L); lcurl_share_initlib(L, NUP); + LCURL_PUSH_NUP(L); lcurl_url_initlib (L, NUP); + + LCURL_PUSH_NUP(L); + +#if LCURL_CURL_VER_GE(7,56,0) + lua_rawsetp(L, LUA_REGISTRYINDEX, LCURL_MIME_EASY_MAP); +#endif + + lua_rawsetp(L, LUA_REGISTRYINDEX, LCURL_USERVAL); + lua_rawsetp(L, LUA_REGISTRYINDEX, LCURL_REGISTRY); + + lcurl_util_set_const(L, lcurl_flags); + + lutil_push_null(L); + lua_setfield(L, -2, "null"); + + return 1; +} + +LCURL_EXPORT_API +int luaopen_lcurl(lua_State *L){ return luaopen_lcurl_(L, lcurl_functions); } + +LCURL_EXPORT_API +int luaopen_lcurl_safe(lua_State *L){ return luaopen_lcurl_(L, lcurl_functions_safe); } + diff --git a/watchdog/third_party/lua-curl/src/lcurl.h b/watchdog/third_party/lua-curl/src/lcurl.h new file mode 100644 index 0000000..8f43320 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcurl.h @@ -0,0 +1,31 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#ifndef _LCURL_H_ +#define _LCURL_H_ + +#include "l52util.h" +#include "curl/curl.h" +#include "curl/easy.h" +#include "curl/multi.h" + +#include +#include + +#define LCURL_PREFIX "LcURL" + +#define LCURL_LUA_REGISTRY lua_upvalueindex(1) + +#define LCURL_USERVALUES lua_upvalueindex(2) + +/* only for `mime` API */ +#define LCURL_MIME_EASY lua_upvalueindex(3) + +#endif diff --git a/watchdog/third_party/lua-curl/src/lcurlapi.c b/watchdog/third_party/lua-curl/src/lcurlapi.c new file mode 100644 index 0000000..2a8d503 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcurlapi.c @@ -0,0 +1,218 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#include "lcurlapi.h" +#include "lcurl.h" +#include "lcerror.h" +#include "lcutils.h" +#include + +#define LCURL_URL_NAME LCURL_PREFIX" URL" +static const char *LCURL_URL = LCURL_URL_NAME; + +#if LCURL_CURL_VER_GE(7,62,0) + +#define lcurl_geturl(L) lcurl_geturl_at(L, 1) + +int lcurl_url_create(lua_State *L, int error_mode){ + lcurl_url_t *p; + + p = lutil_newudatap(L, lcurl_url_t, LCURL_URL); + + p->url = curl_url(); + if(!p->url) return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_URL, CURLUE_OUT_OF_MEMORY); + + p->err_mode = error_mode; + + if (lua_gettop(L) > 1) { + const char *url = luaL_checkstring(L, 1); + unsigned int flags = 0; + CURLUcode code; + + if (lua_gettop(L) > 2) { + flags = (unsigned int)lutil_optint64(L, 2, 0); + } + + code = curl_url_set(p->url, CURLUPART_URL, url, flags); + if (code != CURLUE_OK) { + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_URL, code); + } + } + + return 1; +} + +lcurl_url_t *lcurl_geturl_at(lua_State *L, int i){ + lcurl_url_t *p = (lcurl_url_t *)lutil_checkudatap (L, i, LCURL_URL); + luaL_argcheck (L, p != NULL, 1, LCURL_URL_NAME" object expected"); + return p; +} + +static int lcurl_url_cleanup(lua_State *L){ + lcurl_url_t *p = lcurl_geturl(L); + + if (p->url){ + curl_url_cleanup(p->url); + p->url = NULL; + } + + return 0; +} + +static int lcurl_url_dup(lua_State *L) { + lcurl_url_t *r = lcurl_geturl(L); + lcurl_url_t *p = lutil_newudatap(L, lcurl_url_t, LCURL_URL); + + p->url = curl_url_dup(r->url); + if (!p->url) return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_URL, CURLUE_OUT_OF_MEMORY); + + p->err_mode = r->err_mode; + + return 1; +} + +static int lcurl_url_set(lua_State *L, CURLUPart what){ + lcurl_url_t *p = lcurl_geturl(L); + CURLUcode code; + const char *part; + unsigned int flags = 0; + + luaL_argcheck(L, lua_type(L, 2) == LUA_TSTRING || lutil_is_null(L, 2), 2, "string expected"); + + part = lua_tostring(L, 2); + flags = (unsigned int)lutil_optint64(L, 3, 0); + + code = curl_url_set(p->url, what, part, flags); + if (code != CURLUE_OK) { + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_URL, code); + } + + lua_settop(L, 1); + return 1; +} + +static int lcurl_url_get(lua_State *L, CURLUPart what, CURLUcode empty) { + lcurl_url_t *p = lcurl_geturl(L); + CURLUcode code; + char *part = NULL; + unsigned int flags = 0; + + flags = (unsigned int)lutil_optint64(L, 2, 0); + + code = curl_url_get(p->url, what, &part, flags); + if (code != CURLUE_OK) { + if (part) { + curl_free(part); + part = NULL; + } + + if (code != empty) { + return lcurl_fail_ex(L, p->err_mode, LCURL_ERROR_URL, code); + } + } + + if (part == NULL) { + lutil_push_null(L); + } + else { + lua_pushstring(L, part); + curl_free(part); + } + + return 1; +} + +static int lcurl_url_to_s(lua_State *L) { + lcurl_url_t *p = lcurl_geturl(L); + char *part = NULL; + + CURLUcode code = curl_url_get(p->url, CURLUPART_URL, &part, 0); + + if (code != CURLUE_OK) { + if (part) { + curl_free(part); + } + + return lcurl_fail_ex(L, LCURL_ERROR_RAISE, LCURL_ERROR_URL, code); + } + + if (part == NULL) { + lua_pushliteral(L, ""); + } + else { + lua_pushstring(L, part); + curl_free(part); + } + + return 1; +} + +#define ENTRY_PART(N, S, E) static int lcurl_url_set_##N(lua_State *L){\ + return lcurl_url_set(L, CURL##S);\ +} +#define ENTRY_FLAG(S) + +#include "lcopturl.h" + +#undef ENTRY_PART +#undef ENTRY_FLAG + +#define ENTRY_PART(N, S, E) static int lcurl_url_get_##N(lua_State *L){\ + return lcurl_url_get(L, CURL##S, E);\ +} +#define ENTRY_FLAG(S) + +#include "lcopturl.h" + +#undef ENTRY_PART +#undef ENTRY_FLAG + +static const struct luaL_Reg lcurl_url_methods[] = { + #define ENTRY_PART(N, S, E) { "set_"#N, lcurl_url_set_##N }, + #define ENTRY_FLAG(S) + #include "lcopturl.h" + #undef ENTRY_PART + #undef ENTRY_FLAG + + #define ENTRY_PART(N, S, E) { "get_"#N, lcurl_url_get_##N }, + #define ENTRY_FLAG(S) + #include "lcopturl.h" + #undef ENTRY_PART + #undef ENTRY_FLAG + + { "dup", lcurl_url_dup }, + { "cleanup", lcurl_url_cleanup }, + { "__gc", lcurl_url_cleanup }, + { "__tostring", lcurl_url_to_s }, + + { NULL,NULL } +}; + +static const lcurl_const_t lcurl_url_opt[] = { + #define ENTRY_PART(N, S, E) { #S, CURL##S }, + #define ENTRY_FLAG(S) { "U_"#S, CURLU_##S }, + #include "lcopturl.h" + #undef ENTRY_PART + #undef ENTRY_FLAG + {NULL, 0} +}; +#endif + +void lcurl_url_initlib(lua_State *L, int nup){ +#if LCURL_CURL_VER_GE(7,62,0) + if(!lutil_createmetap(L, LCURL_URL, lcurl_url_methods, nup)) + lua_pop(L, nup); + lua_pop(L, 1); + + lcurl_util_set_const(L, lcurl_url_opt); +#else + lua_pop(L, nup); +#endif +} diff --git a/watchdog/third_party/lua-curl/src/lcurlapi.h b/watchdog/third_party/lua-curl/src/lcurlapi.h new file mode 100644 index 0000000..4dd4672 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcurlapi.h @@ -0,0 +1,34 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2018 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#ifndef _LURL_H_ +#define _LURL_H_ + +#include "lcurl.h" +#include "lcutils.h" +#include + +void lcurl_url_initlib(lua_State *L, int nup); + +#if LCURL_CURL_VER_GE(7,62,0) + +typedef struct lcurl_url_tag { + CURLU *url; + + int err_mode; +}lcurl_url_t; + +int lcurl_url_create(lua_State *L, int error_mode); + +lcurl_url_t *lcurl_geturl_at(lua_State *L, int i); + +#endif + +#endif \ No newline at end of file diff --git a/watchdog/third_party/lua-curl/src/lcutils.c b/watchdog/third_party/lua-curl/src/lcutils.c new file mode 100644 index 0000000..6a406b4 --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcutils.c @@ -0,0 +1,408 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2021 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#include "lcurl.h" +#include "lcutils.h" +#include "lcerror.h" + +#define LCURL_STORAGE_SLIST 1 +#define LCURL_STORAGE_KV 2 + +int lcurl_storage_init(lua_State *L){ + lua_newtable(L); + return luaL_ref(L, LCURL_LUA_REGISTRY); +} + +void lcurl_storage_preserve_value(lua_State *L, int storage, int i){ + assert(i > 0 && i <= lua_gettop(L)); + luaL_checkany(L, i); + lua_rawgeti(L, LCURL_LUA_REGISTRY, storage); + lua_pushvalue(L, i); lua_pushboolean(L, 1); lua_rawset(L, -3); + lua_pop(L, 1); +} + +void lcurl_storage_remove_value(lua_State *L, int storage, int i){ + assert(i > 0 && i <= lua_gettop(L)); + luaL_checkany(L, i); + lua_rawgeti(L, LCURL_LUA_REGISTRY, storage); + lua_pushvalue(L, i); lua_pushnil(L); lua_rawset(L, -3); + lua_pop(L, 1); +} + +static void lcurl_storage_ensure_t(lua_State *L, int t){ + lua_rawgeti(L, -1, t); + if(!lua_istable(L, -1)){ + lua_pop(L, 1); + lua_newtable(L); + lua_pushvalue(L, -1); + lua_rawseti(L, -3, t); + } +} + +int lcurl_storage_preserve_slist(lua_State *L, int storage, struct curl_slist * list){ + int r; + lua_rawgeti(L, LCURL_LUA_REGISTRY, storage); + lcurl_storage_ensure_t(L, LCURL_STORAGE_SLIST); + lua_pushlightuserdata(L, list); + r = luaL_ref(L, -2); + lua_pop(L, 2); + return r; +} + +void lcurl_storage_preserve_iv(lua_State *L, int storage, int i, int v){ + v = lua_absindex(L, v); + + lua_rawgeti(L, LCURL_LUA_REGISTRY, storage); + lcurl_storage_ensure_t(L, LCURL_STORAGE_KV); + lua_pushvalue(L, v); + lua_rawseti(L, -2, i); + lua_pop(L, 2); +} + +void lcurl_storage_remove_i(lua_State *L, int storage, int i){ + lua_rawgeti(L, LCURL_LUA_REGISTRY, storage); + lua_rawgeti(L, -1, LCURL_STORAGE_KV); + if(lua_istable(L, -1)){ + lua_pushnil(L); + lua_rawseti(L, -2, i); + } + lua_pop(L, 2); +} + +void lcurl_storage_get_i(lua_State *L, int storage, int i){ + lua_rawgeti(L, LCURL_LUA_REGISTRY, storage); + lua_rawgeti(L, -1, LCURL_STORAGE_KV); + if(lua_istable(L, -1)){ + lua_rawgeti(L, -1, i); + lua_remove(L, -2); + } + lua_remove(L, -2); +} + +struct curl_slist* lcurl_storage_remove_slist(lua_State *L, int storage, int idx){ + struct curl_slist* list = NULL; + assert(idx != LUA_NOREF); + lua_rawgeti(L, LCURL_LUA_REGISTRY, storage); + lua_rawgeti(L, -1, LCURL_STORAGE_SLIST); // list storage + if(lua_istable(L, -1)){ + lua_rawgeti(L, -1, idx); + list = lua_touserdata(L, -1); + assert(list); + luaL_unref(L, -2, idx); + lua_pop(L, 1); + } + lua_pop(L, 2); + return list; +} + +int lcurl_storage_free(lua_State *L, int storage){ + lua_rawgeti(L, LCURL_LUA_REGISTRY, storage); + lua_rawgeti(L, -1, LCURL_STORAGE_SLIST); // list storage + if(lua_istable(L, -1)){ + lua_pushnil(L); + while(lua_next(L, -2) != 0){ + struct curl_slist * list = lua_touserdata(L, -1); + curl_slist_free_all(list); + lua_pushvalue(L, -2); lua_pushnil(L); + lua_rawset(L, -5); + lua_pop(L, 1); + } + } + luaL_unref(L, LCURL_LUA_REGISTRY, storage); + lua_pop(L, 2); + return LUA_NOREF; +} + +struct curl_slist* lcurl_util_array_to_slist(lua_State *L, int t){ + struct curl_slist *list = NULL; + int i, n = lua_rawlen(L, t); + + assert(lua_type(L, t) == LUA_TTABLE); + + for(i = 1; i <= n; ++i){ + lua_rawgeti(L, t, i); + list = curl_slist_append(list, lua_tostring(L, -1)); + lua_pop(L, 1); + } + return list; +} + +struct curl_slist* lcurl_util_to_slist(lua_State *L, int t){ + if(lua_type(L, t) == LUA_TTABLE){ + return lcurl_util_array_to_slist(L, t); + } + return 0; +} + +void lcurl_util_slist_set(lua_State *L, int t, struct curl_slist* list){ + int i; + t = lua_absindex(L, t); + for(i = 0;list;list = list->next){ + lua_pushstring(L, list->data); + lua_rawseti(L, t, ++i); + } +} + +void lcurl_util_slist_to_table(lua_State *L, struct curl_slist* list){ + lua_newtable(L); + lcurl_util_slist_set(L, -1, list); +} + +void lcurl_util_set_const(lua_State *L, const lcurl_const_t *reg){ + const lcurl_const_t *p; + for(p = reg; p->name; ++p){ + lua_pushstring(L, p->name); + lua_pushnumber(L, p->value); + lua_settable(L, -3); + } +} + +int lcurl_set_callback(lua_State *L, lcurl_callback_t *c, int i, const char *method){ + int top = lua_gettop(L); + i = lua_absindex(L, i); + + luaL_argcheck(L, !lua_isnoneornil(L, i), i, "no function present"); + luaL_argcheck(L, (top < (i + 2)), i + 2, "no arguments expected"); + + assert((top == i)||(top == (i + 1))); + + if(c->ud_ref != LUA_NOREF){ + luaL_unref(L, LCURL_LUA_REGISTRY, c->ud_ref); + c->ud_ref = LUA_NOREF; + } + + if(c->cb_ref != LUA_NOREF){ + luaL_unref(L, LCURL_LUA_REGISTRY, c->cb_ref); + c->cb_ref = LUA_NOREF; + } + + if(lutil_is_null(L, i)){ + if(top == (i + 1)){ + // Do we can just ignore this? + luaL_argcheck(L, + lua_isnoneornil(L, i + 1) || lutil_is_null(L, i + 1) + ,i + 1, "no context allowed when set callback to null" + ); + } + lua_pop(L, top - i + 1); + + return 1; + } + + if(lua_gettop(L) == (i + 1)){// function + context + c->ud_ref = luaL_ref(L, LCURL_LUA_REGISTRY); + c->cb_ref = luaL_ref(L, LCURL_LUA_REGISTRY); + + assert(top == (2 + lua_gettop(L))); + return 1; + } + + assert(top == i); + + if(lua_isfunction(L, i)){ // function + c->cb_ref = luaL_ref(L, LCURL_LUA_REGISTRY); + + assert(top == (1 + lua_gettop(L))); + return 1; + } + + if(lua_isuserdata(L, i) || lua_istable(L, i)){ // object + lua_getfield(L, i, method); + + luaL_argcheck(L, lua_isfunction(L, -1), 2, "method not found in object"); + + c->cb_ref = luaL_ref(L, LCURL_LUA_REGISTRY); + c->ud_ref = luaL_ref(L, LCURL_LUA_REGISTRY); + + assert(top == (1 + lua_gettop(L))); + return 1; + } + + lua_pushliteral(L, "invalid object type"); + return lua_error(L); +} + +int lcurl_util_push_cb(lua_State *L, lcurl_callback_t *c){ + assert(c->cb_ref != LUA_NOREF); + lua_rawgeti(L, LCURL_LUA_REGISTRY, c->cb_ref); + if(c->ud_ref != LUA_NOREF){ + lua_rawgeti(L, LCURL_LUA_REGISTRY, c->ud_ref); + return 2; + } + return 1; +} + +int lcurl_util_new_weak_table(lua_State*L, const char *mode){ + int top = lua_gettop(L); + lua_newtable(L); + lua_newtable(L); + lua_pushstring(L, mode); + lua_setfield(L, -2, "__mode"); + lua_setmetatable(L,-2); + assert((top+1) == lua_gettop(L)); + return 1; +} + +int lcurl_util_pcall_method(lua_State *L, const char *name, int nargs, int nresults, int errfunc){ + int obj_index = -nargs - 1; + lua_getfield(L, obj_index, name); + lua_insert(L, obj_index - 1); + return lua_pcall(L, nargs + 1, nresults, errfunc); +} + +static void lcurl_utils_pcall_close(lua_State *L, int obj){ + int top = lua_gettop(L); + lua_pushvalue(L, obj); + lcurl_util_pcall_method(L, "close", 0, 0, 0); + lua_settop(L, top); +} + +int lcurl_utils_apply_options(lua_State *L, int opt, int obj, int do_close, + int error_mode, int error_type, int error_code +){ + int top = lua_gettop(L); + opt = lua_absindex(L, opt); + obj = lua_absindex(L, obj); + + lua_pushnil(L); + while(lua_next(L, opt) != 0){ + int n; + assert(lua_gettop(L) == (top + 2)); + + if(lua_type(L, -2) == LUA_TNUMBER){ /* [curl.OPT_URL] = "http://localhost" */ + lua_pushvalue(L, -2); + lua_insert(L, -2); /*Stack : opt, obj, k, k, v */ + lua_pushliteral(L, "setopt"); /*Stack : opt, obj, k, k, v, "setopt" */ + n = 2; + } + else if(lua_type(L, -2) == LUA_TSTRING){ /* url = "http://localhost" */ + lua_pushliteral(L, "setopt_"); lua_pushvalue(L, -3); lua_concat(L, 2); + /*Stack : opt, obj, k, v, "setopt_XXX" */ + n = 1; + } + else{ + lua_pop(L, 1); + continue; + } + /*Stack : opt, obj, k,[ k,] v, `setoptXXX` */ + + lua_gettable(L, obj); /* get e["settop_XXX]*/ + + if(lua_isnil(L, -1)){ /* unknown option */ + if(do_close) lcurl_utils_pcall_close(L, obj); + lua_settop(L, top); + return lcurl_fail_ex(L, error_mode, error_type, error_code); + } + + lua_insert(L, -n-1); /*Stack : opt, obj, k, setoptXXX, [ k,] v */ + lua_pushvalue(L, obj); /*Stack : opt, obj, k, setoptXXX, [ k,] v, obj */ + lua_insert(L, -n-1); /*Stack : opt, obj, k, setoptXXX, obj, [ k,] v */ + + if(lua_pcall(L, n+1, 2, 0)){ + if(do_close) lcurl_utils_pcall_close(L, obj); + return lua_error(L); + } + + if(lua_isnil(L, -2)){ + if(do_close) lcurl_utils_pcall_close(L, obj); + lua_settop(L, top); + return 2; + } + + /*Stack : opt, obj, k, ok, nil*/ + lua_pop(L, 2); + assert(lua_gettop(L) == (top+1)); + } + assert(lua_gettop(L) == top); + return 0; +} + +void lcurl_stack_dump (lua_State *L){ + int i = 1, top = lua_gettop(L); + + fprintf(stderr, " ---------------- Stack Dump ----------------\n" ); + while( i <= top ) { + int t = lua_type(L, i); + switch (t) { + case LUA_TSTRING: + fprintf(stderr, "%d(%d):`%s'\n", i, i - top - 1, lua_tostring(L, i)); + break; + case LUA_TBOOLEAN: + fprintf(stderr, "%d(%d): %s\n", i, i - top - 1,lua_toboolean(L, i) ? "true" : "false"); + break; + case LUA_TNUMBER: + fprintf(stderr, "%d(%d): %g\n", i, i - top - 1, lua_tonumber(L, i)); + break; + default: + lua_getglobal(L, "tostring"); + lua_pushvalue(L, i); + lua_call(L, 1, 1); + fprintf(stderr, "%d(%d): %s(%s)\n", i, i - top - 1, lua_typename(L, t), lua_tostring(L, -1)); + lua_pop(L, 1); + break; + } + i++; + } + fprintf(stderr, " ------------ Stack Dump Finished ------------\n" ); +} + +curl_socket_t lcurl_opt_os_socket(lua_State *L, int idx, curl_socket_t def) { + if (lua_islightuserdata(L, idx)) + return (curl_socket_t)lua_touserdata(L, idx); + + return (curl_socket_t)lutil_optint64(L, idx, def); +} + +void lcurl_push_os_socket(lua_State *L, curl_socket_t fd) { +#if !defined(_WIN32) + lutil_pushint64(L, fd); +#else /*_WIN32*/ + /* Assumes that compiler can optimize constant conditions. MSVC do this. */ + + /*On Lua 5.3 lua_Integer type can be represented exactly*/ +#if LUA_VERSION_NUM >= 503 + if (sizeof(curl_socket_t) <= sizeof(lua_Integer)) { + lua_pushinteger(L, (lua_Integer)fd); + return; + } +#endif + +#if defined(LUA_NUMBER_DOUBLE) || defined(LUA_NUMBER_FLOAT) + /*! @todo test DBL_MANT_DIG, FLT_MANT_DIG */ + + if (sizeof(lua_Number) == 8) { /*we have 53 bits for integer*/ + if ((sizeof(curl_socket_t) <= 6)) { + lua_pushnumber(L, (lua_Number)fd); + return; + } + + if(((UINT_PTR)fd & 0x1FFFFFFFFFFFFF) == (UINT_PTR)fd) + lua_pushnumber(L, (lua_Number)fd); + else + lua_pushlightuserdata(L, (void*)fd); + + return; + } + + if (sizeof(lua_Number) == 4) { /*we have 24 bits for integer*/ + if (((UINT_PTR)fd & 0xFFFFFF) == (UINT_PTR)fd) + lua_pushnumber(L, (lua_Number)fd); + else + lua_pushlightuserdata(L, (void*)fd); + return; + } +#endif + + lutil_pushint64(L, fd); + if (lcurl_opt_os_socket(L, -1, 0) != fd) + lua_pushlightuserdata(L, (void*)fd); + +#endif /*_WIN32*/ +} diff --git a/watchdog/third_party/lua-curl/src/lcutils.h b/watchdog/third_party/lua-curl/src/lcutils.h new file mode 100644 index 0000000..cfad1af --- /dev/null +++ b/watchdog/third_party/lua-curl/src/lcutils.h @@ -0,0 +1,108 @@ +/****************************************************************************** +* Author: Alexey Melnichuk +* +* Copyright (C) 2014-2021 Alexey Melnichuk +* +* Licensed according to the included 'LICENSE' document +* +* This file is part of Lua-cURL library. +******************************************************************************/ + +#ifndef _LCUTILS_H_ +#define _LCUTILS_H_ + +#include "lcurl.h" + +#if defined(_MSC_VER) || defined(__cplusplus) +# define LCURL_CC_SUPPORT_FORWARD_TYPEDEF 1 +#elif defined(__STDC_VERSION__) +# if __STDC_VERSION__ >= 201112 +# define LCURL_CC_SUPPORT_FORWARD_TYPEDEF 1 +# endif +#endif + +#ifndef LCURL_CC_SUPPORT_FORWARD_TYPEDEF +# define LCURL_CC_SUPPORT_FORWARD_TYPEDEF 0 +#endif + +#ifdef __GNUC__ + #define LCURL_UNUSED_TYPEDEF __attribute__ ((unused)) +#else + #define LCURL_UNUSED_TYPEDEF +#endif + +#define LCURL_UNUSED_VAR LCURL_UNUSED_TYPEDEF + +#define LCURL_MAKE_VERSION(MIN, MAJ, PAT) ((MIN<<16) + (MAJ<<8) + PAT) +#define LCURL_CURL_VER_GE(MIN, MAJ, PAT) (LIBCURL_VERSION_NUM >= LCURL_MAKE_VERSION(MIN, MAJ, PAT)) + +#define LCURL_CONCAT_STATIC_ASSERT_IMPL_(x, y) LCURL_CONCAT1_STATIC_ASSERT_IMPL_ (x, y) +#define LCURL_CONCAT1_STATIC_ASSERT_IMPL_(x, y) LCURL_UNUSED_TYPEDEF x##y +#define LCURL_STATIC_ASSERT(expr) typedef char LCURL_CONCAT_STATIC_ASSERT_IMPL_(static_assert_failed_at_line_, __LINE__) [(expr) ? 1 : -1] + +#define LCURL_ASSERT_SAME_SIZE(a, b) LCURL_STATIC_ASSERT( sizeof(a) == sizeof(b) ) +#define LCURL_ASSERT_SAME_OFFSET(a, am, b, bm) LCURL_STATIC_ASSERT( (offsetof(a,am)) == (offsetof(b,bm)) ) +#define LCURL_ASSERT_SAME_FIELD_SIZE(a, am, b, bm) LCURL_ASSERT_SAME_SIZE(((a*)0)->am, ((b*)0)->bm) + +typedef struct lcurl_const_tag{ + const char *name; + long value; +}lcurl_const_t; + +typedef struct lcurl_callback_tag{ + int cb_ref; + int ud_ref; +}lcurl_callback_t; + +typedef struct lcurl_read_buffer_tag{ + int ref; + size_t off; +}lcurl_read_buffer_t; + +int lcurl_storage_init(lua_State *L); + +void lcurl_storage_preserve_value(lua_State *L, int storage, int i); + +void lcurl_storage_remove_value(lua_State *L, int storage, int i); + +int lcurl_storage_preserve_slist(lua_State *L, int storage, struct curl_slist * list); + +struct curl_slist* lcurl_storage_remove_slist(lua_State *L, int storage, int idx); + +void lcurl_storage_preserve_iv(lua_State *L, int storage, int i, int v); + +void lcurl_storage_remove_i(lua_State *L, int storage, int i); + +void lcurl_storage_get_i(lua_State *L, int storage, int i); + +int lcurl_storage_free(lua_State *L, int storage); + +struct curl_slist* lcurl_util_array_to_slist(lua_State *L, int t); + +struct curl_slist* lcurl_util_to_slist(lua_State *L, int t); + +void lcurl_util_slist_set(lua_State *L, int t, struct curl_slist* list); + +void lcurl_util_slist_to_table(lua_State *L, struct curl_slist* list); + +void lcurl_util_set_const(lua_State *L, const lcurl_const_t *reg); + +int lcurl_set_callback(lua_State *L, lcurl_callback_t *c, int i, const char *method); + +int lcurl_util_push_cb(lua_State *L, lcurl_callback_t *c); + +int lcurl_util_new_weak_table(lua_State*L, const char *mode); + +int lcurl_util_pcall_method(lua_State *L, const char *name, int nargs, int nresults, int errfunc); + +int lcurl_utils_apply_options(lua_State *L, int opt, int obj, int do_close, + int error_mode, int error_type, int error_code + ); + +void lcurl_stack_dump (lua_State *L); + +curl_socket_t lcurl_opt_os_socket(lua_State *L, int idx, curl_socket_t def); + +void lcurl_push_os_socket(lua_State *L, curl_socket_t fd); + +#endif