diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..9c134ec --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +target/ +.git/ +.github/ +.gitignore +.dockerignore +*.md +LICENSE +deny.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..9808469 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,68 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +env: + CARGO_TERM_COLOR: always + CARGO_INCREMENTAL: 0 + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + fmt: + name: cargo fmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.93.1 + components: rustfmt + - run: cargo fmt --all --check + + clippy-test: + name: clippy + test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.93.1 + components: clippy + - uses: Swatinem/rust-cache@v2 + - name: Install libftdi + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends libftdi1-dev pkg-config + - run: cargo clippy --all-targets --all-features -- -D warnings + - run: cargo test --all-features --no-run + - run: cargo test --all-features + + deny: + name: cargo deny + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check advisories bans licenses sources + + docker: + name: docker build (amd64) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: docker/setup-buildx-action@v3 + - uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64 + push: false + tags: naas-api:ci + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..974d111 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,71 @@ +name: Release + +on: + push: + tags: ["v*.*.*"] + +env: + IMAGE: ghcr.io/${{ github.repository }} + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + id-token: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Compute image tags + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE }} + tags: | + type=ref,event=tag + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ !contains(github.ref_name, '-') }} + labels: | + org.opencontainers.image.title=naas-api + org.opencontainers.image.description=Noise as a Service: HTTP API for the Infinite Noise tRNG + org.opencontainers.image.licenses=MIT + + - name: Build & push (linux/amd64, linux/arm64) + uses: docker/build-push-action@v6 + with: + context: . + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + provenance: true + sbom: true + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + body: | + ## Container image + + Multi-arch image (`linux/amd64`, `linux/arm64`) published to GHCR: + + ```sh + docker pull ${{ env.IMAGE }}:${{ github.ref_name }} + ``` + + See `INTEGRATION.md` for configuration and deployment notes. diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..05d085f --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +/target +*.swp +*.swo +.DS_Store +.env +.env.* +!.env.example diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b447b0b --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1430 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7aa268c23bfbbd2c4363b9cd302a4f504fb2a9dfe7e3451d66f35dd392e20aca" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", + "terminal_size", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", +] + +[[package]] +name = "h2" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "171fefbc92fe4a4de27e0698d6a5b392d6a0e333506bc49133760b3bcf948733" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", +] + +[[package]] +name = "infnoise-core" +version = "0.1.0" +source = "git+https://github.com/alpineworks/infnoise-rs?tag=v0.1.0#6747e28c6489ddb819a5a26ef844d0194a4cf38d" +dependencies = [ + "infnoise-healthcheck", + "infnoise-keccak", + "thiserror 2.0.18", +] + +[[package]] +name = "infnoise-ftdi" +version = "0.1.0" +source = "git+https://github.com/alpineworks/infnoise-rs?tag=v0.1.0#6747e28c6489ddb819a5a26ef844d0194a4cf38d" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "infnoise-healthcheck" +version = "0.1.0" +source = "git+https://github.com/alpineworks/infnoise-rs?tag=v0.1.0#6747e28c6489ddb819a5a26ef844d0194a4cf38d" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "infnoise-keccak" +version = "0.1.0" +source = "git+https://github.com/alpineworks/infnoise-rs?tag=v0.1.0#6747e28c6489ddb819a5a26ef844d0194a4cf38d" + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67df7112613f8bfd9150013a0314e196f4800d3201ae742489d999db2f979f08" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "metrics" +version = "0.24.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff56c2e7dce6bd462e3b8919986a617027481b1dcc703175b58cf9dd98a2f071" +dependencies = [ + "portable-atomic", + "rapidhash", +] + +[[package]] +name = "metrics-exporter-prometheus" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd7399781913e5393588a8d8c6a2867bf85fb38eaf2502fdce465aad2dc6f034" +dependencies = [ + "base64", + "indexmap", + "metrics", + "metrics-util", + "quanta", + "thiserror 1.0.69", +] + +[[package]] +name = "metrics-util" +version = "0.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8496cc523d1f94c1385dd8f0f0c2c480b2b8aeccb5b7e4485ad6365523ae376" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", + "hashbrown 0.15.5", + "metrics", + "quanta", + "rand", + "rand_xoshiro", + "sketches-ddsketch", +] + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mio" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1" +dependencies = [ + "libc", + "wasi", + "windows-sys", +] + +[[package]] +name = "naas-api" +version = "0.0.1" +dependencies = [ + "anyhow", + "async-channel", + "axum", + "base64", + "bytes", + "clap", + "futures", + "hex", + "infnoise-core", + "infnoise-ftdi", + "metrics", + "metrics-exporter-prometheus", + "thiserror 2.0.18", + "tokio", + "tokio-util", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quanta" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3ab5a9d756f0d97bdc89019bd2e4ea098cf9cde50ee7564dde6b81ccc8f06c7" +dependencies = [ + "crossbeam-utils", + "libc", + "once_cell", + "raw-cpuid", + "wasi", + "web-sys", + "winapi", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + +[[package]] +name = "raw-cpuid" +version = "11.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "sketches-ddsketch" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6f73aeb92d671e0cc4dca167e59b2deb6387c375391bc99ee743f326994a2b" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "terminal_size" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874" +dependencies = [ + "rustix", + "windows-sys", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-util" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68d6fdd9f81c2819c9a8b0e0cd91660e7746a8e6ea2ba7c6b2b057985f6bcb51" +dependencies = [ + "bitflags", + "bytes", + "http", + "http-body", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.1+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ace1d07c165b0864824eee619580c4689389afa9dc9ed3a4c75040d82e6790" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e68e6f4afd367a562002c05637acb8578ff2dea1943df76afb9e83d177c8578" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95a9ec35c64b2a7cb35d3fead40c4238d0940c86d107136999567a4703259f2" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.121" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4e0100b01e9f0d03189a92b96772a1fb998639d981193d7dbab487302513441" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b572dff8bcf38bad0fa19729c89bb5748b2b9b1d8be70cf90df697e3a8f32aa" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ed11fc1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,31 @@ +[workspace] +resolver = "3" +members = ["crates/naas-api"] + +[workspace.package] +version = "0.0.1" +edition = "2024" +license = "MIT" +repository = "https://github.com/alpineworks/naas-api" +authors = ["AlpineWorks"] +rust-version = "1.85" + +[workspace.lints.rust] +unsafe_code = "forbid" +missing_debug_implementations = "warn" + +[workspace.lints.clippy] +all = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } +module_name_repetitions = "allow" +missing_errors_doc = "allow" +missing_panics_doc = "allow" +must_use_candidate = "allow" +cast_possible_truncation = "allow" +cast_precision_loss = "allow" +cast_sign_loss = "allow" + +[profile.release] +lto = "thin" +codegen-units = 1 +strip = "symbols" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..8b0e19b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,28 @@ +ARG RUST_VERSION=1.93.1 +ARG DEBIAN_VERSION=bookworm + +FROM rust:${RUST_VERSION}-slim-${DEBIAN_VERSION} AS builder +RUN apt-get update && apt-get install -y --no-install-recommends \ + libftdi1-dev \ + pkg-config \ + ca-certificates \ + git \ + && rm -rf /var/lib/apt/lists/* +WORKDIR /app +COPY . . +RUN cargo build --release --bin naas-api + +FROM debian:${DEBIAN_VERSION}-slim AS runtime +RUN apt-get update && apt-get install -y --no-install-recommends \ + libftdi1-2 \ + ca-certificates \ + tini \ + && rm -rf /var/lib/apt/lists/* \ + && groupadd --system --gid 10001 naas \ + && useradd --system --uid 10001 --gid naas --shell /usr/sbin/nologin --no-create-home naas + +COPY --from=builder /app/target/release/naas-api /usr/local/bin/naas-api + +USER naas:naas +EXPOSE 8080 8081 +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/naas-api"] diff --git a/INTEGRATION.md b/INTEGRATION.md new file mode 100644 index 0000000..572a43e --- /dev/null +++ b/INTEGRATION.md @@ -0,0 +1,219 @@ +# naas-api integration plan + +HTTP API wrapping [`alpineworks/infnoise-rs`](https://github.com/alpineworks/infnoise-rs) — the Rust port of the Infinite Noise Multiplier driver — to expose a real-hardware true-RNG over the network. + +## Dependency + +`infnoise-rs` is published only on GitHub for now (no crates.io). Pull it in via git: + +```toml +[dependencies] +infnoise-core = { git = "https://github.com/alpineworks/infnoise-rs", tag = "v0.1.0" } +infnoise-ftdi = { git = "https://github.com/alpineworks/infnoise-rs", tag = "v0.1.0" } +``` + +Pin to a tag (or rev) for reproducible builds. `infnoise-core` gives `extract_bytes`, `Whitener`, and `clamp_entropy`; `infnoise-ftdi` gives `Device::open` / `Device::read_buffer`. + +We do **not** depend on the `infnoise` binary crate — naas-api owns its own read loop. + +License compatibility: infnoise-rs is CC0 (public-domain dedication), naas-api is MIT — that's fine, CC0 work can be used in any license context. + +## Throughput / latency baseline + +### Per-device hardware ceiling (upstream-reported, optimistic) +| Mode | Output rate | +|---|---| +| `--raw` | ~60 KB/s | +| `--multiplier 10` | ~600 KB/s | +| `--multiplier 100` | ~6 MB/s | +| `--multiplier 1000` | ~60 MB/s | + +### Measured against naas-api on a real device (release build, multiplier=100) +| Scenario | Throughput | +|---|---| +| Single sustained stream | ~2.5 MB/s | +| 32 × 1 MiB parallel requests | ~2.3 MB/s aggregate, 8–14 s tail latency | +| 8 concurrent streams | ~2.4 MB/s aggregate, share spread 0.9–2.1 MiB over 4 s | + +The measured ceiling matches the theoretical `multiplier=100` rate (FT240X 480 KB/s ÷ 512-byte buffer × 3200-byte output ≈ 3 MB/s) minus health-check rejections and per-buffer USB latency. Upstream's 6 MB/s figure appears optimistic for this device; **plan for ~2.4 MB/s per device**, scale by adding hardware. + +### Algorithm-only (no USB), benchmarked on M-series Mac +| Mode | Sustained | +|---|---| +| raw | 8 MB/s | +| multiplier=10 | 33 MB/s | +| multiplier=100 | 141 MB/s | +| multiplier=1000 | 189 MB/s (I/O-bound at this point) | + +**Key implication:** the algorithm is ~100× faster than what one device can feed it. CPU is never the bottleneck for sane multipliers; the FT240X's bit-bang clock (480 KB/s of FTDI bytes → 60 KB/s of raw INM) is the hard floor. Scaling is by adding devices, not by tuning code. + +USB latency: ~16 ms per 512-byte buffer at the configured baud. Producer-side queue depth dominates request latency for small reads. + +## Architecture + +``` +┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ ┌──────────────────────┐ +│ FTDI device │──▶│ Reader thread(s) │──▶│ Bounded buffer │──▶│ API listener :8080 │ +│ (one per HW) │ │ (sync, blocking) │ │ pool (1 MiB) │ │ axum, /api/v1/random │ +└──────────────┘ └──────────────────┘ └─────────────────┘ └──────────────────────┘ + │ + ▼ + ┌────────────────────────┐ + │ Ops listener :8081 │ + │ axum, /healthz /metrics│ + └────────────────────────┘ +``` + +**One reader thread per USB device.** Sync I/O (libftdi has no real async story we want to bother with). Each reader runs the same loop the `infnoise` binary does: + +```rust +device.read_buffer(&mut in_buf)?; +let entropy = extract_bytes(&mut bytes, &in_buf, &mut hc)?; +if hc.ok_to_use_data() && hc.entropy_on_target(entropy, BUFLEN) { + let entropy_clamped = clamp_entropy(entropy, hc.expected_entropy_per_bit()); + let n = whitener.process(&bytes, &mut out, entropy_clamped, mode); + pool.push_blocking(&out[..n]); + while whitener.bytes_pending() > 0 { + let n = whitener.squeeze_next(&mut out); + pool.push_blocking(&out[..n]); + } +} +``` + +**Bounded buffer pool** — fixed-size, default 1 MiB, `async-channel::bounded::` (mpmc; sync `send_blocking` for readers, async `recv` for handlers). Backpressure: readers block on push when full (natural rate-limiting); handlers `await` on pull when empty. + +**Two axum listeners on one tokio runtime.** Public API on 8080, ops (health + metrics) on 8081. `tokio::join!` both `serve()` futures; signal handler triggers graceful shutdown on each. + +**Multi-device fan-in** for scaling: N devices → N reader threads → one shared pool. Scaling is linear. + +## API surface + +**API listener — `0.0.0.0:8080`:** + +| Method | Path | Behavior | +|---|---|---| +| `GET` | `/api/v1/random/` | Return exactly `n` bytes as `application/octet-stream`. 400 if `n` > 1 MiB. | +| `GET` | `/api/v1/random/stream` | `Transfer-Encoding: chunked` octet-stream. Streams from the pool until the client disconnects. For sustained consumers. | +| `GET` | `/api/v1/random/hex/` | Same bytes, hex-encoded `text/plain; charset=us-ascii`. 1 MiB cap on the underlying byte count. | +| `GET` | `/api/v1/random/base64/` | Same bytes, standard base64 `text/plain; charset=us-ascii`. 1 MiB cap on the underlying byte count. | + +**Ops listener — `0.0.0.0:8081`:** + +| Method | Path | Behavior | +|---|---|---| +| `GET` | `/healthz` | 200 if at least one device is producing entropy within the last health-window; 503 if all readers are stuck. | +| `GET` | `/metrics` | Prometheus exposition format. Per-device labels by FTDI serial: pool depth, entropy bytes pushed, healthcheck rejections, USB error counts. | + +No authentication in v1 (private-network deployment is the assumed posture). API-key middleware is a v1.1 candidate. + +## Configuration + +All settings via env vars with matching CLI flags (clap derive + `env` feature). CLI flags override env. + +| Env var | CLI flag | Default | Notes | +|---|---|---|---| +| `NAAS_API_ADDR` | `--api-addr` | `0.0.0.0:8080` | Public API listener. | +| `NAAS_OPS_ADDR` | `--ops-addr` | `0.0.0.0:8081` | Health + metrics listener. | +| `NAAS_MULTIPLIER` | `--multiplier` | `100` | INM whitener multiplier. Supported: 1, 10, 100, 1000. | +| `NAAS_POOL_BYTES` | `--pool-bytes` | `1048576` | Bounded pool capacity. | +| `NAAS_MAX_REQUEST_BYTES` | `--max-request-bytes` | `1048576` | Per-request cap on the bounded `random` endpoints. | +| `NAAS_SERIAL_ALLOWLIST` | `--serial-allowlist` | *(unset)* | Comma-separated FTDI serials. Unset = scan and use all detected devices. | +| `NAAS_LOG_LEVEL` | `--log-level` | `info` | `tracing` env-filter syntax (e.g. `info,naas_api=debug`). | + +Logging: always JSON via `tracing-subscriber` `fmt().json()`. One stream to stdout. + +## Resolved decisions + +| Topic | Decision | +|---|---| +| HTTP framework | `axum` on `tokio` multi-thread runtime | +| Streaming response | Chunked `application/octet-stream` (no SSE) | +| Pool implementation | `async-channel::bounded::` (mpmc, sync↔async bridge) | +| Multi-device discovery | Scan FTDI devices at boot; optional `NAAS_SERIAL_ALLOWLIST` | +| Linux `RNDADDENTROPY` feed | Out of scope (separate daemon if ever revisited) | +| Metrics granularity | Per-device, labeled by serial | +| Graceful shutdown | Close listeners on SIGTERM/Ctrl-C, drop readers, exit | +| Container base | `debian:bookworm-slim` + `libftdi1` from apt | +| Architectures | `linux/amd64`, `linux/arm64` | +| Auth | None in v1 | +| Multiplier scope | Server config only | +| Per-request cap | 1 MiB on bounded endpoints; use `/stream` for sustained consumers | +| Output formats | binary, hex, base64 | +| Listener layout | API on 8080, ops on 8081 | + +## Test surface + +We don't re-validate bit-extraction, healthcheck, or whitening — `infnoise-rs` does that against the C reference. naas-api's own test surface: + +- HTTP layer: handlers return correct content-type, length, status codes; cap enforcement; hex/base64 encoding round-trips. +- Pool / backpressure semantics: readers block on push when full, handlers wait or 503 when empty (configurable behavior, default: short bounded wait then 503). +- Concurrent-request behavior: no torn reads, no double-served bytes, fairness across in-flight handlers (a single big request shouldn't starve a stream of small ones). +- Graceful device disconnect/reconnect: reader thread exits cleanly, supervisor restarts after backoff, health flips to degraded when no producer is live. +- Streaming: client disconnect doesn't leak buffers; backpressure on slow consumers. + +Hardware is not required for any of these — fake the producer with a deterministic byte source for the HTTP/pool tests. + +## Release & packaging + +**Versioning:** SemVer. Releases tagged `vX.Y.Z`. + +**Release workflow** (`.github/workflows/release.yml`, on tag push `v*`): +1. `docker/setup-qemu-action` + `docker/setup-buildx-action`. +2. `docker/login-action` against `ghcr.io` with `GITHUB_TOKEN`. +3. `docker/build-push-action` with `platforms: linux/amd64,linux/arm64`, tags `ghcr.io/alpineworks/naas-api:vX.Y.Z`, plus `:latest` only on non-prerelease tags. +4. `gh release create` with auto-generated notes; body links the GHCR image and `docker pull` snippet. + +**CI workflow** (`.github/workflows/ci.yml`, on push / PR): +- `cargo fmt --check` +- `cargo clippy --all-targets -- -D warnings` +- `cargo test --all-features` +- `cargo deny check` (advisories + licenses + bans) +- `docker buildx build --platform linux/amd64` (no push) to catch Dockerfile drift + +Runner: `ubuntu-latest`. macOS jobs optional once cross-platform dev becomes a real need. + +## Rust standards + +- Edition 2024, latest stable rustc; pin via `rust-toolchain.toml`. +- `clippy` baseline `-D warnings`; opt into `clippy::pedantic` per module where it pulls its weight. +- Errors: `thiserror` for typed boundaries (`ConfigError`, `DeviceError`, `PoolError`); `anyhow::Result` at `main` only. +- Async: `tokio` multi-thread runtime for HTTP; readers stay sync on `std::thread`. +- Buffers: `bytes::Bytes` end-to-end so axum response bodies hand them out without copying. +- No `unsafe` outside what `infnoise-ftdi` already encapsulates. +- Layout: workspace `Cargo.toml` with a single `naas-api` binary crate (room to split into a lib later without restructuring imports). + +## What's already proven in infnoise-rs + +We don't need to re-validate any of this in naas-api — it's covered by the upstream cross-validation: + +- Bit-extraction, healthcheck, and Keccak whitening produce **byte-identical output to the C reference** across raw + AllEntropy + multiplier {1, 2, 3, 5, 10}. +- Verified against both synthetic INM data and 256 buffers (~131 KB) of real INM hardware noise checked into `infnoise-rs/tests/fixtures/hw-capture.bin`. +- CI runs the full cross-validation on every push (Linux + macOS). + +## Suggested first commit + +1. Workspace `Cargo.toml` declaring `naas-api` as a single binary crate, edition 2024. `rust-toolchain.toml` pinning latest stable. +2. Deps: `axum`, `tokio` (`rt-multi-thread`, `macros`, `signal`), `tokio-util` (`CancellationToken`), `tower-http` (`trace`), `async-channel`, `bytes`, `futures`, `clap` (`derive`, `env`), `tracing`, `tracing-subscriber` (`env-filter`, `json`), `metrics` + `metrics-exporter-prometheus`, `hex`, `base64`, `thiserror`, `anyhow`, `infnoise-core` (git tag), `infnoise-ftdi` (git tag). +3. `src/main.rs` skeleton: + - `Config` parsed via `clap`. + - Per-detected (or allow-listed) FTDI device: spawn one `std::thread` reader pushing `Bytes` into a shared `crossbeam_channel`. + - Two `axum::Router`s — API on 8080 (`/api/v1/random/*`), ops on 8081 (`/healthz`, `/metrics`). + - `tokio::join!` both servers under graceful-shutdown wired to `tokio::signal::ctrl_c` + SIGTERM. +4. `Dockerfile`: `cargo-chef` two-stage build → runtime on `debian:bookworm-slim` with `libftdi1` apt-installed; `USER` non-root; `EXPOSE 8080 8081`. +5. `.github/workflows/ci.yml`: fmt + clippy + test + cargo-deny + Docker build (no push). +6. `.github/workflows/release.yml`: on tag `v*`, multi-arch buildx + push to `ghcr.io/alpineworks/naas-api` + `gh release create`. + +That gets us to a working end-to-end demo. Explicit v1 non-goals: auth, request-id propagation, OpenTelemetry traces, Linux kernel-pool feed, JSON/UUID/integer output formats, per-request multiplier override. + +## Known v1 limitations (deferred fixes) + +Surfaced during initial load testing; safe for the documented private-network deployment, **must address before public exposure**: + +1. **No pull timeout.** `pool.pull()` and `/random/stream` await indefinitely on `async-channel::recv`. If the device disappears and the pool drains, in-flight handlers hang until the client times out. The reader supervisor retries `Device::open()` every 2 s, but persistent failure leaves handlers blocked. Fix: wrap `pool.pull()` in `tokio::time::timeout` keyed off `NAAS_REQUEST_TIMEOUT`; return 503 on expiry. +2. **No request-concurrency cap.** All inbound requests are accepted regardless of count. Each holds a `BytesMut::with_capacity(n)` allocation while waiting; pathological clients can pre-allocate gigabytes. Fix: `tower::limit::ConcurrencyLimitLayer` on the API router (e.g. 256 in flight). +3. **No per-IP rate limiting.** A single client can drain the pool and starve others. Fix: `tower_governor` or similar per-`X-Forwarded-For` (or peer addr) limiter, configurable by env. + +## Bugs found and fixed during initial integration + +- **`metrics::counter!()` does not accumulate** when called inline with the same name+labels each time — the macro returns a fresh handle whose increments don't sum into the registered atomic. Fix: cache `Counter` handles once at startup and call `.increment()` on the cached value. Applied in `reader.rs` (`DeviceMetrics`) and `handlers.rs` (`ResponseMetrics`). All future counter use must follow this pattern. +- **`/healthz` was 503 when the pool was full** because the staleness check ignored "reader parked because pool is at capacity". Fix: `pending_chunks > 0` → 200 first, then fall through to the staleness check. diff --git a/crates/naas-api/Cargo.toml b/crates/naas-api/Cargo.toml new file mode 100644 index 0000000..aa04495 --- /dev/null +++ b/crates/naas-api/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "naas-api" +description = "Noise as a Service: HTTP API for the Infinite Noise tRNG" +version.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +authors.workspace = true +rust-version.workspace = true + +[lints] +workspace = true + +[dependencies] +axum = { version = "0.8", features = ["macros", "http2"] } +tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal", "sync", "time", "net"] } +tokio-util = { version = "0.7", features = ["rt"] } +tower = "0.5" +tower-http = { version = "0.6", features = ["trace"] } +bytes = "1" +async-channel = "2" +clap = { version = "4", features = ["derive", "env", "wrap_help"] } +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json", "fmt"] } +metrics = "0.24" +metrics-exporter-prometheus = { version = "0.16", default-features = false } +hex = "0.4" +base64 = "0.22" +thiserror = "2" +anyhow = "1" +futures = "0.3" +infnoise-core = { git = "https://github.com/alpineworks/infnoise-rs", tag = "v0.1.0", version = "0.1.0" } +infnoise-ftdi = { git = "https://github.com/alpineworks/infnoise-rs", tag = "v0.1.0", version = "0.1.0" } diff --git a/crates/naas-api/src/config.rs b/crates/naas-api/src/config.rs new file mode 100644 index 0000000..fcacb12 --- /dev/null +++ b/crates/naas-api/src/config.rs @@ -0,0 +1,62 @@ +use std::net::SocketAddr; + +use clap::Parser; + +#[derive(Debug, Clone, Parser)] +#[command( + name = "naas-api", + version, + about = "Noise as a Service: HTTP API for the Infinite Noise tRNG", + long_about = None, +)] +pub struct Config { + #[arg(long, env = "NAAS_API_ADDR", default_value = "0.0.0.0:8080")] + pub api_addr: SocketAddr, + + #[arg(long, env = "NAAS_OPS_ADDR", default_value = "0.0.0.0:8081")] + pub ops_addr: SocketAddr, + + #[arg( + long, + env = "NAAS_MULTIPLIER", + default_value_t = 100, + value_parser = parse_multiplier, + )] + pub multiplier: u32, + + #[arg(long, env = "NAAS_POOL_BYTES", default_value_t = 1_048_576)] + pub pool_bytes: usize, + + #[arg(long, env = "NAAS_MAX_REQUEST_BYTES", default_value_t = 1_048_576)] + pub max_request_bytes: usize, + + #[arg(long, env = "NAAS_SERIAL_ALLOWLIST", value_delimiter = ',')] + pub serial_allowlist: Vec, + + #[arg(long, env = "NAAS_LOG_LEVEL", default_value = "info")] + pub log_level: String, +} + +impl Config { + pub fn pool_chunks(&self) -> usize { + const MAX_CHUNK_BYTES: usize = 128; + (self.pool_bytes / MAX_CHUNK_BYTES).max(64) + } + + pub fn process_mode(&self) -> infnoise_core::ProcessMode { + match self.multiplier { + 0 => infnoise_core::ProcessMode::AllEntropy, + m => infnoise_core::ProcessMode::Multiplier(m), + } + } +} + +fn parse_multiplier(s: &str) -> Result { + let n: u32 = s + .parse() + .map_err(|e: std::num::ParseIntError| e.to_string())?; + match n { + 0 | 1 | 10 | 100 | 1000 => Ok(n), + _ => Err("multiplier must be one of: 0, 1, 10, 100, 1000".into()), + } +} diff --git a/crates/naas-api/src/error.rs b/crates/naas-api/src/error.rs new file mode 100644 index 0000000..dee9b28 --- /dev/null +++ b/crates/naas-api/src/error.rs @@ -0,0 +1,27 @@ +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; +use thiserror::Error; + +#[derive(Debug, Error)] +pub enum ServiceError { + #[error("requested {requested} bytes exceeds max of {max}")] + RequestTooLarge { requested: usize, max: usize }, + + #[error("requested zero bytes")] + EmptyRequest, + + #[error("entropy pool closed")] + PoolClosed, +} + +impl IntoResponse for ServiceError { + fn into_response(self) -> Response { + let status = match self { + ServiceError::RequestTooLarge { .. } | ServiceError::EmptyRequest => { + StatusCode::BAD_REQUEST + } + ServiceError::PoolClosed => StatusCode::SERVICE_UNAVAILABLE, + }; + (status, self.to_string()).into_response() + } +} diff --git a/crates/naas-api/src/handlers.rs b/crates/naas-api/src/handlers.rs new file mode 100644 index 0000000..47899b4 --- /dev/null +++ b/crates/naas-api/src/handlers.rs @@ -0,0 +1,139 @@ +use axum::Router; +use axum::body::Body; +use axum::extract::{Path, State}; +use axum::http::{HeaderValue, StatusCode, header}; +use axum::response::{IntoResponse, Response}; +use axum::routing::get; +use base64::Engine; +use bytes::Bytes; +use futures::stream; +use metrics::Counter; +use tower_http::trace::TraceLayer; + +use crate::error::ServiceError; +use crate::pool::Pool; + +#[derive(Clone)] +struct ApiState { + pool: Pool, + max_bytes: usize, + metrics: ResponseMetrics, +} + +#[derive(Clone)] +struct ResponseMetrics { + binary: Counter, + hex: Counter, + base64: Counter, + stream: Counter, +} + +impl ResponseMetrics { + fn new() -> Self { + Self { + binary: metrics::counter!("naas_response_bytes_total", "format" => "binary"), + hex: metrics::counter!("naas_response_bytes_total", "format" => "hex"), + base64: metrics::counter!("naas_response_bytes_total", "format" => "base64"), + stream: metrics::counter!("naas_response_bytes_total", "format" => "stream"), + } + } +} + +pub fn router(pool: Pool, max_bytes: usize) -> Router { + let state = ApiState { + pool, + max_bytes, + metrics: ResponseMetrics::new(), + }; + Router::new() + .route("/api/v1/random/stream", get(random_stream)) + .route("/api/v1/random/{n}", get(random_bytes)) + .route("/api/v1/random/hex/{n}", get(random_hex)) + .route("/api/v1/random/base64/{n}", get(random_base64)) + .layer(TraceLayer::new_for_http()) + .with_state(state) +} + +fn check_n(n: usize, max: usize) -> Result<(), ServiceError> { + if n == 0 { + return Err(ServiceError::EmptyRequest); + } + if n > max { + return Err(ServiceError::RequestTooLarge { requested: n, max }); + } + Ok(()) +} + +async fn random_bytes( + State(state): State, + Path(n): Path, +) -> Result { + check_n(n, state.max_bytes)?; + let bytes = state.pool.pull(n).await?; + state.metrics.binary.increment(n as u64); + Ok(( + [( + header::CONTENT_TYPE, + HeaderValue::from_static("application/octet-stream"), + )], + bytes, + ) + .into_response()) +} + +async fn random_hex( + State(state): State, + Path(n): Path, +) -> Result { + check_n(n, state.max_bytes)?; + let bytes = state.pool.pull(n).await?; + state.metrics.hex.increment(n as u64); + Ok(( + [( + header::CONTENT_TYPE, + HeaderValue::from_static("text/plain; charset=us-ascii"), + )], + hex::encode(&bytes), + ) + .into_response()) +} + +async fn random_base64( + State(state): State, + Path(n): Path, +) -> Result { + check_n(n, state.max_bytes)?; + let bytes = state.pool.pull(n).await?; + state.metrics.base64.increment(n as u64); + let body = base64::engine::general_purpose::STANDARD.encode(&bytes); + Ok(( + [( + header::CONTENT_TYPE, + HeaderValue::from_static("text/plain; charset=us-ascii"), + )], + body, + ) + .into_response()) +} + +async fn random_stream(State(state): State) -> Response { + let pool = state.pool.clone(); + let counter = state.metrics.stream.clone(); + let body_stream = stream::unfold((pool, counter), |(pool, counter)| async move { + match pool.pull_chunk().await { + Ok(chunk) => { + counter.increment(chunk.len() as u64); + Some(( + Ok::(chunk), + (pool, counter), + )) + } + Err(_) => None, + } + }); + Response::builder() + .status(StatusCode::OK) + .header(header::CONTENT_TYPE, "application/octet-stream") + .body(Body::from_stream(body_stream)) + .expect("static response builder") +} diff --git a/crates/naas-api/src/main.rs b/crates/naas-api/src/main.rs new file mode 100644 index 0000000..febfaea --- /dev/null +++ b/crates/naas-api/src/main.rs @@ -0,0 +1,130 @@ +use std::process::ExitCode; + +use anyhow::{Context, Result}; +use clap::Parser; +use tokio_util::sync::CancellationToken; + +mod config; +mod error; +mod handlers; +mod ops; +mod pool; +mod reader; +mod telemetry; + +use config::Config; + +fn main() -> ExitCode { + let cfg = Config::parse(); + + if let Err(e) = telemetry::init_tracing(&cfg) { + eprintln!("failed to initialize tracing: {e:#}"); + return ExitCode::from(1); + } + + let runtime = match tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + { + Ok(rt) => rt, + Err(e) => { + tracing::error!(error = %e, "failed to build tokio runtime"); + return ExitCode::from(1); + } + }; + + match runtime.block_on(run(cfg)) { + Ok(()) => ExitCode::SUCCESS, + Err(e) => { + tracing::error!(error = ?e, "fatal: {e:#}"); + ExitCode::from(1) + } + } +} + +async fn run(cfg: Config) -> Result<()> { + tracing::info!( + version = env!("CARGO_PKG_VERSION"), + api_addr = %cfg.api_addr, + ops_addr = %cfg.ops_addr, + multiplier = cfg.multiplier, + pool_bytes = cfg.pool_bytes, + max_request_bytes = cfg.max_request_bytes, + serial_count = cfg.serial_allowlist.len(), + "starting naas-api", + ); + + let metrics_handle = telemetry::install_metrics()?; + let shutdown = CancellationToken::new(); + let pool = pool::Pool::new(cfg.pool_chunks()); + + let reader_handles = + reader::spawn_all(&cfg, &pool.sender(), &shutdown).context("spawning reader threads")?; + + let api_router = handlers::router(pool.clone(), cfg.max_request_bytes); + let ops_router = ops::router(pool.clone(), metrics_handle); + + let api_listener = tokio::net::TcpListener::bind(cfg.api_addr) + .await + .with_context(|| format!("binding API listener to {}", cfg.api_addr))?; + let ops_listener = tokio::net::TcpListener::bind(cfg.ops_addr) + .await + .with_context(|| format!("binding ops listener to {}", cfg.ops_addr))?; + + tracing::info!(addr = %cfg.api_addr, "api listening"); + tracing::info!(addr = %cfg.ops_addr, "ops listening"); + + let signal_token = shutdown.clone(); + tokio::spawn(async move { + wait_for_signal().await; + tracing::info!("shutdown signal received"); + signal_token.cancel(); + }); + + let api_shutdown = shutdown.clone(); + let ops_shutdown = shutdown.clone(); + + let api_server = axum::serve(api_listener, api_router) + .with_graceful_shutdown(async move { api_shutdown.cancelled().await }); + let ops_server = axum::serve(ops_listener, ops_router) + .with_graceful_shutdown(async move { ops_shutdown.cancelled().await }); + + let (api_res, ops_res) = tokio::join!(api_server, ops_server); + api_res.context("api server error")?; + ops_res.context("ops server error")?; + + pool.close(); + + tracing::info!("waiting for reader threads to drain"); + for h in reader_handles { + if let Err(e) = h.join() { + tracing::error!(panic = ?e, "reader thread panicked"); + } + } + + tracing::info!("shutdown complete"); + Ok(()) +} + +async fn wait_for_signal() { + #[cfg(unix)] + { + use tokio::signal::unix::{SignalKind, signal}; + match signal(SignalKind::terminate()) { + Ok(mut term) => { + tokio::select! { + _ = tokio::signal::ctrl_c() => {} + _ = term.recv() => {} + } + } + Err(e) => { + tracing::warn!(error = %e, "could not install SIGTERM handler; using ctrl_c only"); + let _ = tokio::signal::ctrl_c().await; + } + } + } + #[cfg(not(unix))] + { + let _ = tokio::signal::ctrl_c().await; + } +} diff --git a/crates/naas-api/src/ops.rs b/crates/naas-api/src/ops.rs new file mode 100644 index 0000000..aa19aff --- /dev/null +++ b/crates/naas-api/src/ops.rs @@ -0,0 +1,68 @@ +use std::time::{SystemTime, UNIX_EPOCH}; + +use axum::Router; +use axum::extract::State; +use axum::http::{StatusCode, header}; +use axum::response::IntoResponse; +use axum::routing::get; +use metrics_exporter_prometheus::PrometheusHandle; + +use crate::pool::Pool; + +const HEALTH_STALE_MS: u64 = 10_000; + +#[derive(Clone)] +struct OpsState { + pool: Pool, + metrics: PrometheusHandle, +} + +impl std::fmt::Debug for OpsState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OpsState") + .field("pool", &self.pool) + .finish_non_exhaustive() + } +} + +pub fn router(pool: Pool, metrics: PrometheusHandle) -> Router { + let state = OpsState { pool, metrics }; + Router::new() + .route("/healthz", get(health)) + .route("/metrics", get(metrics_endpoint)) + .with_state(state) +} + +async fn health(State(state): State) -> impl IntoResponse { + if state.pool.pending_chunks() > 0 { + return (StatusCode::OK, "ok\n"); + } + let last = state.pool.last_push_ms(); + if last == 0 { + return (StatusCode::SERVICE_UNAVAILABLE, "starting up\n"); + } + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + if now.saturating_sub(last) > HEALTH_STALE_MS { + return ( + StatusCode::SERVICE_UNAVAILABLE, + "no entropy producer healthy\n", + ); + } + (StatusCode::OK, "ok\n") +} + +async fn metrics_endpoint(State(state): State) -> impl IntoResponse { + metrics::gauge!("naas_pool_chunks_pending").set(state.pool.pending_chunks() as f64); + metrics::gauge!("naas_pool_chunks_capacity").set(state.pool.capacity_chunks() as f64); + let body = state.metrics.render(); + ( + [( + header::CONTENT_TYPE, + "text/plain; version=0.0.4; charset=utf-8", + )], + body, + ) +} diff --git a/crates/naas-api/src/pool.rs b/crates/naas-api/src/pool.rs new file mode 100644 index 0000000..1958b77 --- /dev/null +++ b/crates/naas-api/src/pool.rs @@ -0,0 +1,92 @@ +use std::sync::Arc; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use async_channel::{Receiver, Sender}; +use bytes::{Bytes, BytesMut}; + +use crate::error::ServiceError; + +#[derive(Clone, Debug)] +pub struct Pool { + tx: Sender, + rx: Receiver, + last_push_ms: Arc, +} + +impl Pool { + pub fn new(capacity_chunks: usize) -> Self { + let (tx, rx) = async_channel::bounded(capacity_chunks); + Self { + tx, + rx, + last_push_ms: Arc::new(AtomicU64::new(0)), + } + } + + pub fn sender(&self) -> PoolSender { + PoolSender { + tx: self.tx.clone(), + last_push_ms: self.last_push_ms.clone(), + } + } + + pub async fn pull(&self, n: usize) -> Result { + let mut out = BytesMut::with_capacity(n); + while out.len() < n { + let chunk = self.rx.recv().await.map_err(|_| ServiceError::PoolClosed)?; + let need = n - out.len(); + if chunk.len() <= need { + out.extend_from_slice(&chunk); + } else { + out.extend_from_slice(&chunk[..need]); + } + } + Ok(out.freeze()) + } + + pub async fn pull_chunk(&self) -> Result { + self.rx.recv().await.map_err(|_| ServiceError::PoolClosed) + } + + pub fn pending_chunks(&self) -> usize { + self.rx.len() + } + + pub fn capacity_chunks(&self) -> usize { + self.rx.capacity().unwrap_or(0) + } + + pub fn last_push_ms(&self) -> u64 { + self.last_push_ms.load(Ordering::Relaxed) + } + + pub fn close(&self) { + self.tx.close(); + self.rx.close(); + } +} + +#[derive(Clone, Debug)] +pub struct PoolSender { + tx: Sender, + last_push_ms: Arc, +} + +impl PoolSender { + pub fn push_blocking(&self, bytes: Bytes) -> Result<(), ()> { + if self.tx.send_blocking(bytes).is_err() { + return Err(()); + } + let now = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as u64) + .unwrap_or(0); + self.last_push_ms.store(now, Ordering::Relaxed); + Ok(()) + } + + pub fn is_closed(&self) -> bool { + self.tx.is_closed() + } +} diff --git a/crates/naas-api/src/reader.rs b/crates/naas-api/src/reader.rs new file mode 100644 index 0000000..42a5089 --- /dev/null +++ b/crates/naas-api/src/reader.rs @@ -0,0 +1,170 @@ +use std::thread::{self, JoinHandle}; +use std::time::Duration; + +use bytes::Bytes; +use metrics::Counter; +use thiserror::Error; +use tokio_util::sync::CancellationToken; + +use infnoise_core::{ + BUFLEN, ExtractError, HealthCheck, HealthCheckError, ProcessMode, Whitener, clamp_entropy, + extract_bytes, +}; +use infnoise_ftdi::{Device, FtdiError}; + +use crate::config::Config; +use crate::pool::PoolSender; + +const PREDICTION_BITS: u8 = 14; +const DESIGN_K: f64 = 1.84; +const WARMUP_LIMIT: u32 = 5_000; +const RECONNECT_BACKOFF: Duration = Duration::from_secs(2); + +#[derive(Debug, Error)] +pub enum ReaderError { + #[error("ftdi: {0}")] + Ftdi(#[from] FtdiError), + #[error("health check init failed: {0}")] + HealthInit(HealthCheckError), + #[error("extract: {0}")] + Extract(#[from] ExtractError), + #[error("warmup exceeded {0} rounds without health checker accepting data")] + WarmupTimeout(u32), +} + +pub fn spawn_all( + cfg: &Config, + sender: &PoolSender, + shutdown: &CancellationToken, +) -> std::io::Result>> { + let serials: Vec> = if cfg.serial_allowlist.is_empty() { + vec![None] + } else { + cfg.serial_allowlist + .iter() + .map(|s| Some(s.clone())) + .collect() + }; + + let mode = cfg.process_mode(); + let mut handles = Vec::with_capacity(serials.len()); + + for serial in serials { + let label = serial.clone().unwrap_or_else(|| "default".to_string()); + let sender = sender.clone(); + let shutdown = shutdown.clone(); + let h = thread::Builder::new() + .name(format!("infnoise:{label}")) + .spawn(move || { + supervise(serial.as_deref(), &label, mode, &sender, &shutdown); + })?; + handles.push(h); + } + Ok(handles) +} + +struct DeviceMetrics { + entropy_bytes: Counter, + health_rejections: Counter, + reader_errors: Counter, +} + +impl DeviceMetrics { + fn new(label: &str) -> Self { + let device = label.to_string(); + Self { + entropy_bytes: metrics::counter!("naas_entropy_bytes_total", "device" => device.clone()), + health_rejections: metrics::counter!("naas_health_rejections_total", "device" => device.clone()), + reader_errors: metrics::counter!("naas_reader_errors_total", "device" => device), + } + } +} + +fn supervise( + serial: Option<&str>, + label: &str, + mode: ProcessMode, + sender: &PoolSender, + shutdown: &CancellationToken, +) { + let m = DeviceMetrics::new(label); + while !shutdown.is_cancelled() && !sender.is_closed() { + match read_loop(serial, label, mode, sender, shutdown, &m) { + Ok(()) => break, + Err(e) => { + m.reader_errors.increment(1); + tracing::error!(device = %label, error = %e, "reader loop exited; backing off"); + if shutdown.is_cancelled() { + break; + } + thread::sleep(RECONNECT_BACKOFF); + } + } + } + tracing::info!(device = %label, "reader thread exiting"); +} + +fn read_loop( + serial: Option<&str>, + label: &str, + mode: ProcessMode, + sender: &PoolSender, + shutdown: &CancellationToken, + m: &DeviceMetrics, +) -> Result<(), ReaderError> { + tracing::info!(device = %label, ?serial, "opening device"); + let mut device = Device::open(serial)?; + let mut hc = HealthCheck::new(PREDICTION_BITS, DESIGN_K).map_err(ReaderError::HealthInit)?; + let mut whitener = Whitener::new(); + + let mut in_buf = [0u8; BUFLEN]; + let mut bytes = [0u8; BUFLEN / 8]; + let mut out = [0u8; 128]; + + let mut warmup_rounds = 0u32; + while !hc.ok_to_use_data() { + if shutdown.is_cancelled() { + return Ok(()); + } + device.read_buffer(&mut in_buf)?; + let _ = extract_bytes(&mut bytes, &in_buf, &mut hc)?; + warmup_rounds += 1; + if warmup_rounds > WARMUP_LIMIT { + return Err(ReaderError::WarmupTimeout(WARMUP_LIMIT)); + } + } + tracing::info!(device = %label, warmup_rounds, "device warmed up"); + + while !shutdown.is_cancelled() && !sender.is_closed() { + if whitener.bytes_pending() > 0 { + let n = whitener.squeeze_next(&mut out); + if !push(sender, &out[..n], &m.entropy_bytes) { + return Ok(()); + } + continue; + } + + device.read_buffer(&mut in_buf)?; + let entropy = extract_bytes(&mut bytes, &in_buf, &mut hc)?; + if !hc.ok_to_use_data() || !hc.entropy_on_target(entropy, BUFLEN as u32) { + m.health_rejections.increment(1); + continue; + } + let entropy_clamped = clamp_entropy(entropy, hc.expected_entropy_per_bit()); + let n = whitener.process(&bytes, &mut out, entropy_clamped, mode); + if n > 0 && !push(sender, &out[..n], &m.entropy_bytes) { + return Ok(()); + } + } + Ok(()) +} + +fn push(sender: &PoolSender, data: &[u8], entropy_counter: &Counter) -> bool { + let bytes = Bytes::copy_from_slice(data); + let len = bytes.len() as u64; + if sender.push_blocking(bytes).is_err() { + return false; + } + entropy_counter.increment(len); + true +} diff --git a/crates/naas-api/src/telemetry.rs b/crates/naas-api/src/telemetry.rs new file mode 100644 index 0000000..ca150d6 --- /dev/null +++ b/crates/naas-api/src/telemetry.rs @@ -0,0 +1,27 @@ +use anyhow::{Context, Result}; +use metrics_exporter_prometheus::{PrometheusBuilder, PrometheusHandle}; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; + +use crate::config::Config; + +pub fn init_tracing(cfg: &Config) -> Result<()> { + let filter = EnvFilter::try_new(&cfg.log_level).or_else(|_| EnvFilter::try_new("info"))?; + let layer = tracing_subscriber::fmt::layer() + .json() + .with_current_span(false) + .with_span_list(false); + tracing_subscriber::registry() + .with(filter) + .with(layer) + .try_init() + .context("install tracing subscriber")?; + Ok(()) +} + +pub fn install_metrics() -> Result { + PrometheusBuilder::new() + .install_recorder() + .context("install prometheus recorder") +} diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..6521556 --- /dev/null +++ b/deny.toml @@ -0,0 +1,24 @@ +[advisories] +yanked = "deny" +ignore = [] + +[licenses] +allow = [ + "MIT", + "Apache-2.0", + "BSD-2-Clause", + "BSD-3-Clause", + "Unicode-3.0", + "CC0-1.0", + "Zlib", +] +confidence-threshold = 0.93 + +[bans] +multiple-versions = "warn" +wildcards = "deny" + +[sources] +unknown-registry = "deny" +unknown-git = "deny" +allow-git = ["https://github.com/alpineworks/infnoise-rs"] diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..4eff4b4 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.93.1" +components = ["rustfmt", "clippy"] +profile = "minimal"