From 4ccf2b0437413732db352a4a0e00c02c97eb74d9 Mon Sep 17 00:00:00 2001 From: Eugene Pankov Date: Sun, 10 Apr 2022 22:58:58 +0200 Subject: [PATCH] import --- .cargo/config | 2 + .env | 1 + .github/workflows/build.yml | 67 + .github/workflows/dependency-review.yml | 20 + .github/workflows/release.yml | 18 + .gitignore | 20 + Cargo.lock | 4312 +++++++++++++++++ Cargo.toml | 15 + LICENSE | 201 + README.md | 33 + deny.toml | 198 + justfile | 27 + russh | 1 + rust-toolchain.toml | 2 + rustfmt.toml | 1 + warpgate-admin/Cargo.toml | 28 + warpgate-admin/app/.editorconfig | 9 + warpgate-admin/app/.eslintrc.yaml | 140 + warpgate-admin/app/.gitignore | 28 + warpgate-admin/app/index.html | 13 + warpgate-admin/app/openapi-schema.json | 669 +++ warpgate-admin/app/openapitools.json | 7 + warpgate-admin/app/package.json | 51 + warpgate-admin/app/public/assets/logo.svg | 42 + warpgate-admin/app/src/App.svelte | 136 + warpgate-admin/app/src/CreateTicket.svelte | 108 + warpgate-admin/app/src/Home.svelte | 110 + warpgate-admin/app/src/Login.svelte | 80 + warpgate-admin/app/src/Recording.svelte | 49 + warpgate-admin/app/src/RelativeDate.svelte | 6 + warpgate-admin/app/src/SSH.svelte | 73 + warpgate-admin/app/src/Session.svelte | 123 + warpgate-admin/app/src/Targets.svelte | 111 + warpgate-admin/app/src/Tickets.svelte | 71 + warpgate-admin/app/src/assets/svelte.png | Bin 0 -> 5185 bytes warpgate-admin/app/src/lib/api.ts | 8 + warpgate-admin/app/src/lib/ssh.ts | 5 + warpgate-admin/app/src/lib/store.ts | 3 + warpgate-admin/app/src/lib/time.ts | 5 + warpgate-admin/app/src/main.ts | 9 + warpgate-admin/app/src/theme.scss | 92 + warpgate-admin/app/src/vars.scss | 11 + warpgate-admin/app/src/vite-env.d.ts | 5 + warpgate-admin/app/svelte.config.js | 13 + warpgate-admin/app/tsconfig.json | 37 + warpgate-admin/app/tsconfig.node.json | 8 + warpgate-admin/app/vite.config.ts | 16 + warpgate-admin/app/yarn.lock | 2607 ++++++++++ warpgate-admin/src/api/auth.rs | 74 + warpgate-admin/src/api/info.rs | 30 + warpgate-admin/src/api/known_hosts_detail.rs | 56 + warpgate-admin/src/api/known_hosts_list.rs | 43 + warpgate-admin/src/api/mod.rs | 12 + warpgate-admin/src/api/recordings_detail.rs | 191 + warpgate-admin/src/api/sessions_detail.rs | 111 + warpgate-admin/src/api/sessions_list.rs | 73 + warpgate-admin/src/api/ssh_keys.rs | 54 + warpgate-admin/src/api/targets_list.rs | 33 + warpgate-admin/src/api/tickets_detail.rs | 57 + warpgate-admin/src/api/tickets_list.rs | 106 + warpgate-admin/src/api/users_list.rs | 33 + warpgate-admin/src/embed.rs | 96 + warpgate-admin/src/helpers.rs | 28 + warpgate-admin/src/lib.rs | 113 + warpgate-common/Cargo.toml | 29 + warpgate-common/src/auth.rs | 42 + warpgate-common/src/config.rs | 227 + warpgate-common/src/config_providers/file.rs | 226 + warpgate-common/src/config_providers/mod.rs | 77 + warpgate-common/src/consts.rs | 1 + warpgate-common/src/data.rs | 45 + warpgate-common/src/db/mod.rs | 96 + warpgate-common/src/db/uuid.rs | 45 + warpgate-common/src/eventhub.rs | 94 + warpgate-common/src/hash.rs | 32 + warpgate-common/src/helpers/fs.rs | 10 + warpgate-common/src/helpers/mod.rs | 2 + warpgate-common/src/helpers/serde_base64.rs | 21 + warpgate-common/src/lib.rs | 23 + warpgate-common/src/protocols/handle.rs | 90 + warpgate-common/src/protocols/mod.rs | 26 + warpgate-common/src/recordings/mod.rs | 94 + warpgate-common/src/recordings/terminal.rs | 71 + warpgate-common/src/recordings/traffic.rs | 207 + warpgate-common/src/recordings/writer.rs | 86 + warpgate-common/src/services.rs | 40 + warpgate-common/src/state.rs | 102 + warpgate-common/src/types.rs | 49 + warpgate-db-entities/Cargo.toml | 12 + warpgate-db-entities/src/KnownHost.rs | 21 + warpgate-db-entities/src/Recording.rs | 63 + warpgate-db-entities/src/Session.rs | 46 + warpgate-db-entities/src/Ticket.rs | 28 + warpgate-db-entities/src/lib.rs | 6 + warpgate-db-migrations/Cargo.toml | 14 + warpgate-db-migrations/README.md | 37 + warpgate-db-migrations/src/lib.rs | 19 + .../src/m00001_create_ticket.rs | 51 + .../src/m00002_create_session.rs | 72 + .../src/m00003_create_recording.rs | 98 + .../src/m00004_create_known_host.rs | 49 + warpgate-db-migrations/src/main.rs | 7 + warpgate-protocol-ssh/Cargo.toml | 24 + .../src/client/channel_direct_tcpip.rs | 90 + .../src/client/channel_session.rs | 154 + warpgate-protocol-ssh/src/client/handler.rs | 133 + warpgate-protocol-ssh/src/client/mod.rs | 515 ++ warpgate-protocol-ssh/src/common.rs | 58 + warpgate-protocol-ssh/src/compat.rs | 14 + warpgate-protocol-ssh/src/helpers.rs | 16 + warpgate-protocol-ssh/src/keys.rs | 88 + warpgate-protocol-ssh/src/known_hosts.rs | 76 + warpgate-protocol-ssh/src/lib.rs | 112 + warpgate-protocol-ssh/src/server/mod.rs | 83 + .../src/server/russh_handler.rs | 381 ++ .../src/server/service_output.rs | 74 + warpgate-protocol-ssh/src/server/session.rs | 1030 ++++ .../src/server/session_handle.rs | 24 + warpgate/.gitignore | 1 + warpgate/Cargo.toml | 35 + warpgate/src/commands/check.rs | 22 + warpgate/src/commands/client_keys.rs | 15 + warpgate/src/commands/hash.rs | 20 + warpgate/src/commands/mod.rs | 6 + warpgate/src/commands/run.rs | 133 + warpgate/src/commands/setup.rs | 184 + warpgate/src/commands/test_target.rs | 44 + warpgate/src/config.rs | 66 + warpgate/src/logging.rs | 60 + warpgate/src/main.rs | 68 + 130 files changed, 16485 insertions(+) create mode 100644 .cargo/config create mode 100644 .env create mode 100644 .github/workflows/build.yml create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/release.yml create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 deny.toml create mode 100644 justfile create mode 120000 russh create mode 100644 rust-toolchain.toml create mode 100644 rustfmt.toml create mode 100644 warpgate-admin/Cargo.toml create mode 100644 warpgate-admin/app/.editorconfig create mode 100644 warpgate-admin/app/.eslintrc.yaml create mode 100644 warpgate-admin/app/.gitignore create mode 100644 warpgate-admin/app/index.html create mode 100644 warpgate-admin/app/openapi-schema.json create mode 100644 warpgate-admin/app/openapitools.json create mode 100644 warpgate-admin/app/package.json create mode 100644 warpgate-admin/app/public/assets/logo.svg create mode 100644 warpgate-admin/app/src/App.svelte create mode 100644 warpgate-admin/app/src/CreateTicket.svelte create mode 100644 warpgate-admin/app/src/Home.svelte create mode 100644 warpgate-admin/app/src/Login.svelte create mode 100644 warpgate-admin/app/src/Recording.svelte create mode 100644 warpgate-admin/app/src/RelativeDate.svelte create mode 100644 warpgate-admin/app/src/SSH.svelte create mode 100644 warpgate-admin/app/src/Session.svelte create mode 100644 warpgate-admin/app/src/Targets.svelte create mode 100644 warpgate-admin/app/src/Tickets.svelte create mode 100644 warpgate-admin/app/src/assets/svelte.png create mode 100644 warpgate-admin/app/src/lib/api.ts create mode 100644 warpgate-admin/app/src/lib/ssh.ts create mode 100644 warpgate-admin/app/src/lib/store.ts create mode 100644 warpgate-admin/app/src/lib/time.ts create mode 100644 warpgate-admin/app/src/main.ts create mode 100644 warpgate-admin/app/src/theme.scss create mode 100644 warpgate-admin/app/src/vars.scss create mode 100644 warpgate-admin/app/src/vite-env.d.ts create mode 100644 warpgate-admin/app/svelte.config.js create mode 100644 warpgate-admin/app/tsconfig.json create mode 100644 warpgate-admin/app/tsconfig.node.json create mode 100644 warpgate-admin/app/vite.config.ts create mode 100644 warpgate-admin/app/yarn.lock create mode 100644 warpgate-admin/src/api/auth.rs create mode 100644 warpgate-admin/src/api/info.rs create mode 100644 warpgate-admin/src/api/known_hosts_detail.rs create mode 100644 warpgate-admin/src/api/known_hosts_list.rs create mode 100644 warpgate-admin/src/api/mod.rs create mode 100644 warpgate-admin/src/api/recordings_detail.rs create mode 100644 warpgate-admin/src/api/sessions_detail.rs create mode 100644 warpgate-admin/src/api/sessions_list.rs create mode 100644 warpgate-admin/src/api/ssh_keys.rs create mode 100644 warpgate-admin/src/api/targets_list.rs create mode 100644 warpgate-admin/src/api/tickets_detail.rs create mode 100644 warpgate-admin/src/api/tickets_list.rs create mode 100644 warpgate-admin/src/api/users_list.rs create mode 100644 warpgate-admin/src/embed.rs create mode 100644 warpgate-admin/src/helpers.rs create mode 100644 warpgate-admin/src/lib.rs create mode 100644 warpgate-common/Cargo.toml create mode 100644 warpgate-common/src/auth.rs create mode 100644 warpgate-common/src/config.rs create mode 100644 warpgate-common/src/config_providers/file.rs create mode 100644 warpgate-common/src/config_providers/mod.rs create mode 100644 warpgate-common/src/consts.rs create mode 100644 warpgate-common/src/data.rs create mode 100644 warpgate-common/src/db/mod.rs create mode 100644 warpgate-common/src/db/uuid.rs create mode 100644 warpgate-common/src/eventhub.rs create mode 100644 warpgate-common/src/hash.rs create mode 100644 warpgate-common/src/helpers/fs.rs create mode 100644 warpgate-common/src/helpers/mod.rs create mode 100644 warpgate-common/src/helpers/serde_base64.rs create mode 100644 warpgate-common/src/lib.rs create mode 100644 warpgate-common/src/protocols/handle.rs create mode 100644 warpgate-common/src/protocols/mod.rs create mode 100644 warpgate-common/src/recordings/mod.rs create mode 100644 warpgate-common/src/recordings/terminal.rs create mode 100644 warpgate-common/src/recordings/traffic.rs create mode 100644 warpgate-common/src/recordings/writer.rs create mode 100644 warpgate-common/src/services.rs create mode 100644 warpgate-common/src/state.rs create mode 100644 warpgate-common/src/types.rs create mode 100644 warpgate-db-entities/Cargo.toml create mode 100644 warpgate-db-entities/src/KnownHost.rs create mode 100644 warpgate-db-entities/src/Recording.rs create mode 100644 warpgate-db-entities/src/Session.rs create mode 100644 warpgate-db-entities/src/Ticket.rs create mode 100644 warpgate-db-entities/src/lib.rs create mode 100644 warpgate-db-migrations/Cargo.toml create mode 100644 warpgate-db-migrations/README.md create mode 100644 warpgate-db-migrations/src/lib.rs create mode 100644 warpgate-db-migrations/src/m00001_create_ticket.rs create mode 100644 warpgate-db-migrations/src/m00002_create_session.rs create mode 100644 warpgate-db-migrations/src/m00003_create_recording.rs create mode 100644 warpgate-db-migrations/src/m00004_create_known_host.rs create mode 100644 warpgate-db-migrations/src/main.rs create mode 100644 warpgate-protocol-ssh/Cargo.toml create mode 100644 warpgate-protocol-ssh/src/client/channel_direct_tcpip.rs create mode 100644 warpgate-protocol-ssh/src/client/channel_session.rs create mode 100644 warpgate-protocol-ssh/src/client/handler.rs create mode 100644 warpgate-protocol-ssh/src/client/mod.rs create mode 100644 warpgate-protocol-ssh/src/common.rs create mode 100644 warpgate-protocol-ssh/src/compat.rs create mode 100644 warpgate-protocol-ssh/src/helpers.rs create mode 100644 warpgate-protocol-ssh/src/keys.rs create mode 100644 warpgate-protocol-ssh/src/known_hosts.rs create mode 100644 warpgate-protocol-ssh/src/lib.rs create mode 100644 warpgate-protocol-ssh/src/server/mod.rs create mode 100644 warpgate-protocol-ssh/src/server/russh_handler.rs create mode 100644 warpgate-protocol-ssh/src/server/service_output.rs create mode 100644 warpgate-protocol-ssh/src/server/session.rs create mode 100644 warpgate-protocol-ssh/src/server/session_handle.rs create mode 100644 warpgate/.gitignore create mode 100644 warpgate/Cargo.toml create mode 100644 warpgate/src/commands/check.rs create mode 100644 warpgate/src/commands/client_keys.rs create mode 100644 warpgate/src/commands/hash.rs create mode 100644 warpgate/src/commands/mod.rs create mode 100644 warpgate/src/commands/run.rs create mode 100644 warpgate/src/commands/setup.rs create mode 100644 warpgate/src/commands/test_target.rs create mode 100644 warpgate/src/config.rs create mode 100644 warpgate/src/logging.rs create mode 100644 warpgate/src/main.rs diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 0000000..bff29e6 --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[build] +rustflags = ["--cfg", "tokio_unstable"] diff --git a/.env b/.env new file mode 100644 index 0000000..4e6dacc --- /dev/null +++ b/.env @@ -0,0 +1 @@ +DATABASE_URL=sqlite:data/db/db.sqlite3?mode=rwc diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..f09176e --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,67 @@ +name: Build + +on: [push, pull_request] + +jobs: + Build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Install just + run: | + cargo install just + + + - name: Install admin UI deps + run: | + just yarn + + - name: Build admin UI + run: | + just yarn openapi-client + just yarn build + + - name: Build + uses: actions-rs/cargo@v1 + with: + command: build + use-cross: true + args: --release --target x86_64-unknown-linux-gnu + + - name: Rename + run: | + mkdir dist + mv target/x86_64-unknown-linux-gnu/release/warpgate dist/warpgate-${{github.ref_name}}-x86_64-linux + + - uses: actions/upload-artifact@master + name: Upload artifacts + with: + name: warpgate-${{github.ref_name}}-x86_64-linux + path: dist/warpgate-${{github.ref_name}}-x86_64-linux + + # - name: 🔎 Test + # uses: actions-rs/cargo@v1 + # with: + # command: test + + - name: Upload + uses: softprops/action-gh-release@v1 + if: startsWith(github.ref, 'refs/tags/v') + with: + draft: true + append_body: true + generate_release_notes: true + files: dist/* + token: ${{ secrets.GITHUB_TOKEN }} + env: + GITHUB_REPOSITORY: my_gh_org/my_gh_repo diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..0e72a00 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,20 @@ +# Dependency Review Action +# +# This Action will scan dependency manifest files that change as part of a Pull Reqest, surfacing known-vulnerable versions of the packages declared or updated in the PR. Once installed, if the workflow run is marked as required, PRs introducing known-vulnerable packages will be blocked from merging. +# +# Source repository: https://github.com/actions/dependency-review-action +# Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement +name: 'Dependency Review' +on: [pull_request] + +permissions: + contents: read + +jobs: + dependency-review: + runs-on: ubuntu-latest + steps: + - name: 'Checkout Repository' + uses: actions/checkout@v3 + - name: 'Dependency Review' + uses: actions/dependency-review-action@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..529eb70 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,18 @@ +name: "Release" + +on: + push: + tags: + - "v*" + +jobs: + tagged-release: + name: "Tagged Release" + runs-on: "ubuntu-latest" + + steps: + - uses: "marvinpinto/action-automatic-releases@latest" + with: + repo_token: "${{ secrets.GITHUB_TOKEN }}" + prerelease: false + draft: true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3333315 --- /dev/null +++ b/.gitignore @@ -0,0 +1,20 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +temp +host_key* +.vscode + +# --- + +data +config.*.yaml +config.yaml diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..7b47266 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,4312 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static 1.4.0", + "regex 1.5.5", +] + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aead" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b613b8e1e3cf911a086f53f03bf286f52fd7a7258e4fa606f0ef220d39d8877" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "aes" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e8b47f52ea9bae42228d07ec09eb676433d7c4ed1ebdf0f1d1c29ed446f1ab8" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", + "ctr", + "opaque-debug 0.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df5f85a83a7d8b0442b6aa7b504b8212c1733da07b98aae43d4bc21b2cb3cdf6" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + +[[package]] +name = "ahash" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "739f4a8db6605981345c5654f3a85b056ce52f37a39d34da03f25bf2151ea16e" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.6.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81ce3d38065e618af2d7b77e10c5ad9a069859b4be3c2250f674af3840d9c8a5" +dependencies = [ + "memchr", +] + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "aliasable" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd" + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "anyhow" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4361135be9122e0870de935d7c439aef945b9f9ddd4199a553b5270b49c82a27" +dependencies = [ + "backtrace", +] + +[[package]] +name = "argon2" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25df3c03f1040d0069fcd3907e24e36d59f9b6fa07ba49be0eb25a794f036ba7" +dependencies = [ + "base64ct", + "blake2", + "password-hash 0.3.2", +] + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c026b7e44f1316b567ee750fea85103f87fcb80792b860e979f221259796ca0a" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-mutex", + "blocking", + "futures-lite", + "num_cpus", + "once_cell", + "tokio", +] + +[[package]] +name = "async-io" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a811e6a479f2439f0c04038796b5cfb3d2ad56c230e0f2d3f7b04d68cfee607b" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi 0.3.9", +] + +[[package]] +name = "async-lock" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-mutex" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479db852db25d9dbf6204e6cb6253698f175c15726470f78af0d918e99d6156e" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8056f1455169ab86dd47b47391e4ab0cbd25410a70e9fe675544f49bafaf952" +dependencies = [ + "async-attributes", + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dad5c83079eae9969be7fadefe640a1c566901f05ff91ab221de4b6f68d9507e" +dependencies = [ + "async-stream-impl", + "futures-core", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f203db73a71dfa2fb6dd22763990fa26f3d2625a6da2da900d23b87d26be27" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-task" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" + +[[package]] +name = "async-trait" +version = "0.1.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "061a7acccaa286c011ddc30970520b98fa40e00c9d644633fb26b5fc63a265e3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atoi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +dependencies = [ + "num-traits", +] + +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e121dee8023ce33ab248d9ce1493df03c3b38a659b240096fcbd7048ff9c31f" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + +[[package]] +name = "bae" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b8de67cc41132507eeece2584804efcb15f85ba516e34c944b7667f480397a" +dependencies = [ + "heck 0.3.3", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + +[[package]] +name = "base64ct" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a32fd6af2b5827bce66c29053ba0e7c42b9dcab01835835058558c10851a46b" + +[[package]] +name = "bcrypt-pbkdf" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12621b8e87feb183a6e5dbb315e49026b2229c4398797ee0ae2d1bc00aef41b9" +dependencies = [ + "blowfish", + "crypto-mac", + "pbkdf2", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "bimap" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0455254eb5c6964c4545d8bac815e1a1be4f3afe0ae695ea539c12d728d44b" + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "blake2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +dependencies = [ + "digest 0.10.3", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding 0.1.5", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "block-modes" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cb03d1bed155d89dce0f845b7899b18a9a163e148fd004e1c28421a783e2d8e" +dependencies = [ + "block-padding 0.2.1", + "cipher", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + +[[package]] +name = "blocking" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + +[[package]] +name = "blowfish" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe3ff3fc1de48c1ac2e3341c4df38b0d1bfb8fdf04632a187c8b75aaa319a7ab" +dependencies = [ + "byteorder", + "cipher", + "opaque-debug 0.3.0", +] + +[[package]] +name = "bumpalo" +version = "3.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "serde", + "time 0.1.43", + "winapi 0.3.9", +] + +[[package]] +name = "cipher" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ee52072ec15386f770805afd189a01c8841be8696bed250fa2f13c4c0d6dfb7" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "3.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8c93436c21e4698bacadf42917db28b23017027a4deccb35dbe47a7e7840123" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "indexmap", + "lazy_static 1.4.0", + "os_str_bytes", + "strsim 0.10.0", + "termcolor", + "textwrap 0.15.0", +] + +[[package]] +name = "clap_derive" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da95d038ede1a964ce99f49cbe27a7fb538d1da595e4b4f70b8c8f338d17bf16" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clicolors-control" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "495303f76458db51aa330df9510cfbb2e3ad57c6b82da7b38aad2a89088a91da" +dependencies = [ + "clicolors-control 1.0.1", +] + +[[package]] +name = "clicolors-control" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90082ee5dcdd64dc4e9e0d37fbf3ee325419e39c0092191e0393df65518f741e" +dependencies = [ + "atty", + "lazy_static 1.4.0", + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "config" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54ad70579325f1a38ea4c13412b82241c5900700a69785d73e2736bd65a33f86" +dependencies = [ + "async-trait", + "json5", + "lazy_static 1.4.0", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + +[[package]] +name = "console" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5682f3fb20402e2698bdc0ae90cb30ba1e8383b727ca0c026566fc972ca2c167" +dependencies = [ + "clicolors-control 0.3.2", + "kernel32-sys", + "lazy_static 0.2.11", + "libc", + "parking_lot 0.12.0", + "regex 0.2.11", + "unicode-width", + "winapi 0.3.9", +] + +[[package]] +name = "console" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28b32d32ca44b70c3e4acd7db1babf555fa026e385fb95f18028f88848b3c31" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "regex 1.5.5", + "terminal_size", + "unicode-width", + "winapi 0.3.9", +] + +[[package]] +name = "console-api" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc347c19eb5b940f396ac155822caee6662f850d97306890ac3773ed76c90c5a" +dependencies = [ + "prost", + "prost-types", + "tonic", + "tonic-build", + "tracing-core", +] + +[[package]] +name = "console-subscriber" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "565a7dfea2d10dd0e5c57cc394d5d441b1910960d8c9211ed14135e0e6ec3a20" +dependencies = [ + "console-api", + "crossbeam-channel", + "crossbeam-utils", + "futures", + "hdrhistogram", + "humantime", + "prost-types", + "serde", + "serde_json", + "thread_local 1.1.4", + "tokio", + "tokio-stream", + "tonic", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" +dependencies = [ + "aes-gcm", + "base64", + "hkdf", + "hmac 0.12.1", + "percent-encoding", + "rand", + "sha2 0.10.2", + "subtle", + "time 0.3.7", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpufeatures" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95059428f66df56b63431fdb4e1947ed2190586af5c5a8a8b71122bdf5a7f469" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdbfe11fe19ff083c48923cf179540e8cd0535903dc35e178a1fdeeb59aef51f" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +dependencies = [ + "cfg-if", + "lazy_static 1.4.0", +] + +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array 0.14.5", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array 0.14.5", + "subtle", +] + +[[package]] +name = "ctor" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccc0a48a9b826acdf4028595adc9db92caea352f7af011a3034acd172a52a0aa" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "ctr" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "049bb91fb4aaf0e3c7efa6cd5ef877dbbbd15b39dad06d9948de4ec8a75761ea" +dependencies = [ + "cipher", +] + +[[package]] +name = "darling" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0d720b8683f8dd83c65155f0530560cba68cd2bf395f6513a483caee57ff7f4" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a340f241d2ceed1deb47ae36c4144b2707ec7dd0b649f894cb39bb595986324" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim 0.10.0", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c41b3b7352feb3211a0d743dc5700a4e3b60f51bd2b368892d1e0f9a95f44b" +dependencies = [ + "darling_core", + "quote", + "syn", +] + +[[package]] +name = "data-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "dhat" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47003dc9f6368a88e85956c3b2573a7e6872746a3e5d762a8885da3a136a0381" +dependencies = [ + "backtrace", + "lazy_static 1.4.0", + "parking_lot 0.11.2", + "rustc-hash", + "serde", + "serde_json", + "thousands", +] + +[[package]] +name = "dialoguer" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d6b4fabcd9e97e1df1ae15395ac7e49fb144946a0d453959dc2696273b9da" +dependencies = [ + "console 0.15.0", + "tempfile", + "zeroize", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.5", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer 0.10.2", + "crypto-common", + "subtle", +] + +[[package]] +name = "dirs" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30baa043103c9d0c2a57cf537cc2f35623889dc0d405e6c3cccfadbc81c71309" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d86534ed367a67548dc68113a0f5db55432fdfbb6e6f9d77704397d95d5780" +dependencies = [ + "libc", + "redox_users", + "winapi 0.3.9", +] + +[[package]] +name = "dlv-list" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68df3f2b690c1b86e65ef7830956aededf3cb0a16f898f79b9a6f421a7b6211b" +dependencies = [ + "rand", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + +[[package]] +name = "filetime" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "975ccf83d8d9d0d84682850a38c8169027be83368805971cc4f238c2b245bc98" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "winapi 0.3.9", +] + +[[package]] +name = "fixedbitset" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" + +[[package]] +name = "flate2" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6988e897c1c9c485f43b47a529cef42fde0547f9d8d41a7062518f1d8fc53f" +dependencies = [ + "cfg-if", + "crc32fast", + "libc", + "miniz_oxide", +] + +[[package]] +name = "flume" +version = "0.10.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843c03199d0c0ca54bc1ea90ac0d507274c28abcc4f691ae8b4eaa375087c76a" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project", + "spin 0.9.2", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + +[[package]] +name = "fsevent-sys" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76ee7a02da4d231650c7cea31349b889be2f45ddb3ef3032d2ec8185f6313fd2" +dependencies = [ + "libc", +] + +[[package]] +name = "futures" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" + +[[package]] +name = "futures-executor" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-intrusive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + +[[package]] +name = "futures-io" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" + +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" + +[[package]] +name = "futures-task" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" + +[[package]] +name = "futures-util" +version = "0.3.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.10.2+wasi-snapshot-preview1", +] + +[[package]] +name = "ghash" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1583cc1656d7839fd3732b80cf4f38850336cdb9b8ded1cd399ca62958de3c99" +dependencies = [ + "opaque-debug 0.3.0", + "polyval", +] + +[[package]] +name = "gimli" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78cc372d058dcf6d5ecd98510e7fbc9e5aec4d21de70f65fea8fecebcd881bd4" + +[[package]] +name = "gloo-timers" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d12a7f4e95cfe710f1d624fb1210b7d961a5fb05c4fd942f4feab06e61f590e" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "h2" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62eeb471aa3e3c9197aa4bfeabfe02982f6dc96f750486c0bb0009ac58b26d2b" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util 0.6.9", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04" +dependencies = [ + "ahash 0.4.7", +] + +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + +[[package]] +name = "hdrhistogram" +version = "7.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31672b7011be2c4f7456c4ddbcb40e7e9a4a9fad8efe49a6ebaf5f307d0109c0" +dependencies = [ + "base64", + "byteorder", + "flate2", + "nom", + "num-traits", +] + +[[package]] +name = "headers" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cff78e5788be1e0ab65b04d306b2ed5092c815ec97ec70f4ebd5aee158aa55d" +dependencies = [ + "base64", + "bitflags", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha-1 0.10.0", +] + +[[package]] +name = "headers-core" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7f66481bfee273957b1f20485a4ff3362987f85b2c236580d81b4eb7a326429" +dependencies = [ + "http", +] + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hkdf" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.3", +] + +[[package]] +name = "http" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f4c6746584866f0feabcc69893c5b51beef3831656a968ed7ae254cdc4fd03" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9100414882e15fb7feccb4897e5f0ff0ff1ca7d1a86a23208ada4d7a18e6c6c4" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + +[[package]] +name = "humantime-serde" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a3db5ea5923d99402c94e9feb261dc5ee9b4efa158b0315f788cf549cc200c" +dependencies = [ + "humantime", + "serde", +] + +[[package]] +name = "hwaddr" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e414433a9e4338f4e87fa29d0670c883a5e73e7955c45f4a49130c0aa992c85b" +dependencies = [ + "phf", +] + +[[package]] +name = "hyper" +version = "0.14.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043f0e083e9901b6cc658a77d1eb86f4fc650bbb977a4337dd63192826aa85dd" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "indexmap" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" +dependencies = [ + "autocfg", + "hashbrown 0.11.2", +] + +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags", + "inotify-sys", + "libc", +] + +[[package]] +name = "inotify-sys" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb" +dependencies = [ + "libc", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" + +[[package]] +name = "js-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +dependencies = [ + "winapi 0.2.8", + "winapi-build", +] + +[[package]] +name = "kqueue" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058a107a784f8be94c7d35c1300f4facced2e93d2fbe5b1452b44e905ddca4a9" +dependencies = [ + "kqueue-sys", + "libc", +] + +[[package]] +name = "kqueue-sys" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +dependencies = [ + "bitflags", + "libc", +] + +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + +[[package]] +name = "lazy_static" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f033c7ad61445c5b347c7382dd1237847eb1bce590fe50365dcb33d546be73" + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad5c14e80759d0939d013e6ca49930e59fc53dd8e5009132f76240c179380c09" + +[[package]] +name = "libsodium-sys" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b779387cd56adfbc02ea4a668e704f729be8d6a6abd2c27ca5ee537849a92fd" +dependencies = [ + "cc", + "libc", + "pkg-config", + "walkdir", +] + +[[package]] +name = "libsqlite3-sys" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "lock_api" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" +dependencies = [ + "cfg-if", + "value-bag", +] + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + +[[package]] +name = "md5" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771" + +[[package]] +name = "memchr" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" + +[[package]] +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92518e98c078586bc6c934028adcca4c92a53d6a958196de835170a01d84e4b" +dependencies = [ + "adler", + "autocfg", +] + +[[package]] +name = "mio" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ba42135c6a5917b9db9cd7b293e5409e1c6b041e6f9825e92e55a894c63b6f8" +dependencies = [ + "libc", + "log", + "miow", + "ntapi", + "wasi 0.11.0+wasi-snapshot-preview1", + "winapi 0.3.9", +] + +[[package]] +name = "miow" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "multer" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f8f35e687561d5c1667590911e6698a8cb714a134a7505718a182e7bc9d3836" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http", + "httparse", + "log", + "memchr", + "mime", + "spin 0.9.2", + "tokio", + "version_check", +] + +[[package]] +name = "multimap" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" + +[[package]] +name = "native-tls" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48ba9f7719b5a0f42f338907614285fb5fd70e53858141f69898a1fb7203b24d" +dependencies = [ + "lazy_static 1.4.0", + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "notify" +version = "5.0.0-pre.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d13c22db70a63592e098fb51735bab36646821e6389a0ba171f3549facdf0b74" +dependencies = [ + "bitflags", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify", + "kqueue", + "libc", + "mio", + "walkdir", + "winapi 0.3.9", +] + +[[package]] +name = "ntapi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_threads" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c539a50b93a303167eded6e8dff5220cd39447409fb659f4cd24b1f72fe4f133" +dependencies = [ + "libc", +] + +[[package]] +name = "object" +version = "0.27.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ac1d3f9a1d3616fd9a60c8d74296f22406a238b6a72f5cc1e6f314df4ffbf9" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f3e037eac156d1775da914196f0f37741a274155e34a0b7e427c35d2a2ecb9" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "openssl" +version = "0.10.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c7ae222234c30df141154f159066c5093ff73b63204dcda7121eb082fc56a95" +dependencies = [ + "bitflags", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-sys", +] + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "openssl-src" +version = "111.18.0+1.1.1n" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7897a926e1e8d00219127dc020130eca4292e5ca666dd592480d72c3eca2ff6c" +dependencies = [ + "cc", +] + +[[package]] +name = "openssl-sys" +version = "0.9.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e46109c383602735fa0a2e48dd2b7c892b048e1bf69e5c3b1d804b7d9c203cb" +dependencies = [ + "autocfg", + "cc", + "libc", + "openssl-src", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "ordered-multimap" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c672c7ad9ec066e428c00eb917124a06f08db19e2584de982cc34b1f4c12485" +dependencies = [ + "dlv-list", + "hashbrown 0.9.1", +] + +[[package]] +name = "os_str_bytes" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" +dependencies = [ + "memchr", +] + +[[package]] +name = "ouroboros" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71643f290d126e18ac2598876d01e1d57aed164afc78fdb6e2a0c6589a1f6662" +dependencies = [ + "aliasable", + "ouroboros_macro", + "stable_deref_trait", +] + +[[package]] +name = "ouroboros_macro" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed9a247206016d424fe8497bc611e510887af5c261fbbf977877c4bb55ca4d82" +dependencies = [ + "Inflector", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "packet" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c136c7ad0619ed4f88894aecf66ad86c80683e7b5d707996e6a3a7e0e3916944" +dependencies = [ + "bitflags", + "byteorder", + "hwaddr", + "thiserror", +] + +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.1", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi 0.3.9", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "password-hash" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77e0b28ace46c5a396546bcf443bf422b57049617433d8854227352a4a9b24e7" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "password-hash" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d791538a6dcc1e7cb7fe6f6b58aca40e7f79403c45b2bc274008b5e647af1d8" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" + +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + +[[package]] +name = "pbkdf2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +dependencies = [ + "base64ct", + "crypto-mac", + "hmac 0.11.0", + "password-hash 0.2.3", + "sha2 0.9.9", +] + +[[package]] +name = "pem" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947" +dependencies = [ + "base64", +] + +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + +[[package]] +name = "pest" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" +dependencies = [ + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d" +dependencies = [ + "maplit", + "pest", + "sha-1 0.8.2", +] + +[[package]] +name = "petgraph" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a13a2fa9d0b63e5f22328828741e523766fff0ee9e779316902290dff3f824f" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "phf" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dfb61232e34fcb633f43d12c58f83c1df82962dcdfa565a4e866ffc17dafe12" +dependencies = [ + "phf_shared", +] + +[[package]] +name = "phf_shared" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c00cf8b9eafe68dde5e9eaa2cef8ee84a9336a47d566ec55ca16589633b65af7" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkg-config" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" + +[[package]] +name = "poem" +version = "1.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ebe001b1703015f652398319527ec143dc5a309d1978305b0c6fb42c2a3b9e" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "cookie", + "futures-util", + "headers", + "http", + "httpdate", + "hyper", + "mime", + "mime_guess", + "multer", + "parking_lot 0.12.0", + "percent-encoding", + "pin-project-lite", + "poem-derive", + "priority-queue", + "rand", + "regex 1.5.5", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "tempfile", + "thiserror", + "time 0.3.7", + "tokio", + "tokio-rustls", + "tokio-stream", + "tokio-util 0.7.0", + "tracing", +] + +[[package]] +name = "poem-derive" +version = "1.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93704c52afb1b1a37f80b173fbe5215409bf95651049fa28cb721ddac6a0acb6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "poem-openapi" +version = "1.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c87bf260e007b7bc53447a25367e60f21535816707e8201014dc233f64389a11" +dependencies = [ + "base64", + "bytes", + "chrono", + "derive_more", + "futures-util", + "mime", + "num-traits", + "poem", + "poem-openapi-derive", + "regex 1.5.5", + "serde", + "serde_json", + "thiserror", + "tokio", + "uuid", +] + +[[package]] +name = "poem-openapi-derive" +version = "1.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dd1f8a41e10d02a78f94e8dde68f3dde1f6b292db2e581d77980b63e3856f91" +dependencies = [ + "Inflector", + "darling", + "http", + "indexmap", + "mime", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex 1.5.5", + "syn", + "thiserror", +] + +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if", + "libc", + "log", + "wepoll-ffi", + "winapi 0.3.9", +] + +[[package]] +name = "polyval" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8419d2b623c7c0896ff2d5d96e2cb4ede590fed28fcc34934f4c33c036e620a1" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug 0.3.0", + "universal-hash", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "priority-queue" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00ba480ac08d3cfc40dea10fd466fd2c14dee3ea6fc7873bc4079eda2727caf0" +dependencies = [ + "autocfg", + "indexmap", +] + +[[package]] +name = "proc-macro-crate" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" +dependencies = [ + "thiserror", + "toml", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "prost" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" +dependencies = [ + "bytes", + "heck 0.3.3", + "itertools", + "lazy_static 1.4.0", + "log", + "multimap", + "petgraph", + "prost", + "prost-types", + "regex 1.5.5", + "tempfile", + "which", +] + +[[package]] +name = "prost-derive" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" +dependencies = [ + "anyhow", + "itertools", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "prost-types" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" +dependencies = [ + "bytes", + "prost", +] + +[[package]] +name = "quote" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rcgen" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7fa2d386df8533b02184941c76ae2e0d0c1d053f5d43339169d80f21275fc5e" +dependencies = [ + "pem", + "ring", + "time 0.3.7", + "yasna 0.5.0", + "zeroize", +] + +[[package]] +name = "redox_syscall" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8380fe0152551244f0747b1bf41737e0f8a74f97a14ccefd1148187271634f3c" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +dependencies = [ + "getrandom", + "redox_syscall", +] + +[[package]] +name = "regex" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9329abc99e39129fcceabd24cf5d85b4671ef7c29c50e972bc5afe32438ec384" +dependencies = [ + "aho-corasick 0.6.10", + "memchr", + "regex-syntax 0.5.6", + "thread_local 0.3.6", + "utf8-ranges", +] + +[[package]] +name = "regex" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a11647b6b25ff05a515cb92c365cec08801e83423a235b51e231e1808747286" +dependencies = [ + "aho-corasick 0.7.18", + "memchr", + "regex-syntax 0.6.25", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.25", +] + +[[package]] +name = "regex-syntax" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d707a4fa2637f2dca2ef9fd02225ec7661fe01a53623c1e6515b6916511f7a7" +dependencies = [ + "ucd-util", +] + +[[package]] +name = "regex-syntax" +version = "0.6.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" + +[[package]] +name = "remove_dir_all" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +dependencies = [ + "winapi 0.3.9", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + +[[package]] +name = "ron" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b861ecaade43ac97886a512b360d01d66be9f41f3c61088b42cedf92e03d678" +dependencies = [ + "base64", + "bitflags", + "serde", +] + +[[package]] +name = "russh" +version = "0.34.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d662ff7b0b00a0b8dc6d66b1cfcdb6e587421cec345805fde78890ca0158de" +dependencies = [ + "bitflags", + "byteorder", + "digest 0.9.0", + "flate2", + "futures", + "generic-array 0.14.5", + "log", + "openssl", + "rand", + "russh-cryptovec", + "russh-keys", + "russh-libsodium", + "sha2 0.9.9", + "thiserror", + "tokio", +] + +[[package]] +name = "russh-cryptovec" +version = "0.7.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e89fd30a2ef98dfa621409d5bc56a2479d5810bf13a5eea3de89d859437b7e2e" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "russh-keys" +version = "0.22.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab150b6cf2ee69249e89449873d9b1104f871105ef852f6cd4c03587899aec6b" +dependencies = [ + "aes", + "bcrypt-pbkdf", + "bit-vec", + "block-modes", + "byteorder", + "data-encoding", + "dirs", + "futures", + "hmac 0.11.0", + "log", + "md5", + "num-bigint 0.4.3", + "num-integer", + "openssl", + "pbkdf2", + "rand", + "russh-cryptovec", + "russh-libsodium", + "serde", + "serde_derive", + "sha2 0.9.9", + "thiserror", + "tokio", + "tokio-stream", + "yasna 0.4.0", +] + +[[package]] +name = "russh-libsodium" +version = "0.3.0-beta.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01be4f65a8447ddbbac295d381c065e3a74f8c944aad48e37f64e53e1942f186" +dependencies = [ + "lazy_static 1.4.0", + "libc", + "libsodium-sys", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "rust-embed" +version = "6.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40377bff8cceee81e28ddb73ac97f5c2856ce5522f0b260b763f434cdfae602" +dependencies = [ + "rust-embed-impl", + "rust-embed-utils", + "walkdir", +] + +[[package]] +name = "rust-embed-impl" +version = "6.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94e763e24ba2bf0c72bc6be883f967f794a019fafd1b86ba1daff9c91a7edd30" +dependencies = [ + "proc-macro2", + "quote", + "rust-embed-utils", + "syn", + "walkdir", +] + +[[package]] +name = "rust-embed-utils" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad22c7226e4829104deab21df575e995bfbc4adfad13a595e387477f238c1aec" +dependencies = [ + "sha2 0.9.9", + "walkdir", +] + +[[package]] +name = "rust-ini" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63471c4aa97a1cf8332a5f97709a79a4234698de6a1f5087faf66f2dae810e22" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust_decimal" +version = "1.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d37baa70cf8662d2ba1c1868c5983dda16ef32b105cce41fb5c47e72936a90b3" +dependencies = [ + "arrayvec", + "num-traits", + "serde", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustls" +version = "0.20.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fbfeb8d0ddb84706bc597a5574ab8912817c52a397f819e5b614e2265206921" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee86d63972a7c661d1536fefe8c3c8407321c3df668891286de28abcd087360" +dependencies = [ + "base64", +] + +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" + +[[package]] +name = "ryu" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +dependencies = [ + "lazy_static 1.4.0", + "winapi 0.3.9", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "sct" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "sd-notify" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fde85c94a50dc789df8ca7b39f6b8b1eaa6cd320cc729e9ce1e1e1104292719" + +[[package]] +name = "sea-orm" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd24380b48dacd3ed1c3d467c7b17ffa5818555a2c04066f4a0a9e17d830abc9" +dependencies = [ + "async-stream", + "async-trait", + "chrono", + "futures", + "futures-util", + "once_cell", + "ouroboros", + "rust_decimal", + "sea-orm-macros", + "sea-query", + "sea-strum", + "serde", + "serde_json", + "sqlx", + "tracing", + "url", + "uuid", +] + +[[package]] +name = "sea-orm-macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c199fa8630b1e195d7aef24ce8944af8f4ced67c4eccffd8926453b59f2565a1" +dependencies = [ + "bae", + "heck 0.3.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sea-query" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9088ff96158860a75d98a85a654fdd9d97b10515773af6d87339bfc48258c800" +dependencies = [ + "chrono", + "rust_decimal", + "sea-query-derive", + "serde_json", + "uuid", +] + +[[package]] +name = "sea-query-derive" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34cdc022b4f606353fe5dc85b09713a04e433323b70163e81513b141c6ae6eb5" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn", + "thiserror", +] + +[[package]] +name = "sea-schema" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd47c9936ada6b8649b6602b7873ca4dd5edd0c2d8051a8ac3d9aba22b9e406" +dependencies = [ + "async-std", + "async-trait", + "clap 2.34.0", + "dotenv", + "log", + "sea-orm", + "sea-query", + "sea-schema-derive", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "sea-schema-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56821b7076f5096b8f726e2791ad255a99c82498e08ec477a65a96c461ff1927" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sea-strum" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "391d06a6007842cfe79ac6f7f53911b76dfd69fc9a6769f1cf6569d12ce20e1b" +dependencies = [ + "sea-strum_macros", +] + +[[package]] +name = "sea-strum_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b4397b825df6ccf1e98bcdabef3bbcfc47ff5853983467850eeab878384f21" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "security-framework" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d" + +[[package]] +name = "serde" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.136" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[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 = "serde_yaml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a521f2940385c165a24ee286aa8599633d162077a54bdcae2a6fd5a7bfa7a0" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "sha-1" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.3", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static 1.4.0", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "slab" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" + +[[package]] +name = "socket2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "511254be0c5bcf062b019a6c89c01a664aa359ded62f78aa72c6fc137c0590e5" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlformat" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc15591eb44ffb5816a4a70a7efd5dd87bfd3aa84c4c200401c4396140525826" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "195183bf6ff8328bb82c0511a83faf60aacf75840103388851db61d7a9854ae3" +dependencies = [ + "ahash 0.7.6", + "atoi", + "bitflags", + "byteorder", + "bytes", + "chrono", + "crc", + "crossbeam-queue", + "either", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "hashlink", + "hex", + "indexmap", + "itoa", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "num-bigint 0.3.3", + "once_cell", + "paste", + "percent-encoding", + "rust_decimal", + "serde", + "serde_json", + "sha2 0.9.9", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "tokio-stream", + "url", + "uuid", +] + +[[package]] +name = "sqlx-macros" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee35713129561f5e55c554bba1c378e2a7e67f81257b7311183de98c50e6f94" +dependencies = [ + "dotenv", + "either", + "heck 0.3.3", + "once_cell", + "proc-macro2", + "quote", + "serde_json", + "sha2 0.9.9", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b555e70fbbf84e269ec3858b7a6515bcfe7a166a7cc9c636dd6efd20431678b6" +dependencies = [ + "native-tls", + "once_cell", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebd69e719f31e88618baa1eaa6ee2de5c9a1c004f1e9ecdb58e8352a13f20a01" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "tempfile" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +dependencies = [ + "cfg-if", + "fastrand", + "libc", + "redox_syscall", + "remove_dir_all", + "winapi 0.3.9", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminal_size" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thousands" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" + +[[package]] +name = "thread_local" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6b53e329000edc2b34dbe8545fd20e55a333362d0a321909685a19bd28c3f1b" +dependencies = [ + "lazy_static 1.4.0", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + +[[package]] +name = "time" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +dependencies = [ + "libc", + "winapi 0.3.9", +] + +[[package]] +name = "time" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" +dependencies = [ + "itoa", + "libc", + "num_threads", + "time-macros", +] + +[[package]] +name = "time-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" + +[[package]] +name = "tinyvec" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "tokio" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af73ac49756f3f7c01172e34a23e5d0216f6c32333757c2c61feb2bbff5a5ee" +dependencies = [ + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "winapi 0.3.9", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4151fda0cf2798550ad0b34bcfc9b9dcc2a9d2471c895c68f3a8818e54f2389e" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-stream" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64910e1b9c1901aaf5375561e35b9c057d95ff41a44ede043a03e09279eabaf1" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "log", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31142970826733df8241ef35dc040ef98c679ab14d7c3e54d827099b3acecaa" +dependencies = [ + "serde", +] + +[[package]] +name = "tonic" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff08f4649d10a70ffa3522ca559031285d8e421d727ac85c60825761818f5d0a" +dependencies = [ + "async-stream", + "async-trait", + "base64", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "prost-derive", + "tokio", + "tokio-stream", + "tokio-util 0.6.9", + "tower", + "tower-layer", + "tower-service", + "tracing", + "tracing-futures", +] + +[[package]] +name = "tonic-build" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9403f1bafde247186684b230dc6f38b5cd514584e8bec1dd32514be4745fa757" +dependencies = [ + "proc-macro2", + "prost-build", + "quote", + "syn", +] + +[[package]] +name = "tower" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a89fd63ad6adf737582df5db40d286574513c69a11dac5214dc3b5603d6713e" +dependencies = [ + "futures-core", + "futures-util", + "indexmap", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util 0.7.0", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "343bc9466d3fe6b0f960ef45960509f84480bf4fd96f92901afe7ff3df9d3a62" + +[[package]] +name = "tower-service" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" + +[[package]] +name = "tracing" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1bdf54a7c28a2bbf701e1d2233f6c77f473486b94bee4f9678da5a148dca7f" +dependencies = [ + "cfg-if", + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e65ce065b4b5c53e73bb28912318cb8c9e9ad3921f1d669eb0e68b4c8143a2b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa31669fa42c09c34d94d8165dd2012e8ff3c66aca50f3bb226b68f216f2706c" +dependencies = [ + "lazy_static 1.4.0", + "valuable", +] + +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + +[[package]] +name = "tracing-log" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6923477a48e41c1951f1999ef8bb5a3023eb723ceadafe78ffb65dc366761e3" +dependencies = [ + "lazy_static 1.4.0", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e0ab7bdc962035a87fba73f3acca9b8a8d0034c2e6f60b84aeaaddddc155dce" +dependencies = [ + "ansi_term", + "lazy_static 1.4.0", + "matchers", + "regex 1.5.5", + "sharded-slab", + "smallvec", + "thread_local 1.1.4", + "time 0.3.7", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "ucd-trie" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" + +[[package]] +name = "ucd-util" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c85f514e095d348c279b1e5cd76795082cf15bd59b93207832abe0b1d8fed236" + +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + +[[package]] +name = "universal-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f214e8f697e925001e66ec2c6e37a4ef93f0f78c2eed7814394e10c62025b05" +dependencies = [ + "generic-array 0.14.5", + "subtle", +] + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf8-ranges" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" + +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", + "serde", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79923f7731dc61ebfba3633098bf3ac533bbd35ccd8c57e7088d9a5eebe0263f" +dependencies = [ + "ctor", + "version_check", +] + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi 0.3.9", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + +[[package]] +name = "warpgate" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "clap 3.1.6", + "config", + "console 0.1.0", + "console-subscriber", + "dhat", + "dialoguer", + "futures", + "notify", + "openssl", + "rcgen", + "sd-notify", + "serde_yaml", + "time 0.3.7", + "tokio", + "tracing", + "tracing-subscriber", + "warpgate-admin", + "warpgate-common", + "warpgate-protocol-ssh", +] + +[[package]] +name = "warpgate-admin" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "bytes", + "chrono", + "futures", + "hex", + "mime_guess", + "poem", + "poem-openapi", + "russh-keys", + "rust-embed", + "sea-orm", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "uuid", + "warpgate-common", + "warpgate-db-entities", + "warpgate-protocol-ssh", +] + +[[package]] +name = "warpgate-common" +version = "0.1.0" +dependencies = [ + "anyhow", + "argon2", + "async-trait", + "bytes", + "chrono", + "data-encoding", + "humantime-serde", + "packet", + "password-hash 0.3.2", + "poem-openapi", + "rand", + "rand_core", + "sea-orm", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "url", + "uuid", + "warpgate-db-entities", + "warpgate-db-migrations", +] + +[[package]] +name = "warpgate-db-entities" +version = "0.1.0" +dependencies = [ + "chrono", + "poem-openapi", + "sea-orm", + "serde", + "uuid", +] + +[[package]] +name = "warpgate-db-migrations" +version = "0.1.0" +dependencies = [ + "chrono", + "sea-orm", + "sea-schema", + "uuid", +] + +[[package]] +name = "warpgate-protocol-ssh" +version = "0.1.0" +dependencies = [ + "ansi_term", + "anyhow", + "async-trait", + "bimap", + "bytes", + "dialoguer", + "futures", + "russh", + "russh-keys", + "sea-orm", + "thiserror", + "time 0.3.7", + "tokio", + "tracing", + "uuid", + "warpgate-common", + "warpgate-db-entities", +] + +[[package]] +name = "wasi" +version = "0.10.2+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" +dependencies = [ + "bumpalo", + "lazy_static 1.4.0", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2eb6ec270a31b1d3c7e266b999739109abce8b6c87e4b31fcfcd788b65267395" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" + +[[package]] +name = "web-sys" +version = "0.3.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + +[[package]] +name = "which" +version = "4.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2" +dependencies = [ + "either", + "lazy_static 1.4.0", + "libc", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" + +[[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-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[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-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi 0.3.9", +] + +[[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-sys" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" + +[[package]] +name = "windows_i686_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" + +[[package]] +name = "windows_i686_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.32.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yasna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e262a29d0e61ccf2b6190d7050d4b237535fc76ce4c1210d9caa316f71dffa75" +dependencies = [ + "bit-vec", + "num-bigint 0.4.3", +] + +[[package]] +name = "yasna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346d34a236c9d3e5f3b9b74563f238f955bbd05fa0b8b4efa53c130c43982f4c" +dependencies = [ + "time 0.3.7", +] + +[[package]] +name = "zeroize" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50344758e2f40e3a1fcfc8f6f91aa57b5f8ebd8d27919fe6451f15aaaf9ee608" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4940e8c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,15 @@ +[workspace] +members = [ + "warpgate", + "warpgate-admin", + "warpgate-common", + "warpgate-db-migrations", + "warpgate-db-entities", + "warpgate-protocol-ssh", +] +default-members = ["warpgate"] + +[profile.release] +lto = true +panic = "abort" +strip = "debuginfo" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..47b329f --- /dev/null +++ b/README.md @@ -0,0 +1,33 @@ +# Warpgate + +Warpgate is a smart SSH bastion host for Linux that can be used with _any_ SSH client. + +* Set it up in your DMZ, add user accounts and easily assign them to specific hosts within the network. +* Warpgate will record every session for you to replay and review later through a built-in admin web UI. +* Single-file statically linked binary with no dependencies. +* Written in 100% safe Rust. + +## Getting started + +See the [Getting started](https://github.com/Eugeny/warpgate/wiki/Getting-started) wiki page. + +## Project Status + +The project is currently in **alpha** stage and is gathering community feedback. See the [official roadmap](https://github.com/users/Eugeny/projects/1/views/2) for the upcoming features. + +In particular, we're working on: + +* Support for exposing HTTP(S) endpoints through the bastion, +* Support for tunneling database connections, +* Live session view and control, +* Requesting admin approval for sessions +* and much more. + +## Contributing / building from source + +* Clone the repo +* [Just](https://github.com/casey/just) is used to run tasks - install it: `cargo install just` +* Install the admin UI deps: `just yarn` +* Build the API SDK: `just openapi-client` +* Build the frontend: `just yarn build` +* Build Warpgate: `cargo build` (optionally `--release`) diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..f00a164 --- /dev/null +++ b/deny.toml @@ -0,0 +1,198 @@ +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #{ triple = "x86_64-unknown-linux-musl" }, + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url(s) of the advisory databases to use +db-urls = ["https://github.com/rustsec/advisory-db"] +# The lint level for security vulnerabilities +vulnerability = "deny" +# The lint level for unmaintained crates +unmaintained = "warn" +# The lint level for crates that have been yanked from their source registry +yanked = "warn" +# The lint level for crates with security notices. Note that as of +# 2019-12-17 there are no security notice advisories in +# https://github.com/rustsec/advisory-db +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", +] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "deny" +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "WTFPL", +] +# List of explicitly disallowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +deny = [ + #"Nokia", +] +# Lint level for licenses considered copyleft +copyleft = "warn" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "either" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "deny" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + #{ allow = ["Zlib"], name = "adler32", version = "*" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The name of the crate the clarification applies to +#name = "ring" +# The optional version constraint for the crate +#version = "*" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ + # Each entry is a crate relative path, and the (opaque) hash of its contents + #{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries. +# To see how to mark a crate as unpublished (to the official registry), +# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# List of crates that are allowed. Use with care! +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, + # + # Wrapper crates can optionally be specified to allow the crate when it + # is a direct dependency of the otherwise banned crate + #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, +] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite +skip-tree = [ + #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] diff --git a/justfile b/justfile new file mode 100644 index 0000000..22ea9ca --- /dev/null +++ b/justfile @@ -0,0 +1,27 @@ +projects := "warpgate warpgate-admin warpgate-common warpgate-db-entities warpgate-db-migrations warpgate-protocol-ssh" + +run *ARGS: + RUST_BACKTRACE=1 RUST_LOG=warpgate cd warpgate && cargo run -- --config ../config.yaml {{ARGS}} + +fmt: + for p in {{projects}}; do cargo fmt -p $p -v; done + +fix *ARGS: + for p in {{projects}}; do cargo fix -p $p {{ARGS}}; done + +clippy *ARGS: + for p in {{projects}}; do cargo clippy -p $p {{ARGS}}; done + +yarn *ARGS: + cd warpgate-admin/app/ && yarn {{ARGS}} + +svelte-check: + cd warpgate-admin/app/ && yarn run check + +openapi-all: + cd warpgate-admin/app/ && yarn openapi-schema && yarn openapi-client + +openapi: + cd warpgate-admin/app/ && yarn openapi-client + +cleanup: (fix "--allow-dirty") (clippy "--fix" "--allow-dirty") fmt diff --git a/russh b/russh new file mode 120000 index 0000000..2d180b6 --- /dev/null +++ b/russh @@ -0,0 +1 @@ +rust-russh/russh \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..5df4faf --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly-2022-03-14" diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..c1578aa --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +imports_granularity = "Module" diff --git a/warpgate-admin/Cargo.toml b/warpgate-admin/Cargo.toml new file mode 100644 index 0000000..87c346c --- /dev/null +++ b/warpgate-admin/Cargo.toml @@ -0,0 +1,28 @@ +[package] +edition = "2021" +license = "Apache-2.0" +name = "warpgate-admin" +version = "0.1.0" + +[dependencies] +anyhow = {version = "1.0", features = ["std"]} +async-trait = "0.1" +bytes = "1.1" +chrono = "0.4" +futures = "0.3" +hex = "0.4" +mime_guess = "2.0" +poem = {version = "1.3", features = ["cookie", "session", "anyhow", "rustls"]} +poem-openapi = {version = "1.3", features = ["swagger-ui", "chrono", "uuid", "static-files"]} +russh-keys = {version = "0.22.0-beta.1", features = ["openssl"]} +rust-embed = "6.3" +sea-orm = {version = "^0.6", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"], default-features = false} +serde = "1.0" +serde_json = "1.0" +thiserror = "1.0" +tokio = {version = "1.17", features = ["tracing"]} +tracing = "0.1" +uuid = {version = "0.8", features = ["v4", "serde"]} +warpgate-common = {version = "*", path = "../warpgate-common"} +warpgate-db-entities = {version = "*", path = "../warpgate-db-entities"} +warpgate-protocol-ssh = {version = "*", path = "../warpgate-protocol-ssh"} diff --git a/warpgate-admin/app/.editorconfig b/warpgate-admin/app/.editorconfig new file mode 100644 index 0000000..81eba8c --- /dev/null +++ b/warpgate-admin/app/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +end_of_line = lf diff --git a/warpgate-admin/app/.eslintrc.yaml b/warpgate-admin/app/.eslintrc.yaml new file mode 100644 index 0000000..60001c5 --- /dev/null +++ b/warpgate-admin/app/.eslintrc.yaml @@ -0,0 +1,140 @@ +parser: '@typescript-eslint/parser' +parserOptions: + sourceType: module + project: + - ./tsconfig.json + extraFileExtensions: + - .svelte +env: + es6: true + browser: true +extends: + - 'plugin:import/recommended' + - 'plugin:import/typescript' + - 'plugin:@typescript-eslint/all' +plugins: + - import + - svelte3 + - '@typescript-eslint/eslint-plugin' +settings: + svelte3/typescript: true + import/resolver: + typescript: {} +rules: + '@typescript-eslint/semi': + - error + - never + '@typescript-eslint/indent': + - error + - 4 + '@typescript-eslint/explicit-member-accessibility': + - error + - accessibility: no-public + overrides: + parameterProperties: explicit + '@typescript-eslint/no-require-imports': 'off' + '@typescript-eslint/no-parameter-properties': 'off' + '@typescript-eslint/explicit-function-return-type': 'off' + '@typescript-eslint/no-explicit-any': 'off' + '@typescript-eslint/no-magic-numbers': 'off' + '@typescript-eslint/member-delimiter-style': 'off' + '@typescript-eslint/promise-function-async': 'off' + '@typescript-eslint/require-array-sort-compare': 'off' + '@typescript-eslint/no-floating-promises': 'off' + '@typescript-eslint/prefer-readonly': 'off' + '@typescript-eslint/require-await': 'off' + '@typescript-eslint/strict-boolean-expressions': 'off' + '@typescript-eslint/no-misused-promises': + - error + - checksVoidReturn: false + '@typescript-eslint/typedef': 'off' + '@typescript-eslint/consistent-type-imports': 'off' + '@typescript-eslint/sort-type-union-intersection-members': 'off' + '@typescript-eslint/no-use-before-define': + - error + - classes: false + no-duplicate-imports: error + array-bracket-spacing: + - error + - never + block-scoped-var: error + brace-style: 'off' + '@typescript-eslint/brace-style': + - error + - 1tbs + - allowSingleLine: true + computed-property-spacing: + - error + - never + curly: error + eol-last: error + eqeqeq: + - error + - smart + max-depth: + - 1 + - 5 + max-statements: + - 1 + - 80 + no-multiple-empty-lines: error + no-mixed-spaces-and-tabs: error + no-trailing-spaces: error + '@typescript-eslint/no-unused-vars': + - error + - vars: all + args: after-used + argsIgnorePattern: ^_ + no-undef: error + no-var: error + object-curly-spacing: 'off' + '@typescript-eslint/object-curly-spacing': + - error + - always + quote-props: + - warn + - as-needed + - keywords: true + numbers: true + quotes: 'off' + '@typescript-eslint/quotes': + - error + - single + - allowTemplateLiterals: true + '@typescript-eslint/no-confusing-void-expression': + - error + - ignoreArrowShorthand: true + '@typescript-eslint/no-non-null-assertion': 'off' + '@typescript-eslint/no-unnecessary-condition': + - error + - allowConstantLoopConditions: true + '@typescript-eslint/restrict-template-expressions': 'off' + '@typescript-eslint/prefer-readonly-parameter-types': 'off' + '@typescript-eslint/no-unsafe-member-access': 'off' + '@typescript-eslint/no-unsafe-call': 'off' + '@typescript-eslint/no-unsafe-return': 'off' + '@typescript-eslint/no-unsafe-assignment': 'off' + '@typescript-eslint/naming-convention': 'off' + '@typescript-eslint/lines-between-class-members': + - error + - exceptAfterSingleLine: true + '@typescript-eslint/dot-notation': 'off' + '@typescript-eslint/no-implicit-any-catch': 'off' + '@typescript-eslint/member-ordering': 'off' + '@typescript-eslint/no-var-requires': 'off' + '@typescript-eslint/no-unsafe-argument': 'off' + '@typescript-eslint/restrict-plus-operands': 'off' + '@typescript-eslint/space-infix-ops': 'off' + '@typescript-eslint/no-type-alias': + - error + - allowAliases: in-unions-and-intersections + allowLiterals: always + allowCallbacks: always + +overrides: + - files: '*.svelte' + processor: svelte3/svelte3 + +ignorePatterns: +- svelte.config.js +- vite.config.ts diff --git a/warpgate-admin/app/.gitignore b/warpgate-admin/app/.gitignore new file mode 100644 index 0000000..ac614e7 --- /dev/null +++ b/warpgate-admin/app/.gitignore @@ -0,0 +1,28 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + +#--- + +api-client diff --git a/warpgate-admin/app/index.html b/warpgate-admin/app/index.html new file mode 100644 index 0000000..c2bfa96 --- /dev/null +++ b/warpgate-admin/app/index.html @@ -0,0 +1,13 @@ + + + + + + + Warpgate + + +
+ + + diff --git a/warpgate-admin/app/openapi-schema.json b/warpgate-admin/app/openapi-schema.json new file mode 100644 index 0000000..0a42cd2 --- /dev/null +++ b/warpgate-admin/app/openapi-schema.json @@ -0,0 +1,669 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Warpgate", + "version": "0.1.0" + }, + "servers": [ + { + "url": "/api" + } + ], + "tags": [], + "paths": { + "/sessions": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SessionSnapshot" + } + } + } + } + } + }, + "operationId": "get_sessions" + }, + "delete": { + "responses": { + "201": { + "description": "" + } + }, + "operationId": "close_all_sessions" + } + }, + "/sessions/{id}": { + "get": { + "parameters": [ + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SessionSnapshot" + } + } + } + }, + "404": { + "description": "" + } + }, + "operationId": "get_session" + } + }, + "/sessions/{id}/recordings": { + "get": { + "parameters": [ + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Recording" + } + } + } + } + } + }, + "operationId": "get_session_recordings" + } + }, + "/sessions/{id}/close": { + "post": { + "parameters": [ + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false + } + ], + "responses": { + "201": { + "description": "" + }, + "404": { + "description": "" + } + }, + "operationId": "close_session" + } + }, + "/recordings/{id}": { + "get": { + "parameters": [ + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Recording" + } + } + } + }, + "404": { + "description": "" + } + }, + "operationId": "get_recording" + } + }, + "/users": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/UserSnapshot" + } + } + } + } + } + }, + "operationId": "get_users" + } + }, + "/targets": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Target" + } + } + } + } + } + }, + "operationId": "get_targets" + } + }, + "/tickets": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Ticket" + } + } + } + } + } + }, + "operationId": "get_tickets" + }, + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateTicketRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TicketAndSecret" + } + } + } + }, + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "string" + } + } + } + } + }, + "operationId": "create_ticket" + } + }, + "/tickets/{id}": { + "delete": { + "parameters": [ + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false + } + ], + "responses": { + "204": { + "description": "" + }, + "404": { + "description": "" + } + }, + "operationId": "delete_ticket" + } + }, + "/ssh/known-hosts": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SSHKnownHost" + } + } + } + } + } + }, + "operationId": "get_ssh_known_hosts" + } + }, + "/ssh/known-hosts/{id}": { + "delete": { + "parameters": [ + { + "name": "id", + "schema": { + "type": "string", + "format": "uuid" + }, + "in": "path", + "required": true, + "deprecated": false + } + ], + "responses": { + "204": { + "description": "" + }, + "404": { + "description": "" + } + }, + "operationId": "delete_ssh_known_host" + } + }, + "/info": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Info" + } + } + } + } + }, + "operationId": "get_info" + } + }, + "/auth/login": { + "post": { + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/LoginRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "" + }, + "401": { + "description": "" + } + }, + "operationId": "login" + } + }, + "/auth/logout": { + "post": { + "responses": { + "201": { + "description": "" + } + }, + "operationId": "logout" + } + }, + "/ssh/own-keys": { + "get": { + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SSHKey" + } + } + } + } + } + }, + "operationId": "get_ssh_own_keys" + } + } + }, + "components": { + "schemas": { + "CreateTicketRequest": { + "type": "object", + "required": [ + "username", + "target_name" + ], + "properties": { + "username": { + "type": "string" + }, + "target_name": { + "type": "string" + } + } + }, + "Info": { + "type": "object", + "required": [ + "version" + ], + "properties": { + "version": { + "type": "string" + }, + "username": { + "type": "string" + } + } + }, + "LoginRequest": { + "type": "object", + "required": [ + "username", + "password" + ], + "properties": { + "username": { + "type": "string" + }, + "password": { + "type": "string" + } + } + }, + "Recording": { + "type": "object", + "required": [ + "id", + "name", + "started", + "session_id", + "kind" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + }, + "started": { + "type": "string", + "format": "date-time" + }, + "ended": { + "type": "string", + "format": "date-time" + }, + "session_id": { + "type": "string", + "format": "uuid" + }, + "kind": { + "$ref": "#/components/schemas/RecordingKind" + } + } + }, + "RecordingKind": { + "type": "string", + "enum": [ + "Terminal", + "Traffic" + ] + }, + "SSHKey": { + "type": "object", + "required": [ + "kind", + "public_key_base64" + ], + "properties": { + "kind": { + "type": "string" + }, + "public_key_base64": { + "type": "string" + } + } + }, + "SSHKnownHost": { + "type": "object", + "required": [ + "id", + "host", + "port", + "key_type", + "key_base64" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "host": { + "type": "string" + }, + "port": { + "type": "integer", + "format": "uint16" + }, + "key_type": { + "type": "string" + }, + "key_base64": { + "type": "string" + } + } + }, + "SessionSnapshot": { + "type": "object", + "required": [ + "id", + "started" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "username": { + "type": "string" + }, + "target": { + "$ref": "#/components/schemas/Target" + }, + "started": { + "type": "string", + "format": "date-time" + }, + "ended": { + "type": "string", + "format": "date-time" + }, + "ticket_id": { + "type": "string", + "format": "uuid" + } + } + }, + "Target": { + "type": "object", + "required": [ + "name", + "allow_roles" + ], + "properties": { + "name": { + "type": "string" + }, + "allow_roles": { + "type": "array", + "items": { + "type": "string" + } + }, + "ssh": { + "$ref": "#/components/schemas/TargetSSHOptions" + }, + "web_admin": { + "$ref": "#/components/schemas/TargetWebAdminOptions" + } + } + }, + "TargetSSHOptions": { + "type": "object", + "required": [ + "host", + "port", + "username" + ], + "properties": { + "host": { + "type": "string" + }, + "port": { + "type": "integer", + "format": "uint16" + }, + "username": { + "type": "string" + } + } + }, + "TargetWebAdminOptions": { + "type": "object" + }, + "Ticket": { + "type": "object", + "required": [ + "id", + "username", + "target", + "created" + ], + "properties": { + "id": { + "type": "string", + "format": "uuid" + }, + "username": { + "type": "string" + }, + "target": { + "type": "string" + }, + "uses_left": { + "type": "integer", + "format": "uint32" + }, + "expiry": { + "type": "string", + "format": "date-time" + }, + "created": { + "type": "string", + "format": "date-time" + } + } + }, + "TicketAndSecret": { + "type": "object", + "required": [ + "ticket", + "secret" + ], + "properties": { + "ticket": { + "$ref": "#/components/schemas/Ticket" + }, + "secret": { + "type": "string" + } + } + }, + "UserSnapshot": { + "type": "object", + "required": [ + "username" + ], + "properties": { + "username": { + "type": "string" + } + } + } + } + } +} \ No newline at end of file diff --git a/warpgate-admin/app/openapitools.json b/warpgate-admin/app/openapitools.json new file mode 100644 index 0000000..7f483d1 --- /dev/null +++ b/warpgate-admin/app/openapitools.json @@ -0,0 +1,7 @@ +{ + "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", + "spaces": 2, + "generator-cli": { + "version": "5.4.0" + } +} diff --git a/warpgate-admin/app/package.json b/warpgate-admin/app/package.json new file mode 100644 index 0000000..7bc41b2 --- /dev/null +++ b/warpgate-admin/app/package.json @@ -0,0 +1,51 @@ +{ + "name": "warpgate-admin", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "watch": "vite build -w --mode development", + "preview": "vite preview", + "check": "svelte-check --tsconfig ./tsconfig.json", + "lint": "eslint src", + "postinstall": "yarn run openapi-client", + "openapi-schema": "curl http://localhost:8888/api/openapi.json > openapi-schema.json", + "openapi-client": "openapi-generator-cli generate -g typescript-fetch -i openapi-schema.json -o api-client -p npmName=warpgate-api-client -p useSingleRequestParameter=true && cd api-client && npm i && npm run build", + "openapi": "yarn run openapi-schema && yarn run openapi-client" + }, + "devDependencies": { + "@fontsource/work-sans": "^4.5.7", + "@fortawesome/free-regular-svg-icons": "^6.1.1", + "@fortawesome/free-solid-svg-icons": "^6.0.0", + "@openapitools/openapi-generator-cli": "^2.4.26", + "@sveltejs/vite-plugin-svelte": "^1.0.0-next.30", + "@tsconfig/svelte": "^3.0.0", + "@typescript-eslint/eslint-plugin": "^5.12.0", + "@typescript-eslint/parser": "^5.12.0", + "asciinema-player": "3.0.0-rc.1", + "bootstrap": "^5.1.3", + "eslint": "^8.9.0", + "eslint-config-standard": "^16.0.3", + "eslint-import-resolver-typescript": "^2.5.0", + "eslint-plugin-import": "^2.23.4", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-promise": "^6.0.0", + "eslint-plugin-svelte3": "^3.4.0", + "moment": "^2.29.2", + "sass": "^1.49.8", + "svelte": "^3.44.0", + "svelte-check": "^2.2.7", + "svelte-fa": "^2.4.0", + "svelte-preprocess": "^4.9.8", + "svelte-spa-router": "^3.2.0", + "sveltestrap": "^5.8.5", + "thenby": "^1.3.4", + "tslib": "^2.3.1", + "typescript": "^4.5.4", + "vite": "^2.8.0", + "vite-plugin-checker": "^0.4.2", + "vite-tsconfig-paths": "^3.4.0" + } +} diff --git a/warpgate-admin/app/public/assets/logo.svg b/warpgate-admin/app/public/assets/logo.svg new file mode 100644 index 0000000..2c391a2 --- /dev/null +++ b/warpgate-admin/app/public/assets/logo.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/warpgate-admin/app/src/App.svelte b/warpgate-admin/app/src/App.svelte new file mode 100644 index 0000000..8539089 --- /dev/null +++ b/warpgate-admin/app/src/App.svelte @@ -0,0 +1,136 @@ + + +
+
+ + + + {#if $authenticatedUsername} + Sessions + Targets + Tickets + SSH + {/if} + {#if $authenticatedUsername} +
+ +
+ + {/if} +
+
+ +
+
+ {version} +
+
+ + diff --git a/warpgate-admin/app/src/CreateTicket.svelte b/warpgate-admin/app/src/CreateTicket.svelte new file mode 100644 index 0000000..22c9b51 --- /dev/null +++ b/warpgate-admin/app/src/CreateTicket.svelte @@ -0,0 +1,108 @@ + + +{#if error} +{error} +{/if} + +{#if result} +
+

Ticket created

+
+ + + The secret is only shown once - you won't be able to see it again. + + + {#if selectedTarget?.ssh} +

Connection instructions

+ + + + + + + + + {/if} + + Done +{:else} +
+

Create an access ticket

+
+ + {#if users} + + + + {/if} + + {#if targets} + + + + {/if} + + +{/if} diff --git a/warpgate-admin/app/src/Home.svelte b/warpgate-admin/app/src/Home.svelte new file mode 100644 index 0000000..cc1daa4 --- /dev/null +++ b/warpgate-admin/app/src/Home.svelte @@ -0,0 +1,110 @@ + + +{#if !sessions} + +{:else} +
+ {#if $activeSessions } +

Sessions right now: {$activeSessions}

+ + {:else} +

No active sessions

+ {/if} +
+ + {#if $sortedSessions } + + {/if} +{/if} + + diff --git a/warpgate-admin/app/src/Login.svelte b/warpgate-admin/app/src/Login.svelte new file mode 100644 index 0000000..13d0cc2 --- /dev/null +++ b/warpgate-admin/app/src/Login.svelte @@ -0,0 +1,80 @@ + + +
+
+
+
+

Welcome

+
+ + + + + + + + + + + + + {#if incorrectCredentials} + Incorrect credentials + {/if} + {#if error} + {error} + {/if} + +
+
+
diff --git a/warpgate-admin/app/src/Recording.svelte b/warpgate-admin/app/src/Recording.svelte new file mode 100644 index 0000000..8144b2e --- /dev/null +++ b/warpgate-admin/app/src/Recording.svelte @@ -0,0 +1,49 @@ + + + +
+

Session recording

+
+ +{#if !recording && !error} + +{/if} + +{#if error} +{error} +{/if} + +{#if recording?.kind === 'Traffic'} + Download tcpdump file +{/if} +
+ + diff --git a/warpgate-admin/app/src/RelativeDate.svelte b/warpgate-admin/app/src/RelativeDate.svelte new file mode 100644 index 0000000..aecf5c2 --- /dev/null +++ b/warpgate-admin/app/src/RelativeDate.svelte @@ -0,0 +1,6 @@ + + +{timeAgo(date)} diff --git a/warpgate-admin/app/src/SSH.svelte b/warpgate-admin/app/src/SSH.svelte new file mode 100644 index 0000000..a2a1d07 --- /dev/null +++ b/warpgate-admin/app/src/SSH.svelte @@ -0,0 +1,73 @@ + + +
+

SSH

+
+ +{#if error} + {error} +{/if} + +{#if ownKeys} +

Warpgate's own SSH keys

+ Add these keys to the targets' authorized_hosts files +
+ {#each ownKeys as key} +
+
{key.kind} {key.publicKeyBase64}
+
+ {/each} +
+{/if} + +
+{#if knownHosts} + {#if knownHosts.length } +

Known hosts: {knownHosts.length}

+ {:else} +

No known hosts

+ {/if} +
+ {#each knownHosts as host} +
+
+ + {host.host}:{host.port} + + + deleteHost(host)}>Delete +
+
{host.keyType} {host.keyBase64}
+
+ {/each} +
+{/if} + + diff --git a/warpgate-admin/app/src/Session.svelte b/warpgate-admin/app/src/Session.svelte new file mode 100644 index 0000000..b3a5d40 --- /dev/null +++ b/warpgate-admin/app/src/Session.svelte @@ -0,0 +1,123 @@ + + +{#if !session && !error} + +{/if} + +{#if error} + {error} +{/if} + +{#if session} +
+
+

Session

+
+ {#if session.ended} + {moment.duration(moment(session.ended).diff(session.started)).humanize()} long, + {:else} + {moment.duration(moment().diff(session.started)).humanize()} + {/if} +
+
+ {#if !session.ended} + + {/if} +
+ +
+
+ + {#if session.username} + + {:else} + + {/if} + +
+
+ + + +
+
+ + {#if recordings?.length } +

Recordings

+
+ {#each recordings as recording} + +
+ + {recording.name} + + + {timeAgo(recording.started)} + +
+
+ {/each} +
+ {/if} +{/if} + + diff --git a/warpgate-admin/app/src/Targets.svelte b/warpgate-admin/app/src/Targets.svelte new file mode 100644 index 0000000..4d7a272 --- /dev/null +++ b/warpgate-admin/app/src/Targets.svelte @@ -0,0 +1,111 @@ + + +{#if error} +{error} +{/if} + +{#if targets } +
+

Targets

+
+Add or remove targets in the config file. + + + selectedTarget = undefined}> + selectedTarget = undefined}> +
+ {selectedTarget?.name} +
+
+ {#if selectedTarget?.ssh} + SSH target + {/if} + {#if selectedTarget?.webAdmin} + This web admin interface + {/if} +
+
+ + {#if selectedTarget?.ssh} +

Connection instructions

+ {#if users} + + + + {/if} + + + + + + + + + {/if} +
+
+{/if} + + + diff --git a/warpgate-admin/app/src/Tickets.svelte b/warpgate-admin/app/src/Tickets.svelte new file mode 100644 index 0000000..e23cf10 --- /dev/null +++ b/warpgate-admin/app/src/Tickets.svelte @@ -0,0 +1,71 @@ + + +{#if error} +{error} +{/if} + +{#if tickets } +
+ {#if tickets.length } +

Access tickets: {tickets.length}

+ {:else} +

No tickets created yet

+ {/if} + + Create a ticket + +
+ + {#if tickets.length } +
+ {#each tickets as ticket} +
+ + Access to {ticket.target} as {ticket.username} + + + + + deleteTicket(ticket)}>Delete +
+ {/each} +
+ {:else} + + Tickets are secret keys that allow access to one specific target without any additional authentication. + + {/if} +{/if} + + + diff --git a/warpgate-admin/app/src/assets/svelte.png b/warpgate-admin/app/src/assets/svelte.png new file mode 100644 index 0000000000000000000000000000000000000000..e673c91c7bcb0e3a1be737745af56990613a641e GIT binary patch literal 5185 zcmb7Ic{r5c+ka+Z#*(pQFEeFJ2}O~Z8EF@d5F#cN<(nC6vWyuCh3ZR|LZ*_XWJ_TN zGm@>cg|g2KDNlX&N=se&V8QiJoo*%Kj+*cI2_v~tsxBn zz@`(&51#=5sJICQkT7x8Ql!%%u6zlnoR5ga1q=NDo|E#Tvlx+&i6{s!iYADXh@uR# zsDLVZaZglB7qwK1df1}TUeOF!w7eiTzrmZlAzn^C?2LmGAdfM@6NqH$J$fa(m%HH1 zEfIf;QtXMtHqMbFKSx~OKEuf3c~rB^bdVIWHs`$YVN>_&XMCrPgxJLYDO?fn5VAhz zS{B*|nZ)foWa$5LZCB%jF2cAcUORK-k8ut2urUfK=zcD`G@zLOQwDqfy#AkE*PAJx z4GhXXimv`pa!)O#G7HtL5)-th2wK70>Ye}Gbc4OQY3E&j(YUf>x;${qk(kCxEbmWI zRa1Ok9w9+fDE)D8K*t0V9-I9LPEuhSu@$-e+FCf5be=t#I@-)=37iq+*2{ba2H2FWiIdr6?Kc=LDOLd-zI-=UBUAUEa*oP{^!lu7LH2;!g18V=DQ5^+iQ!k z_q?5*CAo2QiN^^sS&F$uuNyr&q(NB8NGlnH{spB704y!@*#_GxnoJ8qq88l_0H z+N{Dd%ic8-6zjYP(|CfMwWz_vgae*Bibc6^4}Og8iZd$Yf4Repz2wpP>3;iml^>LE z`w;i4F4)5cz@2j~(2rZE^7n+Zt|0ArFaOnDB?vsW`og-;ZCkJ^5x)HRA?fCWgW)zB zg1~Q;P$%t_;4=ablxaey+KXQ#Xw*;6TBXLuGrh`S!3$3}W!F+Ez<6C=C$36`#$<4o z2Aq=F0bzwdNlU@mYD4k}PCy`=ROKjuMP9x;^KGmGwMRYm8*QDRWTM^$Gyh8QP44y# zw7$mydNNyM=`F6N=&QmP3(t%#k5_LV-qq&p!=wBhv8E=5kjvE3$O+~yx7&~UyC8_ zdv9csIBh?UT&>PkUg{VHHzZYoe}Xg?@|i;L__UJe=IPTwWY0%%dk#LMf0}Ac5k#XfN13Ts3vSg+4s*G0A2*i-!;o3ErBBhw2|*>K@EQww znf^f!xTE_@s7_PkuJ)~8rI}A;&6ld&a}7i3?1U)Pp-(-9EcnGvwz|YS&0_(h0e;dA zbBSOC`|;P9$%`iGmcT>9E6uKAPw4|J&SX)_6gE+>4gyy-1TB~UZUyw+;Zu=gr(wiZ z3HoBGc;BZ{)UPu5>~4^37zY%30f`CxB&WtPibuS|Y;D{aNIqr05-Z7eA%3ip5Su`- zSb#;)f^dqDc*mX?iLbEYa6E2NXN!=vFjGqjlm0fb%^zS;P-09~OdLn5d+7u9B8sZt zDL|(kE>dqXUPu>ov_Zx%jiZV+&c1+Ihn#>UE$`-B&VaOxE62#Es?vlP)aJgZDTVj= zYWcOyQ@GP-k72ie-G*$-V4@$%xbXoC=>+XyTwdF5t6j@^whHV|O!P*{YaUiQ5{b8; zr>x}Uo|yQW(=2Dw$3$c2=-K9-L`0=H1X&@y9nn@R*QmES;KDVBhKA1kI0RX&@Q&U( zZEv*fLeDCmj&40dS7Jl!^`ReE>(J!YL1Z|NP~R#`4!ZbzK&cLf6f*H`{#?q+dWJ)Z zE;le*hCP6kdU-5@x~nDj9$bd1to2-K2-4KyL^Xm5TB`CJJ|M13oBU>apA(C+IN+xc z{dvi-b$)i1jKBt;$rAG9&0t))j(N&03`^cbiCIttM9R5|C-^kg6(HsYK|Ho@j{1s$ zZhJ*9hkd?v%zE*6SFHZW=R#Uch#l2#bgAofCx}fDgHC-23)O2VYAEIdr&Iz4L6eh9 zvvdbLoEqmVgbVAi^EtCGjvb&p!z#3t`l%xw9*8i%i6)oV+COulKRG@iqiD17y!;yP zd!+y9?X@j{zP;Sg%Zxbl9Cy&Jl7X z1#?Mo4FtI~z0*VQWA%&DgYK2Z||2J*(0x8`gi> zxV0QcKX>)4YA2SUC3fkQyFdLjogxe(wgSJUofsu5w57^ z3+#?&yX#h36xC^deink;;{E+nyg};Nmpb9Ix4HJ?(rwoZ)#Odo$G|gtq~7YPqRh4( zh1ZA?z7enrUBo~5d>1fHwEuL8Y`nQ(^KeV-eyUKR7$WdAqkGklSBG49RabVZ@|_$U z5(RUUylOpjFk=d%4o#g01a`M7_MU_p8+dQZ^FB(UhhLaWUAB#1G$h2hB~+O%As$lX z;5DnxFjV|J1k)ejZQoz><{B+wxYAp$#rsZK%cH90XTbV+rNK`HD^$aDIy~$`kL=1V z`DjIA%#f)v6T$5{CSbt*co0r72lYjlUKk|PVo%7XI_b4T#PSd=@}MpzD6m6YMqxmg zog14%H-elu+8&v4tu$t6kCV{}wmPe-@$`>V=~P>Td7p3i__?d2W?didI7KO0`AtDS zNkYFh{fi?q_87+Zuy(-sy>bf*vYQb2Zu$O-%G;w6LaQy~^@6 zi%!2m+^_dUu`8tYw+hDBoVCb>vvT?YvVi1wJd0XA;TNQDu?xVxPSOf7n?0s5$TrhD4#!Ej8RWHotCK$T>pJr<6W}ft zs2=&E!~c=f`Z4B`3$P}ftU2Efp@%slfc-J;xRRfVU{RNDpRBms=jB%j5mx;R-|v;vEX+_-hII!_*f};KVAN?G&KRX2GAP z@M-P#1(Lu}Vf%(uI#n;@WUr&j6T9yeKm(vc3$0bvQVrP+0>Gj(#Mx=P07kC*HFfwN zL@_McO}h|6=EYg>1Wid!yHn^8@{Wrac4o6d;9D$$eI)Dq^iw7pk3j;75`Y_=EP$1W zV@}mQsr#6i*6kMpfC>Qgw};`VlrIpn0(C`5t*y2QT|UXZ83+LaJPXTFRLcbf&;$?? z*o01LS#cm2mpPaQt^Q6K4)<7a_aXez;t12qY*}+D5Y(;1-=Wkwzuh}`7!Jd@I*TP< z{kaqVyWCNRCgT21z|n_T{krVdCM4`SutmqRNR#5u{Qmfb-+6{vSI7Eyw!BMVJ_^_V z=e)8FLDBy9)HQtG^Qy*B9zxH2=uOs+Fi7E~92GST6s^KC-+fiaTdfwdNsskFo15Aw z>Y0)goNAwX{kFLGl+yEV)Wm3qF_(yxO)113`bU1q^?tmduw|-0m;uYduI4Y_u*6%Q zD_HN#Ir9SFY2xda>Rz&Y!FC)~sCq?a{nIB@6U;;a8yAD{C0-UVtlm}gpx(Jv#iCS5 za~|tC=IwX7Ce%$se?DYzGp13*Dcw74EzW6C4fnsgQ1_ftW(glh zYR`vEVWs!4#3U~BlYDPlNkkH3?^}zBVx;XO=;oPdo>SK>Wmc7%E)<{7oEXQ)P_97y zW^Mys9}K7)M**F*?y+#TLcw6>1W3pOwun;-HlY$c!d|P?OP0jdwL{H#Ju41xj#=wQ zK1%#&e@95andgyN3Xp->QqM`sS$Hr$>(OL$g~x$7q;xwy^sp4bD$|?g$X<~}&jbCG z!mwp&N@N864PGXd{FIENON#LY4&g3Hb68}-^3p7<7|&i7!qYv82c zWzcl^2op_+0jl*Z)ll9|^7uIEu}Vo`l`?kH>gC>=20o%p1Sho>_*hqbcTI!%!uka) zm37F1BxUAQlmHfdlujuuchBZ$u^?W4Db}C;@aS>HzF2dqzyMOy*Sh z(5Wv}OKL;O7>XObV}F;DhLVKI!>&4SlHa~ZNj{@va7%gk!tN9yH)f`)Y>BNNee-wqA@-P7 zmo+fE1fDFDy5jJ;Xx%Vphi<8q*sE+o6j#svA+b8COA9Tb>VG}kVH{;4npU-WV@SN> z7h5iYHXpu;bW`YCjvKbdZ+RuWyp}W%apAIAI#7XabEo}8k*lC(H12@_m>L8(PF&v^ zaNz#Z{+A36u5PQePx%t|DWl-{b)%94C(3iFnQCKqB@UdvUJ&t}uRrZ-(~}LzHt>s? zI4^1WJ-_da&#$`sHM;;m#u)`M=-XB+@(Dr3e1V1XFj+N$#+uG$EhjA+$Y(InEUE1| zzr;{K2u|<}LNm zeA;QzyA%d`Y%7x3CQmytPLj~7MjBV}+Y1oeosBMhsAZtpM^q-K2SK$1RuY)*r>Ac) zyx&D(@M4P!OS?bxb&=*qsLrp#$aL5l~B@cgqSn$l)9a+Ej#0$9I`r}~GR>lgGJLL0AYHaiMz z57?PKj3e0X-KfnMGScNGpI}CopnjI306}!4=8YMK!NNC_o5B*XvJ~Q7gN|s#j?BxH z&pqp-7!uE}Lf;N#&_OrAd-W3Ju4q6>@mIUVW8H-gbD950f3-t{IF#cVf1gTT#;Fi% zL3ztx?fKh2{6f@fl5oybzmlxNPrT}|$H{0{B)$ED+1bc(~OSM{-l{1dmLsMzh(PL+# z^-QYsfRKLw0CxvyusMaFRAGzu=X-Ta&i1yewRWmEXKzr^arb{88cLjS{NPaL18a*Igysgcdvt!TEjakV5xkVE<*{Q0J4)t!~JyB2ikK)7;hr{KEi1Gggj~dWS literal 0 HcmV?d00001 diff --git a/warpgate-admin/app/src/lib/api.ts b/warpgate-admin/app/src/lib/api.ts new file mode 100644 index 0000000..9f03115 --- /dev/null +++ b/warpgate-admin/app/src/lib/api.ts @@ -0,0 +1,8 @@ +import { DefaultApi, Configuration } from '../../api-client/src' + +const configuration = new Configuration({ + basePath: '/api' +}) + +export const api = new DefaultApi(configuration) +export * from '../../api-client/src/models' diff --git a/warpgate-admin/app/src/lib/ssh.ts b/warpgate-admin/app/src/lib/ssh.ts new file mode 100644 index 0000000..72ca716 --- /dev/null +++ b/warpgate-admin/app/src/lib/ssh.ts @@ -0,0 +1,5 @@ +import type { Target, UserSnapshot } from './api' + +export function getSSHUsername (user: UserSnapshot|undefined, target: Target|undefined): string { + return `${user?.username ?? ""}:${target?.name}` +} diff --git a/warpgate-admin/app/src/lib/store.ts b/warpgate-admin/app/src/lib/store.ts new file mode 100644 index 0000000..ec292eb --- /dev/null +++ b/warpgate-admin/app/src/lib/store.ts @@ -0,0 +1,3 @@ +import { writable } from 'svelte/store' + +export const authenticatedUsername = writable(null) diff --git a/warpgate-admin/app/src/lib/time.ts b/warpgate-admin/app/src/lib/time.ts new file mode 100644 index 0000000..5c9389b --- /dev/null +++ b/warpgate-admin/app/src/lib/time.ts @@ -0,0 +1,5 @@ +import moment from 'moment' + +export function timeAgo(t: any): string { + return moment(t).fromNow() +} diff --git a/warpgate-admin/app/src/main.ts b/warpgate-admin/app/src/main.ts new file mode 100644 index 0000000..3ee0213 --- /dev/null +++ b/warpgate-admin/app/src/main.ts @@ -0,0 +1,9 @@ +import '@fontsource/work-sans' +import './theme.scss' +import App from './App.svelte' + +const app = new App({ + target: document.getElementById('app')! +}) + +export default app diff --git a/warpgate-admin/app/src/theme.scss b/warpgate-admin/app/src/theme.scss new file mode 100644 index 0000000..4c11304 --- /dev/null +++ b/warpgate-admin/app/src/theme.scss @@ -0,0 +1,92 @@ +@import "bootstrap/scss/functions"; +// @import "bootstrap/scss/variables"; +@import "./vars"; + +// $component-hover-bg: rgba(#fff, .05); +// $component-active-color: hsl(224deg 100% 78%); +// $component-active-bg: hsl(224deg 73% 21% / 52%); + +// $list-group-bg: transparent; +// $list-group-color: #bbb; +// $list-group-hover-bg: $component-hover-bg; +// $list-group-action-hover-color: #fff; + + +// Configuration +@import "bootstrap/scss/mixins"; +@import "bootstrap/scss/utilities"; + +// Layout & components +@import "bootstrap/scss/root"; +@import "bootstrap/scss/reboot"; +@import "bootstrap/scss/type"; +// @import "bootstrap/scss/images"; +@import "bootstrap/scss/containers"; +@import "bootstrap/scss/grid"; +// @import "bootstrap/scss/tables"; +@import "bootstrap/scss/forms"; +@import "bootstrap/scss/buttons"; +@import "bootstrap/scss/transitions"; +// @import "bootstrap/scss/dropdown"; +// @import "bootstrap/scss/button-group"; +@import "bootstrap/scss/nav"; +@import "bootstrap/scss/navbar"; +// @import "bootstrap/scss/card"; +// @import "bootstrap/scss/accordion"; +// @import "bootstrap/scss/breadcrumb"; +// @import "bootstrap/scss/pagination"; +// @import "bootstrap/scss/badge"; +@import "bootstrap/scss/alert"; +// @import "bootstrap/scss/progress"; +@import "bootstrap/scss/list-group"; +@import "bootstrap/scss/close"; +// @import "bootstrap/scss/toasts"; +@import "bootstrap/scss/modal"; +@import "bootstrap/scss/tooltip"; +// @import "bootstrap/scss/popover"; +// @import "bootstrap/scss/carousel"; +@import "bootstrap/scss/spinners"; +// @import "bootstrap/scss/offcanvas"; +// @import "bootstrap/scss/placeholders"; + +// Helpers +@import "bootstrap/scss/helpers"; + +// Utilities +@import "bootstrap/scss/utilities/api"; + + +// .list-group-flush > .list-group-item { +// border-radius: $border-radius; +// text-shadow: 0 0 1px black; +// } + +a { + text-decoration-color: rgba($body-color, 0.25); + text-underline-offset: 2px; + + &:hover, &.active { + text-decoration-color: $body-color; + } +} + +.page-summary-bar { + display: flex; + align-items: center; + margin: 0.25rem 0 1.5rem; + + h1 { + margin: 0; + } +} + +.alert { + background: none !important; + border-top-style: none; + border-bottom-style: none; + border-right-style: none; + border-left-width: 2px; + font-style: italic; + padding: 0 10px; + margin: 20px 0; +} diff --git a/warpgate-admin/app/src/vars.scss b/warpgate-admin/app/src/vars.scss new file mode 100644 index 0000000..5dc79e4 --- /dev/null +++ b/warpgate-admin/app/src/vars.scss @@ -0,0 +1,11 @@ +@import "../node_modules/bootstrap/scss/functions"; + +$body-bg: #fffcf6; +$body-color: #555; +$font-family-sans-serif: "Work Sans", system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; +$link-color: $body-color; +$list-group-bg: transparent; +$alert-border-radius: 0; +$alert-border-scale: -30%; + +@import "../node_modules/bootstrap/scss/variables"; diff --git a/warpgate-admin/app/src/vite-env.d.ts b/warpgate-admin/app/src/vite-env.d.ts new file mode 100644 index 0000000..2007016 --- /dev/null +++ b/warpgate-admin/app/src/vite-env.d.ts @@ -0,0 +1,5 @@ +/// +/// + +// eslint-disable-next-line @typescript-eslint/no-type-alias +declare type GlobalFetch = WindowOrWorkerGlobalScope diff --git a/warpgate-admin/app/svelte.config.js b/warpgate-admin/app/svelte.config.js new file mode 100644 index 0000000..072e005 --- /dev/null +++ b/warpgate-admin/app/svelte.config.js @@ -0,0 +1,13 @@ +import sveltePreprocess from 'svelte-preprocess' + +export default { + compilerOptions: { + enableSourcemap: true, + }, + preprocess: sveltePreprocess({ + sourceMap: true, + }), + experimental: { + prebundleSvelteLibraries: true, + }, +} diff --git a/warpgate-admin/app/tsconfig.json b/warpgate-admin/app/tsconfig.json new file mode 100644 index 0000000..bf84cd2 --- /dev/null +++ b/warpgate-admin/app/tsconfig.json @@ -0,0 +1,37 @@ +{ + "extends": "@tsconfig/svelte/tsconfig.json", + "compilerOptions": { + "target": "esnext", + "useDefineForClassFields": true, + "module": "esnext", + "resolveJsonModule": true, + "strictNullChecks": true, + "baseUrl": ".", + "preserveValueImports": false, + "noUnusedLocals": false, + /** + * Typecheck JS in `.svelte` and `.js` files by default. + * Disable checkJs if you'd like to use dynamic types in JS. + * Note that setting allowJs false does not prevent the use + * of JS in `.svelte` files. + */ + "allowJs": true, + "checkJs": true, + "paths": { + "*": [ + "src/*" + ] + } + }, + "include": [ + "src/**/*.d.ts", + "src/**/*.ts", + "src/**/*.js", + "src/**/*.svelte" + ], + "references": [ + { + "path": "./tsconfig.node.json" + } + ] +} diff --git a/warpgate-admin/app/tsconfig.node.json b/warpgate-admin/app/tsconfig.node.json new file mode 100644 index 0000000..e993792 --- /dev/null +++ b/warpgate-admin/app/tsconfig.node.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "composite": true, + "module": "esnext", + "moduleResolution": "node" + }, + "include": ["vite.config.ts"] +} diff --git a/warpgate-admin/app/vite.config.ts b/warpgate-admin/app/vite.config.ts new file mode 100644 index 0000000..cc2fc43 --- /dev/null +++ b/warpgate-admin/app/vite.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from 'vite' +import { svelte } from '@sveltejs/vite-plugin-svelte' +import tsconfigPaths from 'vite-tsconfig-paths' +import * as checker from 'vite-plugin-checker/lib/main.js' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + svelte(), + tsconfigPaths(), + (checker.default.default)({ typescript: true }), + ], + build: { + sourcemap: true, + }, +}) diff --git a/warpgate-admin/app/yarn.lock b/warpgate-admin/app/yarn.lock new file mode 100644 index 0000000..0b378bb --- /dev/null +++ b/warpgate-admin/app/yarn.lock @@ -0,0 +1,2607 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@babel/code-frame@^7.12.13": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.16.7.tgz#44416b6bd7624b998f5b1af5d470856c40138789" + integrity sha512-iAXqUn8IIeBTNd72xsFlgaXHkMBMt6y4HJp1tIaK465CWLT/fG1aqB7ykr95gHHmlBdGbFeWWfyB4NJJ0nmeIg== + dependencies: + "@babel/highlight" "^7.16.7" + +"@babel/helper-validator-identifier@^7.16.7": + version "7.16.7" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.16.7.tgz#e8c602438c4a8195751243da9031d1607d247cad" + integrity sha512-hsEnFemeiW4D08A5gUAZxLBTXpZ39P+a+DGDsHw1yxqyQ/jzFEnxf5uTEGp+3bzAbNOxU1paTgYS4ECU/IgfDw== + +"@babel/highlight@^7.16.7": + version "7.16.10" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.16.10.tgz#744f2eb81579d6eea753c227b0f570ad785aba88" + integrity sha512-5FnTQLSLswEj6IkgVw5KusNUUFY9ZGqe/TRFnP/BKYHYgfh7tc+C7mwiy95/yNP7Dh9x580Vv8r7u7ZfTBFxdw== + dependencies: + "@babel/helper-validator-identifier" "^7.16.7" + chalk "^2.0.0" + js-tokens "^4.0.0" + +"@babel/runtime@^7.15.4": + version "7.17.2" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941" + integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw== + dependencies: + regenerator-runtime "^0.13.4" + +"@cush/relative@^1.0.0": + version "1.0.0" + resolved "https://registry.yarnpkg.com/@cush/relative/-/relative-1.0.0.tgz#8cd1769bf9bde3bb27dac356b1bc94af40f6cc16" + integrity sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA== + +"@eslint/eslintrc@^1.1.0": + version "1.1.0" + resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.1.0.tgz#583d12dbec5d4f22f333f9669f7d0b7c7815b4d3" + integrity sha512-C1DfL7XX4nPqGd6jcP01W9pVM1HYCuUkFk1432D7F0v3JSlUIeOYn9oCoi3eoLZ+iwBSb29BMFxxny0YrrEZqg== + dependencies: + ajv "^6.12.4" + debug "^4.3.2" + espree "^9.3.1" + globals "^13.9.0" + ignore "^4.0.6" + import-fresh "^3.2.1" + js-yaml "^4.1.0" + minimatch "^3.0.4" + strip-json-comments "^3.1.1" + +"@fontsource/work-sans@^4.5.7": + version "4.5.7" + resolved "https://registry.yarnpkg.com/@fontsource/work-sans/-/work-sans-4.5.7.tgz#e8d070896af8d751ca4064e9b0dd134faad3536b" + integrity sha512-DlVEYsShbL0ZUV96yPhie6rJN3eeCta4iI6UbLdbLptlLnkoryfbMIqeQLe+o7OsIoMNWqHTunKMW0x1BmUNpw== + +"@fortawesome/fontawesome-common-types@6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.1.1.tgz#7dc996042d21fc1ae850e3173b5c67b0549f9105" + integrity sha512-wVn5WJPirFTnzN6tR95abCx+ocH+3IFLXAgyavnf9hUmN0CfWoDjPT/BAWsUVwSlYYVBeCLJxaqi7ZGe4uSjBA== + +"@fortawesome/fontawesome-common-types@^0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-0.3.0.tgz#949995a05c0d8801be7e0a594f775f1dbaa0d893" + integrity sha512-CA3MAZBTxVsF6SkfkHXDerkhcQs0QPofy43eFdbWJJkZiq3SfiaH1msOkac59rQaqto5EqWnASboY1dBuKen5w== + +"@fortawesome/free-regular-svg-icons@^6.1.1": + version "6.1.1" + resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.1.1.tgz#3f2f58262a839edf0643cbacee7a8a8230061c98" + integrity sha512-xXiW7hcpgwmWtndKPOzG+43fPH7ZjxOaoeyooptSztGmJxCAflHZxXNK0GcT0uEsR4jTGQAfGklDZE5NHoBhKg== + dependencies: + "@fortawesome/fontawesome-common-types" "6.1.1" + +"@fortawesome/free-solid-svg-icons@^6.0.0": + version "6.0.0" + resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.0.0.tgz#bed4a501b631c6cfa35c09830f7cb63ffca1589d" + integrity sha512-o4FZ1XbndcgeWNb8Wh0y+Hgf73CjmyOQowUSaqQCtgIIdS+XliSBSOwCl330wER+I6CGYE96hT27bHBPmzX2Gg== + dependencies: + "@fortawesome/fontawesome-common-types" "^0.3.0" + +"@humanwhocodes/config-array@^0.9.2": + version "0.9.3" + resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.9.3.tgz#f2564c744b387775b436418491f15fce6601f63e" + integrity sha512-3xSMlXHh03hCcCmFc0rbKp3Ivt2PFEJnQUJDDMTJQ2wkECZWdq4GePs2ctc5H8zV+cHPaq8k2vU8mrQjA6iHdQ== + dependencies: + "@humanwhocodes/object-schema" "^1.2.1" + debug "^4.1.1" + minimatch "^3.0.4" + +"@humanwhocodes/object-schema@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz#b520529ec21d8e5945a1851dfd1c32e94e39ff45" + integrity sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA== + +"@nestjs/common@8.2.6": + version "8.2.6" + resolved "https://registry.yarnpkg.com/@nestjs/common/-/common-8.2.6.tgz#34cd5cc44082d3525c56c95db42ca0e5277b7d85" + integrity sha512-flLYSXunxcKyjbYddrhwbc49uE705MxBt85rS3mHyhDbAIPSGGeZEqME44YyAzCg1NTfJSNe7ztmOce5kNkb9A== + dependencies: + axios "0.24.0" + iterare "1.2.1" + tslib "2.3.1" + uuid "8.3.2" + +"@nestjs/core@8.2.6": + version "8.2.6" + resolved "https://registry.yarnpkg.com/@nestjs/core/-/core-8.2.6.tgz#08eb38203fb01a828227ea25972d38bfef5c818f" + integrity sha512-NwPcEIMmCsucs3QaDlQvkoU1FlFM2wm/WjaqLQhkSoIEmAR1gNtBo88f5io5cpMwCo1k5xYhqGlaSl6TfngwWQ== + dependencies: + "@nuxtjs/opencollective" "0.3.2" + fast-safe-stringify "2.1.1" + iterare "1.2.1" + object-hash "2.2.0" + path-to-regexp "3.2.0" + tslib "2.3.1" + uuid "8.3.2" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@nuxtjs/opencollective@0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@nuxtjs/opencollective/-/opencollective-0.3.2.tgz#620ce1044f7ac77185e825e1936115bb38e2681c" + integrity sha512-um0xL3fO7Mf4fDxcqx9KryrB7zgRM5JSlvGN5AGkP6JLM5XEKyjeAiPbNxdXVXQ16isuAhYpvP88NgL2BGd6aA== + dependencies: + chalk "^4.1.0" + consola "^2.15.0" + node-fetch "^2.6.1" + +"@openapitools/openapi-generator-cli@^2.4.26": + version "2.4.26" + resolved "https://registry.yarnpkg.com/@openapitools/openapi-generator-cli/-/openapi-generator-cli-2.4.26.tgz#67622fc41c258aeae3ff074cd92772978e03484f" + integrity sha512-O42H9q1HWGoIpcpMaUu318b6bmOgcjP3MieHwOrFdoG3KyttceBGlbLf9Kbf7WM91WSNCDXum7cnEKASuoGjAg== + dependencies: + "@nestjs/common" "8.2.6" + "@nestjs/core" "8.2.6" + "@nuxtjs/opencollective" "0.3.2" + chalk "4.1.2" + commander "8.3.0" + compare-versions "3.6.0" + concurrently "6.5.1" + console.table "0.10.0" + fs-extra "10.0.0" + glob "7.1.6" + inquirer "8.2.0" + lodash "4.17.21" + reflect-metadata "0.1.13" + rxjs "7.5.2" + tslib "2.0.3" + +"@popperjs/core@^2.9.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" + integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== + +"@rollup/pluginutils@^4.1.2": + version "4.1.2" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-4.1.2.tgz#ed5821c15e5e05e32816f5fb9ec607cdf5a75751" + integrity sha512-ROn4qvkxP9SyPeHaf7uQC/GPFY6L/OWy9+bd9AwcjOAWQwxRscoEyAUD8qCY5o5iL4jqQwoLk2kaTKJPb/HwzQ== + dependencies: + estree-walker "^2.0.1" + picomatch "^2.2.2" + +"@sveltejs/vite-plugin-svelte@^1.0.0-next.30": + version "1.0.0-next.37" + resolved "https://registry.yarnpkg.com/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-1.0.0-next.37.tgz#bb553425a3f9b780221134b04b9ace4165279d3c" + integrity sha512-EdSXw2rXeOahNrQfMJVZxa/NxZxW1a0TiBI3s+pVxnxU14hEQtnkLtdbTFhnceu22gJpNPFSIJRcIwRBBDQIeA== + dependencies: + "@rollup/pluginutils" "^4.1.2" + debug "^4.3.3" + kleur "^4.1.4" + magic-string "^0.25.7" + svelte-hmr "^0.14.9" + +"@tsconfig/svelte@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@tsconfig/svelte/-/svelte-3.0.0.tgz#b06e059209f04c414de0069f2f0e2796d979fc6f" + integrity sha512-pYrtLtOwku/7r1i9AMONsJMVYAtk3hzOfiGNekhtq5tYBGA7unMve8RvUclKLMT3PrihvJqUmzsRGh0RP84hKg== + +"@types/json-schema@^7.0.9": + version "7.0.9" + resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.9.tgz#97edc9037ea0c38585320b28964dde3b39e4660d" + integrity sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ== + +"@types/json5@^0.0.29": + version "0.0.29" + resolved "https://registry.yarnpkg.com/@types/json5/-/json5-0.0.29.tgz#ee28707ae94e11d2b827bcbe5270bcea7f3e71ee" + integrity sha1-7ihweulOEdK4J7y+UnC86n8+ce4= + +"@types/node@*": + version "17.0.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-17.0.18.tgz#3b4fed5cfb58010e3a2be4b6e74615e4847f1074" + integrity sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA== + +"@types/pug@^2.0.4": + version "2.0.6" + resolved "https://registry.yarnpkg.com/@types/pug/-/pug-2.0.6.tgz#f830323c88172e66826d0bde413498b61054b5a6" + integrity sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg== + +"@types/sass@^1.16.0": + version "1.43.1" + resolved "https://registry.yarnpkg.com/@types/sass/-/sass-1.43.1.tgz#86bb0168e9e881d7dade6eba16c9ed6d25dc2f68" + integrity sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g== + dependencies: + "@types/node" "*" + +"@typescript-eslint/eslint-plugin@^5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.12.0.tgz#bb46dd7ce7015c0928b98af1e602118e97df6c70" + integrity sha512-fwCMkDimwHVeIOKeBHiZhRUfJXU8n6xW1FL9diDxAyGAFvKcH4csy0v7twivOQdQdA0KC8TDr7GGRd3L4Lv0rQ== + dependencies: + "@typescript-eslint/scope-manager" "5.12.0" + "@typescript-eslint/type-utils" "5.12.0" + "@typescript-eslint/utils" "5.12.0" + debug "^4.3.2" + functional-red-black-tree "^1.0.1" + ignore "^5.1.8" + regexpp "^3.2.0" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/parser@^5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-5.12.0.tgz#0ca669861813df99ce54916f66f524c625ed2434" + integrity sha512-MfSwg9JMBojMUoGjUmX+D2stoQj1CBYTCP0qnnVtu9A+YQXVKNtLjasYh+jozOcrb/wau8TCfWOkQTiOAruBog== + dependencies: + "@typescript-eslint/scope-manager" "5.12.0" + "@typescript-eslint/types" "5.12.0" + "@typescript-eslint/typescript-estree" "5.12.0" + debug "^4.3.2" + +"@typescript-eslint/scope-manager@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-5.12.0.tgz#59619e6e5e2b1ce6cb3948b56014d3a24da83f5e" + integrity sha512-GAMobtIJI8FGf1sLlUWNUm2IOkIjvn7laFWyRx7CLrv6nLBI7su+B7lbStqVlK5NdLvHRFiJo2HhiDF7Ki01WQ== + dependencies: + "@typescript-eslint/types" "5.12.0" + "@typescript-eslint/visitor-keys" "5.12.0" + +"@typescript-eslint/type-utils@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/type-utils/-/type-utils-5.12.0.tgz#aaf45765de71c6d9707c66ccff76ec2b9aa31bb6" + integrity sha512-9j9rli3zEBV+ae7rlbBOotJcI6zfc6SHFMdKI9M3Nc0sy458LJ79Os+TPWeBBL96J9/e36rdJOfCuyRSgFAA0Q== + dependencies: + "@typescript-eslint/utils" "5.12.0" + debug "^4.3.2" + tsutils "^3.21.0" + +"@typescript-eslint/types@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-5.12.0.tgz#5b4030a28222ee01e851836562c07769eecda0b8" + integrity sha512-JowqbwPf93nvf8fZn5XrPGFBdIK8+yx5UEGs2QFAYFI8IWYfrzz+6zqlurGr2ctShMaJxqwsqmra3WXWjH1nRQ== + +"@typescript-eslint/typescript-estree@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-5.12.0.tgz#cabf545fd592722f0e2b4104711e63bf89525cd2" + integrity sha512-Dd9gVeOqt38QHR0BEA8oRaT65WYqPYbIc5tRFQPkfLquVEFPD1HAtbZT98TLBkEcCkvwDYOAvuSvAD9DnQhMfQ== + dependencies: + "@typescript-eslint/types" "5.12.0" + "@typescript-eslint/visitor-keys" "5.12.0" + debug "^4.3.2" + globby "^11.0.4" + is-glob "^4.0.3" + semver "^7.3.5" + tsutils "^3.21.0" + +"@typescript-eslint/utils@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/utils/-/utils-5.12.0.tgz#92fd3193191621ab863add2f553a7b38b65646af" + integrity sha512-k4J2WovnMPGI4PzKgDtQdNrCnmBHpMUFy21qjX2CoPdoBcSBIMvVBr9P2YDP8jOqZOeK3ThOL6VO/sy6jtnvzw== + dependencies: + "@types/json-schema" "^7.0.9" + "@typescript-eslint/scope-manager" "5.12.0" + "@typescript-eslint/types" "5.12.0" + "@typescript-eslint/typescript-estree" "5.12.0" + eslint-scope "^5.1.1" + eslint-utils "^3.0.0" + +"@typescript-eslint/visitor-keys@5.12.0": + version "5.12.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-5.12.0.tgz#1ac9352ed140b07ba144ebf371b743fdf537ec16" + integrity sha512-cFwTlgnMV6TgezQynx2c/4/tx9Tufbuo9LPzmWqyRC3QC4qTGkAG1C6pBr0/4I10PAI/FlYunI3vJjIcu+ZHMg== + dependencies: + "@typescript-eslint/types" "5.12.0" + eslint-visitor-keys "^3.0.0" + +acorn-jsx@^5.3.1: + version "5.3.2" + resolved "https://registry.yarnpkg.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz#7ed5bb55908b3b2f1bc55c6af1653bada7f07937" + integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== + +acorn@^8.7.0: + version "8.7.0" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.7.0.tgz#90951fde0f8f09df93549481e5fc141445b791cf" + integrity sha512-V/LGr1APy+PXIwKebEWrkZPwoeoF+w1jiOBUmuxuiUIaOHtob8Qc9BTrYo7VuI5fR8tqsy+buA2WFooR5olqvQ== + +ajv@^6.10.0, ajv@^6.12.4: + version "6.12.6" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.12.6.tgz#baf5a62e802b07d977034586f8c3baf5adf26df4" + integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g== + dependencies: + fast-deep-equal "^3.1.1" + fast-json-stable-stringify "^2.0.0" + json-schema-traverse "^0.4.1" + uri-js "^4.2.2" + +ansi-escapes@^4.2.1, ansi-escapes@^4.3.0: + version "4.3.2" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.3.2.tgz#6b2291d1db7d98b6521d5f1efa42d0f3a9feb65e" + integrity sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ== + dependencies: + type-fest "^0.21.3" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-styles@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.1.tgz#41fbb20243e50b12be0f04b8dedbf07520ce841d" + integrity sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA== + dependencies: + color-convert "^1.9.0" + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +anymatch@~3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.2.tgz#c0557c096af32f106198f4f4e2a383537e378716" + integrity sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + +array-includes@^3.1.4: + version "3.1.4" + resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.4.tgz#f5b493162c760f3539631f005ba2bb46acb45ba9" + integrity sha512-ZTNSQkmWumEbiHO2GF4GmWxYVTiQyJy2XOTa15sdQSrvKn7l+180egQMqlrMOUMCyLMD7pmyQe4mMDUT6Behrw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + get-intrinsic "^1.1.1" + is-string "^1.0.7" + +array-union@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" + integrity sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw== + +array.prototype.flat@^1.2.5: + version "1.2.5" + resolved "https://registry.yarnpkg.com/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" + integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.0" + +asciinema-player@3.0.0-rc.1: + version "3.0.0-rc.1" + resolved "https://registry.yarnpkg.com/asciinema-player/-/asciinema-player-3.0.0-rc.1.tgz#dfb6394307490ecfac49bead9381de5c52ebf243" + integrity sha512-r0yRCnifQ+UuyInLBwanupOUk7FPIs1NgD3D+egaSCXzK1+PSQf0aHo/dfpZFY2sml9mA0cqUHJFQ4KnuUJS1Q== + dependencies: + "@babel/runtime" "^7.15.4" + solid-js "^1.1.6" + +axios@0.24.0: + version "0.24.0" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6" + integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== + dependencies: + follow-redirects "^1.14.4" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +binary-extensions@^2.0.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d" + integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA== + +bl@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-4.1.0.tgz#451535264182bec2fbbc83a62ab98cf11d9f7b3a" + integrity sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w== + dependencies: + buffer "^5.5.0" + inherits "^2.0.4" + readable-stream "^3.4.0" + +bootstrap@^5.1.3: + version "5.1.3" + resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.1.3.tgz#ba081b0c130f810fa70900acbc1c6d3c28fa8f34" + integrity sha512-fcQztozJ8jToQWXxVuEyXWW+dSo8AiXWKwiSSrKWsRB/Qt+Ewwza+JWoLKiTuQLaEPhdNAJ7+Dosc9DOIqNy7Q== + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +braces@^3.0.1, braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + +buffer-crc32@^0.2.5: + version "0.2.13" + resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" + integrity sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI= + +buffer@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-5.7.1.tgz#ba62e7c13133053582197160851a8f648e99eed0" + integrity sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.1.13" + +call-bind@^1.0.0, call-bind@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.2.tgz#b1d4e89e688119c3c9a903ad30abb2f6a919be3c" + integrity sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA== + dependencies: + function-bind "^1.1.1" + get-intrinsic "^1.0.2" + +callsites@^3.0.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/callsites/-/callsites-3.1.0.tgz#b3630abd8943432f54b3f0519238e33cd7df2f73" + integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== + +chalk@4.1.2, chalk@^4.0.0, chalk@^4.1.0, chalk@^4.1.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chalk@^2.0.0: + version "2.4.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" + integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== + dependencies: + ansi-styles "^3.2.1" + escape-string-regexp "^1.0.5" + supports-color "^5.3.0" + +chardet@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/chardet/-/chardet-0.7.0.tgz#90094849f0937f2eedc2425d0d28a9e5f0cbad9e" + integrity sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA== + +"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1, chokidar@^3.5.1: + version "3.5.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" + integrity sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + +cli-spinners@^2.5.0: + version "2.6.1" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-2.6.1.tgz#adc954ebe281c37a6319bfa401e6dd2488ffb70d" + integrity sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g== + +cli-width@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/cli-width/-/cli-width-3.0.0.tgz#a2f48437a2caa9a22436e794bf071ec9e61cedf6" + integrity sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw== + +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + +clone@^1.0.2: + version "1.0.4" + resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" + integrity sha1-2jCcwmPfFZlMaIypAheco8fNfH4= + +color-convert@^1.9.0: + version "1.9.3" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-1.9.3.tgz#bb71850690e1f136567de629d2d5471deda4c1e8" + integrity sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg== + dependencies: + color-name "1.1.3" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" + integrity sha1-p9BVi9icQveV3UIyj3QIMcpTvCU= + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@8.3.0, commander@^8.0.0: + version "8.3.0" + resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" + integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== + +compare-versions@3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-3.6.0.tgz#1a5689913685e5a87637b8d3ffca75514ec41d62" + integrity sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha1-2Klr13/Wjfd5OnMDajug1UBdR3s= + +concurrently@6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/concurrently/-/concurrently-6.5.1.tgz#4518c67f7ac680cf5c34d5adf399a2a2047edc8c" + integrity sha512-FlSwNpGjWQfRwPLXvJ/OgysbBxPkWpiVjy1042b0U7on7S7qwwMIILRj7WTN1mTgqa582bG6NFuScOoh6Zgdag== + dependencies: + chalk "^4.1.0" + date-fns "^2.16.1" + lodash "^4.17.21" + rxjs "^6.6.3" + spawn-command "^0.0.2-1" + supports-color "^8.1.0" + tree-kill "^1.2.2" + yargs "^16.2.0" + +consola@^2.15.0: + version "2.15.3" + resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550" + integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw== + +console.table@0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/console.table/-/console.table-0.10.0.tgz#0917025588875befd70cf2eff4bef2c6e2d75d04" + integrity sha1-CRcCVYiHW+/XDPLv9L7yxuLXXQQ= + dependencies: + easy-table "1.1.0" + +cross-spawn@^7.0.2: + version "7.0.3" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" + integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +date-fns@^2.16.1: + version "2.28.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" + integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw== + +debug@^2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +debug@^3.2.7: + version "3.2.7" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== + dependencies: + ms "^2.1.1" + +debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.3.tgz#04266e0b70a98d4462e6e288e38259213332b664" + integrity sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q== + dependencies: + ms "2.1.2" + +deep-is@^0.1.3: + version "0.1.4" + resolved "https://registry.yarnpkg.com/deep-is/-/deep-is-0.1.4.tgz#a6f2dce612fadd2ef1f519b73551f17e85199831" + integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== + +defaults@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/defaults/-/defaults-1.0.3.tgz#c656051e9817d9ff08ed881477f3fe4019f3ef7d" + integrity sha1-xlYFHpgX2f8I7YgUd/P+QBnz730= + dependencies: + clone "^1.0.2" + +define-properties@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.1.3.tgz#cf88da6cbee26fe6db7094f61d870cbd84cee9f1" + integrity sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ== + dependencies: + object-keys "^1.0.12" + +detect-indent@^6.0.0: + version "6.1.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.1.0.tgz#592485ebbbf6b3b1ab2be175c8393d04ca0d57e6" + integrity sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA== + +dir-glob@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" + integrity sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA== + dependencies: + path-type "^4.0.0" + +doctrine@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" + integrity sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== + dependencies: + esutils "^2.0.2" + +doctrine@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-3.0.0.tgz#addebead72a6574db783639dc87a121773973961" + integrity sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w== + dependencies: + esutils "^2.0.2" + +easy-table@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/easy-table/-/easy-table-1.1.0.tgz#86f9ab4c102f0371b7297b92a651d5824bc8cb73" + integrity sha1-hvmrTBAvA3G3KXuSplHVgkvIy3M= + optionalDependencies: + wcwidth ">=1.0.1" + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +es-abstract@^1.19.0, es-abstract@^1.19.1: + version "1.19.1" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.19.1.tgz#d4885796876916959de78edaa0df456627115ec3" + integrity sha512-2vJ6tjA/UfqLm2MPs7jxVybLoB8i1t1Jd9R3kISld20sIxPcTbLuggQOUxeWeAvIUkduv/CfMjuh4WmiXr2v9w== + dependencies: + call-bind "^1.0.2" + es-to-primitive "^1.2.1" + function-bind "^1.1.1" + get-intrinsic "^1.1.1" + get-symbol-description "^1.0.0" + has "^1.0.3" + has-symbols "^1.0.2" + internal-slot "^1.0.3" + is-callable "^1.2.4" + is-negative-zero "^2.0.1" + is-regex "^1.1.4" + is-shared-array-buffer "^1.0.1" + is-string "^1.0.7" + is-weakref "^1.0.1" + object-inspect "^1.11.0" + object-keys "^1.1.1" + object.assign "^4.1.2" + string.prototype.trimend "^1.0.4" + string.prototype.trimstart "^1.0.4" + unbox-primitive "^1.0.1" + +es-to-primitive@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.2.1.tgz#e55cd4c9cdc188bcefb03b366c736323fc5c898a" + integrity sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA== + dependencies: + is-callable "^1.1.4" + is-date-object "^1.0.1" + is-symbol "^1.0.2" + +es6-promise@^3.1.2: + version "3.3.1" + resolved "https://registry.yarnpkg.com/es6-promise/-/es6-promise-3.3.1.tgz#a08cdde84ccdbf34d027a1451bc91d4bcd28a613" + integrity sha1-oIzd6EzNvzTQJ6FFG8kdS80ophM= + +esbuild-android-arm64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-android-arm64/-/esbuild-android-arm64-0.14.22.tgz#fb051169a63307d958aec85ad596cfc7d7770303" + integrity sha512-k1Uu4uC4UOFgrnTj2zuj75EswFSEBK+H6lT70/DdS4mTAOfs2ECv2I9ZYvr3w0WL0T4YItzJdK7fPNxcPw6YmQ== + +esbuild-darwin-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-darwin-64/-/esbuild-darwin-64-0.14.22.tgz#615ea0a9de67b57a293a7128d7ac83ee307a856d" + integrity sha512-d8Ceuo6Vw6HM3fW218FB6jTY6O3r2WNcTAU0SGsBkXZ3k8SDoRLd3Nrc//EqzdgYnzDNMNtrWegK2Qsss4THhw== + +esbuild-darwin-arm64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.14.22.tgz#82054dcfcecb15ccfd237093b8008e7745a99ad9" + integrity sha512-YAt9Tj3SkIUkswuzHxkaNlT9+sg0xvzDvE75LlBo4DI++ogSgSmKNR6B4eUhU5EUUepVXcXdRIdqMq9ppeRqfw== + +esbuild-freebsd-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-64/-/esbuild-freebsd-64-0.14.22.tgz#778a818c5b078d5cdd6bb6c0e0797217d196999b" + integrity sha512-ek1HUv7fkXMy87Qm2G4IRohN+Qux4IcnrDBPZGXNN33KAL0pEJJzdTv0hB/42+DCYWylSrSKxk3KUXfqXOoH4A== + +esbuild-freebsd-arm64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.14.22.tgz#18da93b9f3db2e036f72383bfe73b28b73bb332c" + integrity sha512-zPh9SzjRvr9FwsouNYTqgqFlsMIW07O8mNXulGeQx6O5ApgGUBZBgtzSlBQXkHi18WjrosYfsvp5nzOKiWzkjQ== + +esbuild-linux-32@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-32/-/esbuild-linux-32-0.14.22.tgz#d0d5d9f5bb3536e17ac097e9512019c65b7c0234" + integrity sha512-SnpveoE4nzjb9t2hqCIzzTWBM0RzcCINDMBB67H6OXIuDa4KqFqaIgmTchNA9pJKOVLVIKd5FYxNiJStli21qg== + +esbuild-linux-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-64/-/esbuild-linux-64-0.14.22.tgz#2773d540971999ea7f38107ef92fca753f6a8c30" + integrity sha512-Zcl9Wg7gKhOWWNqAjygyqzB+fJa19glgl2JG7GtuxHyL1uEnWlpSMytTLMqtfbmRykIHdab797IOZeKwk5g0zg== + +esbuild-linux-arm64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm64/-/esbuild-linux-arm64-0.14.22.tgz#5d4480ce6d6bffab1dd76a23158f5a5ab33e7ba4" + integrity sha512-8q/FRBJtV5IHnQChO3LHh/Jf7KLrxJ/RCTGdBvlVZhBde+dk3/qS9fFsUy+rs3dEi49aAsyVitTwlKw1SUFm+A== + +esbuild-linux-arm@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-arm/-/esbuild-linux-arm-0.14.22.tgz#c6391b3f7c8fa6d3b99a7e893ce0f45f3a921eef" + integrity sha512-soPDdbpt/C0XvOOK45p4EFt8HbH5g+0uHs5nUKjHVExfgR7du734kEkXR/mE5zmjrlymk5AA79I0VIvj90WZ4g== + +esbuild-linux-mips64le@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.14.22.tgz#2c8dabac355c502e86c38f9f292b3517d8e181f3" + integrity sha512-SiNDfuRXhGh1JQLLA9JPprBgPVFOsGuQ0yDfSPTNxztmVJd8W2mX++c4FfLpAwxuJe183mLuKf7qKCHQs5ZnBQ== + +esbuild-linux-ppc64le@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.14.22.tgz#69d71b2820d5c94306072dac6094bae38e77d1c0" + integrity sha512-6t/GI9I+3o1EFm2AyN9+TsjdgWCpg2nwniEhjm2qJWtJyJ5VzTXGUU3alCO3evopu8G0hN2Bu1Jhz2YmZD0kng== + +esbuild-linux-riscv64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.14.22.tgz#c0ec0fc3a23624deebf657781550d2329cec4213" + integrity sha512-AyJHipZKe88sc+tp5layovquw5cvz45QXw5SaDgAq2M911wLHiCvDtf/07oDx8eweCyzYzG5Y39Ih568amMTCQ== + +esbuild-linux-s390x@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-linux-s390x/-/esbuild-linux-s390x-0.14.22.tgz#ec2af4572d63336cfb27f5a5c851fb1b6617dd91" + integrity sha512-Sz1NjZewTIXSblQDZWEFZYjOK6p8tV6hrshYdXZ0NHTjWE+lwxpOpWeElUGtEmiPcMT71FiuA9ODplqzzSxkzw== + +esbuild-netbsd-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-netbsd-64/-/esbuild-netbsd-64-0.14.22.tgz#0e283278e9fdbaa7f0930f93ee113d7759cd865e" + integrity sha512-TBbCtx+k32xydImsHxvFgsOCuFqCTGIxhzRNbgSL1Z2CKhzxwT92kQMhxort9N/fZM2CkRCPPs5wzQSamtzEHA== + +esbuild-openbsd-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-openbsd-64/-/esbuild-openbsd-64-0.14.22.tgz#2a73bba04e16d8ef278fbe2be85248e12a2f2cc2" + integrity sha512-vK912As725haT313ANZZZN+0EysEEQXWC/+YE4rQvOQzLuxAQc2tjbzlAFREx3C8+uMuZj/q7E5gyVB7TzpcTA== + +esbuild-sunos-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-sunos-64/-/esbuild-sunos-64-0.14.22.tgz#8fe03513b8b2e682a6d79d5e3ca5849651a3c1d8" + integrity sha512-/mbJdXTW7MTcsPhtfDsDyPEOju9EOABvCjeUU2OJ7fWpX/Em/H3WYDa86tzLUbcVg++BScQDzqV/7RYw5XNY0g== + +esbuild-windows-32@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-windows-32/-/esbuild-windows-32-0.14.22.tgz#a75df61e3e49df292a1842be8e877a3153ee644f" + integrity sha512-1vRIkuvPTjeSVK3diVrnMLSbkuE36jxA+8zGLUOrT4bb7E/JZvDRhvtbWXWaveUc/7LbhaNFhHNvfPuSw2QOQg== + +esbuild-windows-64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-windows-64/-/esbuild-windows-64-0.14.22.tgz#d06cf8bbe4945b8bf95a730d871e54a22f635941" + integrity sha512-AxjIDcOmx17vr31C5hp20HIwz1MymtMjKqX4qL6whPj0dT9lwxPexmLj6G1CpR3vFhui6m75EnBEe4QL82SYqw== + +esbuild-windows-arm64@0.14.22: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild-windows-arm64/-/esbuild-windows-arm64-0.14.22.tgz#f8b1b05c548073be8413a5ecb12d7c2f6e717227" + integrity sha512-5wvQ+39tHmRhNpu2Fx04l7QfeK3mQ9tKzDqqGR8n/4WUxsFxnVLfDRBGirIfk4AfWlxk60kqirlODPoT5LqMUg== + +esbuild@^0.14.14: + version "0.14.22" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.14.22.tgz#2b55fde89d7aa5aaaad791816d58ff9dfc5ed085" + integrity sha512-CjFCFGgYtbFOPrwZNJf7wsuzesx8kqwAffOlbYcFDLFuUtP8xloK1GH+Ai13Qr0RZQf9tE7LMTHJ2iVGJ1SKZA== + optionalDependencies: + esbuild-android-arm64 "0.14.22" + esbuild-darwin-64 "0.14.22" + esbuild-darwin-arm64 "0.14.22" + esbuild-freebsd-64 "0.14.22" + esbuild-freebsd-arm64 "0.14.22" + esbuild-linux-32 "0.14.22" + esbuild-linux-64 "0.14.22" + esbuild-linux-arm "0.14.22" + esbuild-linux-arm64 "0.14.22" + esbuild-linux-mips64le "0.14.22" + esbuild-linux-ppc64le "0.14.22" + esbuild-linux-riscv64 "0.14.22" + esbuild-linux-s390x "0.14.22" + esbuild-netbsd-64 "0.14.22" + esbuild-openbsd-64 "0.14.22" + esbuild-sunos-64 "0.14.22" + esbuild-windows-32 "0.14.22" + esbuild-windows-64 "0.14.22" + esbuild-windows-arm64 "0.14.22" + +escalade@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40" + integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw== + +escape-string-regexp@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" + integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= + +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + +eslint-config-standard@^16.0.3: + version "16.0.3" + resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-16.0.3.tgz#6c8761e544e96c531ff92642eeb87842b8488516" + integrity sha512-x4fmJL5hGqNJKGHSjnLdgA6U6h1YW/G2dW9fA+cyVur4SK6lyue8+UgNKWlZtUDTXvgKDD/Oa3GQjmB5kjtVvg== + +eslint-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== + dependencies: + debug "^3.2.7" + resolve "^1.20.0" + +eslint-import-resolver-typescript@^2.5.0: + version "2.5.0" + resolved "https://registry.yarnpkg.com/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-2.5.0.tgz#07661966b272d14ba97f597b51e1a588f9722f0a" + integrity sha512-qZ6e5CFr+I7K4VVhQu3M/9xGv9/YmwsEXrsm3nimw8vWaVHRDrQRp26BgCypTxBp3vUp4o5aVEJRiy0F2DFddQ== + dependencies: + debug "^4.3.1" + glob "^7.1.7" + is-glob "^4.0.1" + resolve "^1.20.0" + tsconfig-paths "^3.9.0" + +eslint-module-utils@^2.7.2: + version "2.7.3" + resolved "https://registry.yarnpkg.com/eslint-module-utils/-/eslint-module-utils-2.7.3.tgz#ad7e3a10552fdd0642e1e55292781bd6e34876ee" + integrity sha512-088JEC7O3lDZM9xGe0RerkOMd0EjFl+Yvd1jPWIkMT5u3H9+HC34mWWPnqPrN13gieT9pBOO+Qt07Nb/6TresQ== + dependencies: + debug "^3.2.7" + find-up "^2.1.0" + +eslint-plugin-es@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893" + integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ== + dependencies: + eslint-utils "^2.0.0" + regexpp "^3.0.0" + +eslint-plugin-import@^2.23.4: + version "2.25.4" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.25.4.tgz#322f3f916a4e9e991ac7af32032c25ce313209f1" + integrity sha512-/KJBASVFxpu0xg1kIBn9AUa8hQVnszpwgE7Ld0lKAlx7Ie87yzEzCgSkekt+le/YVhiaosO4Y14GDAOc41nfxA== + dependencies: + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" + debug "^2.6.9" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.7.2" + has "^1.0.3" + is-core-module "^2.8.0" + is-glob "^4.0.3" + minimatch "^3.0.4" + object.values "^1.1.5" + resolve "^1.20.0" + tsconfig-paths "^3.12.0" + +eslint-plugin-node@^11.1.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d" + integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g== + dependencies: + eslint-plugin-es "^3.0.0" + eslint-utils "^2.0.0" + ignore "^5.1.1" + minimatch "^3.0.4" + resolve "^1.10.1" + semver "^6.1.0" + +eslint-plugin-promise@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-6.0.0.tgz#017652c07c9816413a41e11c30adc42c3d55ff18" + integrity sha512-7GPezalm5Bfi/E22PnQxDWH2iW9GTvAlUNTztemeHb6c1BniSyoeTrM87JkC0wYdi6aQrZX9p2qEiAno8aTcbw== + +eslint-plugin-svelte3@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-svelte3/-/eslint-plugin-svelte3-3.4.0.tgz#0fe6cfcd42a53ff346082d47e7386be66bd8d90e" + integrity sha512-MIQUTuRv3o7LyQ+360qOc9mLT35j1I5YzHr04g/UDcvJTpg0X/kHWELY99ve869Rp/9wjqD7I26Aq5H8OH5RIg== + +eslint-scope@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" + integrity sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw== + dependencies: + esrecurse "^4.3.0" + estraverse "^4.1.1" + +eslint-scope@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-7.1.1.tgz#fff34894c2f65e5226d3041ac480b4513a163642" + integrity sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw== + dependencies: + esrecurse "^4.3.0" + estraverse "^5.2.0" + +eslint-utils@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-2.1.0.tgz#d2de5e03424e707dc10c74068ddedae708741b27" + integrity sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg== + dependencies: + eslint-visitor-keys "^1.1.0" + +eslint-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-3.0.0.tgz#8aebaface7345bb33559db0a1f13a1d2d48c3672" + integrity sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA== + dependencies: + eslint-visitor-keys "^2.0.0" + +eslint-visitor-keys@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz#30ebd1ef7c2fdff01c3a4f151044af25fab0523e" + integrity sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ== + +eslint-visitor-keys@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz#f65328259305927392c938ed44eb0a5c9b2bd303" + integrity sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw== + +eslint-visitor-keys@^3.0.0, eslint-visitor-keys@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-3.3.0.tgz#f6480fa6b1f30efe2d1968aa8ac745b862469826" + integrity sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA== + +eslint@^8.9.0: + version "8.9.0" + resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.9.0.tgz#a2a8227a99599adc4342fd9b854cb8d8d6412fdb" + integrity sha512-PB09IGwv4F4b0/atrbcMFboF/giawbBLVC7fyDamk5Wtey4Jh2K+rYaBhCAbUyEI4QzB1ly09Uglc9iCtFaG2Q== + dependencies: + "@eslint/eslintrc" "^1.1.0" + "@humanwhocodes/config-array" "^0.9.2" + ajv "^6.10.0" + chalk "^4.0.0" + cross-spawn "^7.0.2" + debug "^4.3.2" + doctrine "^3.0.0" + escape-string-regexp "^4.0.0" + eslint-scope "^7.1.1" + eslint-utils "^3.0.0" + eslint-visitor-keys "^3.3.0" + espree "^9.3.1" + esquery "^1.4.0" + esutils "^2.0.2" + fast-deep-equal "^3.1.3" + file-entry-cache "^6.0.1" + functional-red-black-tree "^1.0.1" + glob-parent "^6.0.1" + globals "^13.6.0" + ignore "^5.2.0" + import-fresh "^3.0.0" + imurmurhash "^0.1.4" + is-glob "^4.0.0" + js-yaml "^4.1.0" + json-stable-stringify-without-jsonify "^1.0.1" + levn "^0.4.1" + lodash.merge "^4.6.2" + minimatch "^3.0.4" + natural-compare "^1.4.0" + optionator "^0.9.1" + regexpp "^3.2.0" + strip-ansi "^6.0.1" + strip-json-comments "^3.1.0" + text-table "^0.2.0" + v8-compile-cache "^2.0.3" + +espree@^9.3.1: + version "9.3.1" + resolved "https://registry.yarnpkg.com/espree/-/espree-9.3.1.tgz#8793b4bc27ea4c778c19908e0719e7b8f4115bcd" + integrity sha512-bvdyLmJMfwkV3NCRl5ZhJf22zBFo1y8bYh3VYb+bfzqNB4Je68P2sSuXyuFquzWLebHpNd2/d5uv7yoP9ISnGQ== + dependencies: + acorn "^8.7.0" + acorn-jsx "^5.3.1" + eslint-visitor-keys "^3.3.0" + +esquery@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/esquery/-/esquery-1.4.0.tgz#2148ffc38b82e8c7057dfed48425b3e61f0f24a5" + integrity sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w== + dependencies: + estraverse "^5.1.0" + +esrecurse@^4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/esrecurse/-/esrecurse-4.3.0.tgz#7ad7964d679abb28bee72cec63758b1c5d2c9921" + integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== + dependencies: + estraverse "^5.2.0" + +estraverse@^4.1.1: + version "4.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.3.0.tgz#398ad3f3c5a24948be7725e83d11a7de28cdbd1d" + integrity sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw== + +estraverse@^5.1.0, estraverse@^5.2.0: + version "5.3.0" + resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-5.3.0.tgz#2eea5290702f26ab8fe5370370ff86c965d21123" + integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== + +estree-walker@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +external-editor@^3.0.3: + version "3.1.0" + resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-3.1.0.tgz#cb03f740befae03ea4d283caed2741a83f335495" + integrity sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew== + dependencies: + chardet "^0.7.0" + iconv-lite "^0.4.24" + tmp "^0.0.33" + +fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.2.7, fast-glob@^3.2.9: + version "3.2.11" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.2.11.tgz#a1172ad95ceb8a16e20caa5c5e56480e5129c1d9" + integrity sha512-xrO3+1bxSo3ZVHAnqzyuewYT6aMFHRAd4Kcs92MAonjwQZLsK9d0SF1IyQ3k5PoirxTW0Oe/RqFgMQ6TcNE5Ew== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.4" + +fast-json-stable-stringify@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-levenshtein@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz#3d8a5c66883a16a30ca8643e851f19baa7797917" + integrity sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc= + +fast-safe-stringify@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz#c406a83b6e70d9e35ce3b30a81141df30aeba884" + integrity sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA== + +fastq@^1.6.0: + version "1.13.0" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.13.0.tgz#616760f88a7526bdfc596b7cab8c18938c36b98c" + integrity sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw== + dependencies: + reusify "^1.0.4" + +figures@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.2.0.tgz#625c18bd293c604dc4a8ddb2febf0c88341746af" + integrity sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg== + dependencies: + escape-string-regexp "^1.0.5" + +file-entry-cache@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz#211b2dd9659cb0394b073e7323ac3c933d522027" + integrity sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg== + dependencies: + flat-cache "^3.0.4" + +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + +find-up@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/find-up/-/find-up-2.1.0.tgz#45d1b7e506c717ddd482775a2b77920a3c0c57a7" + integrity sha1-RdG35QbHF93UgndaK3eSCjwMV6c= + dependencies: + locate-path "^2.0.0" + +flat-cache@^3.0.4: + version "3.0.4" + resolved "https://registry.yarnpkg.com/flat-cache/-/flat-cache-3.0.4.tgz#61b0338302b2fe9f957dcc32fc2a87f1c3048b11" + integrity sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg== + dependencies: + flatted "^3.1.0" + rimraf "^3.0.2" + +flatted@^3.1.0: + version "3.2.5" + resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.5.tgz#76c8584f4fc843db64702a6bd04ab7a8bd666da3" + integrity sha512-WIWGi2L3DyTUvUrwRKgGi9TwxQMUEqPOPQBVi71R96jZXJdFskXEmf54BoZaS1kknGODoIGASGEzBUYdyMCBJg== + +follow-redirects@^1.14.4: + version "1.14.9" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.14.9.tgz#dd4ea157de7bfaf9ea9b3fbd85aa16951f78d8d7" + integrity sha512-MQDfihBQYMcyy5dhRDJUHcw7lb2Pv/TuE6xP1vyraLukNDHKbDxDNaOE3NbCAdKQApno+GPRyo1YAp89yCjK4w== + +fs-extra@10.0.0: + version "10.0.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.0.0.tgz#9ff61b655dde53fb34a82df84bb214ce802e17c1" + integrity sha512-C5owb14u9eJwizKGdchcDUQeFtlSHHthBk8pbX9Vc1PFZrLombudjDnNns88aYslCyF6IY5SUw3Roz6xShcEIQ== + dependencies: + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= + +fsevents@~2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.2.tgz#8a526f78b8fdf4623b709e0b975c52c24c02fd1a" + integrity sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== + +function-bind@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.1.tgz#a56899d3ea3c9bab874bb9773b7c5ede92f4895d" + integrity sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A== + +functional-red-black-tree@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327" + integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc= + +get-caller-file@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" + integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== + +get-intrinsic@^1.0.2, get-intrinsic@^1.1.0, get-intrinsic@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.1.1.tgz#15f59f376f855c446963948f0d24cd3637b4abc6" + integrity sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q== + dependencies: + function-bind "^1.1.1" + has "^1.0.3" + has-symbols "^1.0.1" + +get-symbol-description@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.0.0.tgz#7fdb81c900101fbd564dd5f1a30af5aadc1e58d6" + integrity sha512-2EmdH1YvIQiZpltCNgkuiUnyukzxM/R6NDJX31Ke3BG1Nq5b0S2PhX59UKi9vZpPDQVdqn+1IcaAwnzTT5vCjw== + dependencies: + call-bind "^1.0.2" + get-intrinsic "^1.1.1" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob-parent@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-6.0.2.tgz#6d237d99083950c79290f24c7642a3de9a28f9e3" + integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== + dependencies: + is-glob "^4.0.3" + +glob-regex@^0.3.0: + version "0.3.2" + resolved "https://registry.yarnpkg.com/glob-regex/-/glob-regex-0.3.2.tgz#27348f2f60648ec32a4a53137090b9fb934f3425" + integrity sha512-m5blUd3/OqDTWwzBBtWBPrGlAzatRywHameHeekAZyZrskYouOGdNB8T/q6JucucvJXtOuyHIn0/Yia7iDasDw== + +glob@7.1.6: + version "7.1.6" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" + integrity sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +glob@^7.1.3, glob@^7.1.7: + version "7.2.0" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" + integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.0.4" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^13.6.0, globals@^13.9.0: + version "13.12.1" + resolved "https://registry.yarnpkg.com/globals/-/globals-13.12.1.tgz#ec206be932e6c77236677127577aa8e50bf1c5cb" + integrity sha512-317dFlgY2pdJZ9rspXDks7073GpDmXdfbM3vYYp0HAMKGDh1FfWPleI2ljVNLQX5M5lXcAslTcPTrOrMEFOjyw== + dependencies: + type-fest "^0.20.2" + +globby@^11.0.4: + version "11.1.0" + resolved "https://registry.yarnpkg.com/globby/-/globby-11.1.0.tgz#bd4be98bb042f83d796f7e3811991fbe82a0d34b" + integrity sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.2.9" + ignore "^5.2.0" + merge2 "^1.4.1" + slash "^3.0.0" + +globrex@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/globrex/-/globrex-0.1.2.tgz#dd5d9ec826232730cd6793a5e33a9302985e6098" + integrity sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg== + +graceful-fs@^4.1.3, graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.9" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.9.tgz#041b05df45755e587a24942279b9d113146e1c96" + integrity sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ== + +has-bigints@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.0.1.tgz#64fe6acb020673e3b78db035a5af69aa9d07b113" + integrity sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA== + +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" + integrity sha1-tdRU3CGZriJWmfNGfloH87lVuv0= + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-symbols@^1.0.1, has-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.0.2.tgz#165d3070c00309752a1236a479331e3ac56f1423" + integrity sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw== + +has-tostringtag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.0.tgz#7e133818a7d394734f941e73c3d3f9291e658b25" + integrity sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ== + dependencies: + has-symbols "^1.0.2" + +has@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/has/-/has-1.0.3.tgz#722d7cbfc1f6aa8241f16dd814e011e1f41e8796" + integrity sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw== + dependencies: + function-bind "^1.1.1" + +iconv-lite@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" + integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== + dependencies: + safer-buffer ">= 2.1.2 < 3" + +ieee754@^1.1.13: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +ignore@^4.0.6: + version "4.0.6" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" + integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== + +ignore@^5.1.1, ignore@^5.1.8, ignore@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a" + integrity sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ== + +immutable@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/immutable/-/immutable-4.0.0.tgz#b86f78de6adef3608395efb269a91462797e2c23" + integrity sha512-zIE9hX70qew5qTUjSS7wi1iwj/l7+m54KWU247nhM3v806UdGj1yDndXj+IOYxxtW9zyLI+xqFNZjTuDaLUqFw== + +import-fresh@^3.0.0, import-fresh@^3.2.1: + version "3.3.0" + resolved "https://registry.yarnpkg.com/import-fresh/-/import-fresh-3.3.0.tgz#37162c25fcb9ebaa2e6e53d5b4d88ce17d9e0c2b" + integrity sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw== + dependencies: + parent-module "^1.0.0" + resolve-from "^4.0.0" + +imurmurhash@^0.1.4: + version "0.1.4" + resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" + integrity sha1-khi5srkoojixPcT7a21XbyMUU+o= + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk= + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +inquirer@8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-8.2.0.tgz#f44f008dd344bbfc4b30031f45d984e034a3ac3a" + integrity sha512-0crLweprevJ02tTuA6ThpoAERAGyVILC4sS74uib58Xf/zSr1/ZWtmm7D5CI+bSQEaA04f0K7idaHpQbSWgiVQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^4.1.1" + cli-cursor "^3.1.0" + cli-width "^3.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.21" + mute-stream "0.0.8" + ora "^5.4.1" + run-async "^2.4.0" + rxjs "^7.2.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + through "^2.3.6" + +internal-slot@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.0.3.tgz#7347e307deeea2faac2ac6205d4bc7d34967f59c" + integrity sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA== + dependencies: + get-intrinsic "^1.1.0" + has "^1.0.3" + side-channel "^1.0.4" + +is-bigint@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.0.4.tgz#08147a1875bc2b32005d41ccd8291dffc6691df3" + integrity sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg== + dependencies: + has-bigints "^1.0.1" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.1.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.1.2.tgz#5c6dc200246dd9321ae4b885a114bb1f75f63719" + integrity sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-callable@^1.1.4, is-callable@^1.2.4: + version "1.2.4" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.4.tgz#47301d58dd0259407865547853df6d61fe471945" + integrity sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w== + +is-core-module@^2.8.0, is-core-module@^2.8.1: + version "2.8.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.8.1.tgz#f59fdfca701d5879d0a6b100a40aa1560ce27211" + integrity sha512-SdNCUs284hr40hFTFP6l0IfZ/RSrMXF3qgoRHd3/79unUTvrFO/JoXwkGm+5J/Oe3E/b5GsnG330uUNgRpu1PA== + dependencies: + has "^1.0.3" + +is-date-object@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.0.5.tgz#0841d5536e724c25597bf6ea62e1bd38298df31f" + integrity sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ== + dependencies: + has-tostringtag "^1.0.0" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha1-qIwCU1eR8C7TfHahueqXc8gz+MI= + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-interactive@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" + integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== + +is-negative-zero@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-negative-zero/-/is-negative-zero-2.0.2.tgz#7bf6f03a28003b8b3965de3ac26f664d765f3150" + integrity sha512-dqJvarLawXsFbNDeJW7zAz8ItJ9cd28YufuuFzh0G8pNHjJMnY08Dv7sYX2uF5UpQOwieAeOExEYAWWfu7ZZUA== + +is-number-object@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.0.6.tgz#6a7aaf838c7f0686a50b4553f7e54a96494e89f0" + integrity sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g== + dependencies: + has-tostringtag "^1.0.0" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-regex@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.1.4.tgz#eef5663cd59fa4c0ae339505323df6854bb15958" + integrity sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg== + dependencies: + call-bind "^1.0.2" + has-tostringtag "^1.0.0" + +is-shared-array-buffer@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.1.tgz#97b0c85fbdacb59c9c446fe653b82cf2b5b7cfe6" + integrity sha512-IU0NmyknYZN0rChcKhRO1X8LYz5Isj/Fsqh8NJOSf+N/hCOTwy29F32Ik7a+QszE63IdvmwdTPDd6cZ5pg4cwA== + +is-string@^1.0.5, is-string@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.0.7.tgz#0dd12bf2006f255bb58f695110eff7491eebc0fd" + integrity sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg== + dependencies: + has-tostringtag "^1.0.0" + +is-symbol@^1.0.2, is-symbol@^1.0.3: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.0.4.tgz#a6dac93b635b063ca6872236de88910a57af139c" + integrity sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg== + dependencies: + has-symbols "^1.0.2" + +is-unicode-supported@^0.1.0: + version "0.1.0" + resolved "https://registry.yarnpkg.com/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz#3f26c76a809593b52bfa2ecb5710ed2779b522a7" + integrity sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw== + +is-weakref@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.0.2.tgz#9529f383a9338205e89765e0392efc2f100f06f2" + integrity sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ== + dependencies: + call-bind "^1.0.2" + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha1-6PvzdNxVb/iUehDcsFctYz8s+hA= + +iterare@1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/iterare/-/iterare-1.2.1.tgz#139c400ff7363690e33abffa33cbba8920f00042" + integrity sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +js-yaml@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.1.0.tgz#c1fb65f8f5017901cdd2c951864ba18458a10602" + integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA== + dependencies: + argparse "^2.0.1" + +json-schema-traverse@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz#69f6a87d9513ab8bb8fe63bdb0979c448e684660" + integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== + +json-stable-stringify-without-jsonify@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz#9db7b59496ad3f3cfef30a75142d2d930ad72651" + integrity sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE= + +json5@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe" + integrity sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow== + dependencies: + minimist "^1.2.0" + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +kleur@^4.1.4: + version "4.1.4" + resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.4.tgz#8c202987d7e577766d039a8cd461934c01cda04d" + integrity sha512-8QADVssbrFjivHWQU7KkMgptGTl6WAcSdlbBPY4uNF+mWr6DGcKrvY2w4FQJoXch7+fKMjj0dRrL75vk3k23OA== + +levn@^0.4.1: + version "0.4.1" + resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade" + integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== + dependencies: + prelude-ls "^1.2.1" + type-check "~0.4.0" + +locate-path@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-2.0.0.tgz#2b568b265eec944c6d9c0de9c3dbbbca0354cd8e" + integrity sha1-K1aLJl7slExtnA3pw9u7ygNUzY4= + dependencies: + p-locate "^2.0.0" + path-exists "^3.0.0" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha1-gteb/zCmfEAF/9XiUVMArZyk168= + +lodash.merge@^4.6.2: + version "4.6.2" + resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a" + integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== + +lodash.pick@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/lodash.pick/-/lodash.pick-4.4.0.tgz#52f05610fff9ded422611441ed1fc123a03001b3" + integrity sha1-UvBWEP/53tQiYRRB7R/BI6AwAbM= + +lodash@4.17.21, lodash@^4.17.21: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +log-symbols@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-4.1.0.tgz#3fbdbb95b4683ac9fc785111e792e558d4abd503" + integrity sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg== + dependencies: + chalk "^4.1.0" + is-unicode-supported "^0.1.0" + +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + +magic-string@^0.25.7: + version "0.25.7" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.7.tgz#3f497d6fd34c669c6798dcb821f2ef31f5445051" + integrity sha512-4CrMT5DOHTDk4HYDlzmwu4FVCcIYI8gauveasrdCu2IKIFOJ3f0v/8MDGJCDL9oD2ppz/Av1b0Nj345H9M+XIA== + dependencies: + sourcemap-codec "^1.4.4" + +merge2@^1.3.0, merge2@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.4.tgz#896d519dfe9db25fce94ceb7a500919bf881ebf9" + integrity sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg== + dependencies: + braces "^3.0.1" + picomatch "^2.2.3" + +mimic-fn@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-2.1.0.tgz#7ed2c2ccccaf84d3ffcb7a69b57711fc2083401b" + integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg== + +min-indent@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/min-indent/-/min-indent-1.0.1.tgz#a63f681673b30571fbe8bc25686ae746eefa9869" + integrity sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== + +minimatch@^3.0.4: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimist@^1.2.0, minimist@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44" + integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q== + +mkdirp@^0.5.1: + version "0.5.5" + resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.5.tgz#d91cefd62d1436ca0f41620e251288d420099def" + integrity sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ== + dependencies: + minimist "^1.2.5" + +moment@^2.29.2: + version "2.29.2" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.2.tgz#00910c60b20843bcba52d37d58c628b47b1f20e4" + integrity sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg== + +mri@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" + integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g= + +ms@2.1.2: + version "2.1.2" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" + integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== + +ms@^2.1.1: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + +nanoid@^3.2.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.1.tgz#6347a18cac88af88f58af0b3594b723d5e99bb35" + integrity sha512-n6Vs/3KGyxPQd6uO0eH4Bv0ojGSUvuLlIHtC3Y0kEO23YRge8H9x1GCzLn28YX0H66pMkxuaeESFq4tKISKwdw== + +natural-compare@^1.4.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/natural-compare/-/natural-compare-1.4.0.tgz#4abebfeed7541f2c27acfb29bdbbd15c8d5ba4f7" + integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= + +node-fetch@^2.6.1: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +npm-run-path@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" + integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== + dependencies: + path-key "^3.0.0" + +object-hash@2.2.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/object-hash/-/object-hash-2.2.0.tgz#5ad518581eefc443bd763472b8ff2e9c2c0d54a5" + integrity sha512-gScRMn0bS5fH+IuwyIFgnh9zBdo4DV+6GhygmWM9HyNJSgS0hScp1f5vjtm7oIIOiT9trXrShAkLFSc2IqKNgw== + +object-inspect@^1.11.0, object-inspect@^1.9.0: + version "1.12.0" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.12.0.tgz#6e2c120e868fd1fd18cb4f18c31741d0d6e776f0" + integrity sha512-Ho2z80bVIvJloH+YzRmpZVQe87+qASmBUKZDWgx9cu+KDrX2ZDH/3tMy+gXbZETVGs2M8YdxObOh7XAtim9Y0g== + +object-keys@^1.0.12, object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.2.tgz#0ed54a342eceb37b38ff76eb831a0e788cb63940" + integrity sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ== + dependencies: + call-bind "^1.0.0" + define-properties "^1.1.3" + has-symbols "^1.0.1" + object-keys "^1.1.1" + +object.values@^1.1.5: + version "1.1.5" + resolved "https://registry.yarnpkg.com/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" + integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + es-abstract "^1.19.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha1-WDsap3WWHUsROsF9nFC6753Xa9E= + dependencies: + wrappy "1" + +onetime@^5.1.0: + version "5.1.2" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + +optionator@^0.9.1: + version "0.9.1" + resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499" + integrity sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw== + dependencies: + deep-is "^0.1.3" + fast-levenshtein "^2.0.6" + levn "^0.4.1" + prelude-ls "^1.2.1" + type-check "^0.4.0" + word-wrap "^1.2.3" + +ora@^5.4.1: + version "5.4.1" + resolved "https://registry.yarnpkg.com/ora/-/ora-5.4.1.tgz#1b2678426af4ac4a509008e5e4ac9e9959db9e18" + integrity sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ== + dependencies: + bl "^4.1.0" + chalk "^4.1.0" + cli-cursor "^3.1.0" + cli-spinners "^2.5.0" + is-interactive "^1.0.0" + is-unicode-supported "^0.1.0" + log-symbols "^4.1.0" + strip-ansi "^6.0.0" + wcwidth "^1.0.1" + +os-tmpdir@~1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" + integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= + +p-limit@^1.1.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.3.0.tgz#b86bd5f0c25690911c7590fcbfc2010d54b3ccb8" + integrity sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q== + dependencies: + p-try "^1.0.0" + +p-locate@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-2.0.0.tgz#20a0103b222a70c8fd39cc2e580680f3dde5ec43" + integrity sha1-IKAQOyIqcMj9OcwuWAaA893l7EM= + dependencies: + p-limit "^1.1.0" + +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + integrity sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M= + +parent-module@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/parent-module/-/parent-module-1.0.1.tgz#691d2709e78c79fae3a156622452d00762caaaa2" + integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== + dependencies: + callsites "^3.0.0" + +path-exists@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/path-exists/-/path-exists-3.0.0.tgz#ce0ebeaa5f78cb18925ea7d810d7b59b010fd515" + integrity sha1-zg6+ql94yxiSXqfYENe1mwEP1RU= + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha1-F0uSaHNVNP+8es5r9TpanhtcX18= + +path-key@^3.0.0, path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-to-regexp@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-3.2.0.tgz#fa7877ecbc495c601907562222453c43cc204a5f" + integrity sha512-jczvQbCUS7XmS7o+y1aEO9OBVFeZBQ1MDSEqmO7xSoPgOPoowY/SxLpZ6Vh97/8qHZOteiCKb7gkG9gA2ZUxJA== + +path-type@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b" + integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw== + +picocolors@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" + integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.2.3: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +postcss@^8.4.6: + version "8.4.6" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.6.tgz#c5ff3c3c457a23864f32cb45ac9b741498a09ae1" + integrity sha512-OovjwIzs9Te46vlEx7+uXB0PLijpwjXGKXjVGGPIGubGpq7uh5Xgf6D6FiJ/SzJMBosHDp6a2hiXOS97iBXcaA== + dependencies: + nanoid "^3.2.0" + picocolors "^1.0.0" + source-map-js "^1.0.2" + +prelude-ls@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" + integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== + +punycode@^2.1.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.1.1.tgz#b58b010ac40c22c5657616c8d2c2c02c7bf479ec" + integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +readable-stream@^3.4.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" + integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +recrawl-sync@^2.0.3: + version "2.2.1" + resolved "https://registry.yarnpkg.com/recrawl-sync/-/recrawl-sync-2.2.1.tgz#cb02c8084c22b3cea103abf46bb88734076ed6bb" + integrity sha512-A2yLDgeXNaduJJMlqyUdIN7fewopnNm/mVeeGytS1d2HLXKpS5EthQ0j8tWeX+as9UXiiwQRwfoslKC+/gjqxg== + dependencies: + "@cush/relative" "^1.0.0" + glob-regex "^0.3.0" + slash "^3.0.0" + tslib "^1.9.3" + +reflect-metadata@0.1.13: + version "0.1.13" + resolved "https://registry.yarnpkg.com/reflect-metadata/-/reflect-metadata-0.1.13.tgz#67ae3ca57c972a2aa1642b10fe363fe32d49dc08" + integrity sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg== + +regenerator-runtime@^0.13.4: + version "0.13.9" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" + integrity sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA== + +regexparam@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/regexparam/-/regexparam-2.0.0.tgz#059476767d5f5f87f735fc7922d133fd1a118c8c" + integrity sha512-gJKwd2MVPWHAIFLsaYDZfyKzHNS4o7E/v8YmNf44vmeV2e4YfVoDToTOKTvE7ab68cRJ++kLuEXJBaEeJVt5ow== + +regexpp@^3.0.0, regexpp@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/regexpp/-/regexpp-3.2.0.tgz#0425a2768d8f23bad70ca4b90461fa2f1213e1b2" + integrity sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg== + +require-directory@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" + integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= + +resolve-from@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" + integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== + +resolve@^1.10.1, resolve@^1.20.0, resolve@^1.22.0: + version "1.22.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.0.tgz#5e0b8c67c15df57a89bdbabe603a002f21731198" + integrity sha512-Hhtrw0nLeSrFQ7phPp4OOcVjLPIeMnRlr5mcnVuMe7M/7eBn98A3hmFRLoFo3DLZkivSYwhRUJTyPyWAk56WLw== + dependencies: + is-core-module "^2.8.1" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + +reusify@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" + integrity sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw== + +rimraf@^2.5.2: + version "2.7.1" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" + integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== + dependencies: + glob "^7.1.3" + +rimraf@^3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" + integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== + dependencies: + glob "^7.1.3" + +rollup@^2.59.0: + version "2.67.3" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.67.3.tgz#3f04391fc296f807d067c9081d173e0a33dbd37e" + integrity sha512-G/x1vUwbGtP6O5ZM8/sWr8+p7YfZhI18pPqMRtMYMWSbHjKZ/ajHGiM+GWNTlWyOR0EHIdT8LHU+Z4ciIZ1oBw== + optionalDependencies: + fsevents "~2.3.2" + +run-async@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/run-async/-/run-async-2.4.1.tgz#8440eccf99ea3e70bd409d49aab88e10c189a455" + integrity sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ== + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +rxjs@7.5.2: + version "7.5.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.2.tgz#11e4a3a1dfad85dbf7fb6e33cbba17668497490b" + integrity sha512-PwDt186XaL3QN5qXj/H9DGyHhP3/RYYgZZwqBv9Tv8rsAaiwFH1IsJJlcgD37J7UW5a6O67qX0KWKS3/pu0m4w== + dependencies: + tslib "^2.1.0" + +rxjs@^6.6.3: + version "6.6.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" + integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== + dependencies: + tslib "^1.9.0" + +rxjs@^7.2.0: + version "7.5.4" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.4.tgz#3d6bd407e6b7ce9a123e76b1e770dc5761aa368d" + integrity sha512-h5M3Hk78r6wAheJF0a5YahB1yRQKCsZ4MsGdZ5O9ETbVtjPcScGfrMmoOq7EBsCRzd4BDkvDJ7ogP8Sz5tTFiQ== + dependencies: + tslib "^2.1.0" + +sade@^1.7.4: + version "1.8.1" + resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" + integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== + dependencies: + mri "^1.1.0" + +safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +"safer-buffer@>= 2.1.2 < 3": + version "2.1.2" + resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" + integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== + +sander@^0.5.0: + version "0.5.1" + resolved "https://registry.yarnpkg.com/sander/-/sander-0.5.1.tgz#741e245e231f07cafb6fdf0f133adfa216a502ad" + integrity sha1-dB4kXiMfB8r7b98PEzrfohalAq0= + dependencies: + es6-promise "^3.1.2" + graceful-fs "^4.1.3" + mkdirp "^0.5.1" + rimraf "^2.5.2" + +sass@^1.49.8: + version "1.49.8" + resolved "https://registry.yarnpkg.com/sass/-/sass-1.49.8.tgz#9bbbc5d43d14862db07f1c04b786c9da9b641828" + integrity sha512-NoGOjvDDOU9og9oAxhRnap71QaTjjlzrvLnKecUJ3GxhaQBrV6e7gPuSPF28u1OcVAArVojPAe4ZhOXwwC4tGw== + dependencies: + chokidar ">=3.0.0 <4.0.0" + immutable "^4.0.0" + source-map-js ">=0.6.2 <2.0.0" + +semver@^6.1.0: + version "6.3.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" + integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== + +semver@^7.3.4, semver@^7.3.5: + version "7.3.5" + resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.5.tgz#0b621c879348d8998e4b0e4be94b3f12e6018ef7" + integrity sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ== + dependencies: + lru-cache "^6.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf" + integrity sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw== + dependencies: + call-bind "^1.0.0" + get-intrinsic "^1.0.2" + object-inspect "^1.9.0" + +signal-exit@^3.0.2: + version "3.0.7" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.7.tgz#a9a1767f8af84155114eaabd73f99273c8f59ad9" + integrity sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ== + +slash@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/slash/-/slash-3.0.0.tgz#6539be870c165adbd5240220dbe361f1bc4d4634" + integrity sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q== + +solid-js@^1.1.6: + version "1.3.9" + resolved "https://registry.yarnpkg.com/solid-js/-/solid-js-1.3.9.tgz#a4247ce6a72c82b5abcbeaaf8fe2273784daa396" + integrity sha512-BZyDen2oj3XA8g8xe0hhVIzGP2d+TV2dn3w90lXjNYLxveAXeN4aA5pxdO4vn7FKd0e0p4nqWtbWtG7NyaPs2A== + +sorcery@^0.10.0: + version "0.10.0" + resolved "https://registry.yarnpkg.com/sorcery/-/sorcery-0.10.0.tgz#8ae90ad7d7cb05fc59f1ab0c637845d5c15a52b7" + integrity sha1-iukK19fLBfxZ8asMY3hF1cFaUrc= + dependencies: + buffer-crc32 "^0.2.5" + minimist "^1.2.0" + sander "^0.5.0" + sourcemap-codec "^1.3.0" + +"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" + integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== + +source-map@^0.7.3: + version "0.7.3" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383" + integrity sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== + +sourcemap-codec@^1.3.0, sourcemap-codec@^1.4.4: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +spawn-command@^0.0.2-1: + version "0.0.2-1" + resolved "https://registry.yarnpkg.com/spawn-command/-/spawn-command-0.0.2-1.tgz#62f5e9466981c1b796dc5929937e11c9c6921bd0" + integrity sha1-YvXpRmmBwbeW3Fkpk34RycaSG9A= + +string-width@^4.1.0, string-width@^4.2.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string.prototype.trimend@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz#e75ae90c2942c63504686c18b287b4a0b1a45f80" + integrity sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string.prototype.trimstart@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz#b36399af4ab2999b4c9c648bd7a3fb2bb26feeed" + integrity sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw== + dependencies: + call-bind "^1.0.2" + define-properties "^1.1.3" + +string_decoder@^1.1.1: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-bom@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-bom/-/strip-bom-3.0.0.tgz#2334c18e9c759f7bdd56fdef7e9ae3d588e68ed3" + integrity sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM= + +strip-indent@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" + integrity sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== + dependencies: + min-indent "^1.0.0" + +strip-json-comments@^3.1.0, strip-json-comments@^3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz#31f1281b3832630434831c310c01cccda8cbe006" + integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== + +supports-color@^5.3.0: + version "5.5.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.5.0.tgz#e2e69a44ac8772f78a1ec0b35b689df6530efc8f" + integrity sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow== + dependencies: + has-flag "^3.0.0" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-color@^8.1.0: + version "8.1.1" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-8.1.1.tgz#cd6fc17e28500cff56c1b86c0a7fd4a54a73005c" + integrity sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +svelte-check@^2.2.7: + version "2.4.5" + resolved "https://registry.yarnpkg.com/svelte-check/-/svelte-check-2.4.5.tgz#a2001993034d495118980bd95577fb3e7980661a" + integrity sha512-nRft8BbG2wcxyCdHDZ7X43xLcvDzua3xLwq6wzHGcAF3ka3Jyhv2rvgq0+SF9NwHLMefp9C2XkM6etzsxK/cMQ== + dependencies: + chokidar "^3.4.1" + fast-glob "^3.2.7" + import-fresh "^3.2.1" + minimist "^1.2.5" + picocolors "^1.0.0" + sade "^1.7.4" + source-map "^0.7.3" + svelte-preprocess "^4.0.0" + typescript "*" + +svelte-fa@^2.4.0: + version "2.4.0" + resolved "https://registry.yarnpkg.com/svelte-fa/-/svelte-fa-2.4.0.tgz#d94285151a975b92b29efec6ec9e2c11c57780ef" + integrity sha512-0bnbMGbsE1LUnlioDcf27tl2O8kjuXlTXMXzIxC7LoIOWmqn0D+zd539HfLiQbdLuOHGTaynwN9V+4ehhEu1Jw== + +svelte-hmr@^0.14.9: + version "0.14.9" + resolved "https://registry.yarnpkg.com/svelte-hmr/-/svelte-hmr-0.14.9.tgz#35f277efc789e1a6230185717347cddb2f8e9833" + integrity sha512-bKE9+4qb4sAnA+TKHiYurUl970rjA0XmlP9TEP7K/ncyWz3m81kA4HOgmlZK/7irGK7gzZlaPDI3cmf8fp/+tg== + +svelte-preprocess@^4.0.0, svelte-preprocess@^4.9.8: + version "4.10.3" + resolved "https://registry.yarnpkg.com/svelte-preprocess/-/svelte-preprocess-4.10.3.tgz#9aac89a8abc3889fa5740fb34f7dd74f3c578e13" + integrity sha512-ttw17lJfb/dx2ZJT9sesaXT5l7mPQ9Apx1H496Kli3Hkk7orIRGpOw6rCPkRNzr6ueVPqb4vzodS5x7sBFhKHw== + dependencies: + "@types/pug" "^2.0.4" + "@types/sass" "^1.16.0" + detect-indent "^6.0.0" + magic-string "^0.25.7" + sorcery "^0.10.0" + strip-indent "^3.0.0" + +svelte-spa-router@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/svelte-spa-router/-/svelte-spa-router-3.2.0.tgz#fae3311d292451236cb57131262406cf312b15ee" + integrity sha512-igemo5Vs82TGBBw+DjWt6qKameXYzNs6aDXcTxou5XbEvOjiRcAM6MLkdVRCatn6u8r42dE99bt/br7T4qe/AQ== + dependencies: + regexparam "2.0.0" + +svelte@^3.44.0: + version "3.46.4" + resolved "https://registry.yarnpkg.com/svelte/-/svelte-3.46.4.tgz#0c46bc4a3e20a2617a1b7dc43a722f9d6c084a38" + integrity sha512-qKJzw6DpA33CIa+C/rGp4AUdSfii0DOTCzj/2YpSKKayw5WGSS624Et9L1nU1k2OVRS9vaENQXp2CVZNU+xvIg== + +sveltestrap@^5.8.5: + version "5.8.5" + resolved "https://registry.yarnpkg.com/sveltestrap/-/sveltestrap-5.8.5.tgz#3273471ec6784a0fe97bed98cf519a22f5078499" + integrity sha512-2UK2CZlh/QSyP087CS/du+UjIdEqSJkjowxkkiH/YuA0BdRhAOU3ASjMiD8avefvpHwJyUtzA13Z6DDmtznl5A== + dependencies: + "@popperjs/core" "^2.9.2" + +text-table@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/text-table/-/text-table-0.2.0.tgz#7f5ee823ae805207c00af2df4a84ec3fcfa570b4" + integrity sha1-f17oI66AUgfACvLfSoTsP8+lcLQ= + +thenby@^1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/thenby/-/thenby-1.3.4.tgz#81581f6e1bb324c6dedeae9bfc28e59b1a2201cc" + integrity sha512-89Gi5raiWA3QZ4b2ePcEwswC3me9JIg+ToSgtE0JWeCynLnLxNr/f9G+xfo9K+Oj4AFdom8YNJjibIARTJmapQ== + +through@^2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU= + +tiny-invariant@^1.1.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/tiny-invariant/-/tiny-invariant-1.2.0.tgz#a1141f86b672a9148c72e978a19a73b9b94a15a9" + integrity sha512-1Uhn/aqw5C6RI4KejVeTg6mIS7IqxnLJ8Mv2tV5rTc0qWobay7pDUz6Wi392Cnc8ak1H0F2cjoRzb2/AW4+Fvg== + +tmp@^0.0.33: + version "0.0.33" + resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.0.33.tgz#6d34335889768d21b2bcda0aa277ced3b1bfadf9" + integrity sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw== + dependencies: + os-tmpdir "~1.0.2" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha1-gYT9NH2snNwYWZLzpmIuFLnZq2o= + +tree-kill@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/tree-kill/-/tree-kill-1.2.2.tgz#4ca09a9092c88b73a7cdc5e8a01b507b0790a0cc" + integrity sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A== + +tsconfig-paths@^3.12.0, tsconfig-paths@^3.9.0: + version "3.12.0" + resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" + integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg== + dependencies: + "@types/json5" "^0.0.29" + json5 "^1.0.1" + minimist "^1.2.0" + strip-bom "^3.0.0" + +tslib@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.0.3.tgz#8e0741ac45fc0c226e58a17bfc3e64b9bc6ca61c" + integrity sha512-uZtkfKblCEQtZKBF6EBXVZeQNl82yqtDQdv+eck8u7tdPxjLu2/lp5/uPW+um2tpuxINHWy3GhiccY7QgEaVHQ== + +tslib@2.3.1, tslib@^2.1.0, tslib@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.1.tgz#e8a335add5ceae51aa261d32a490158ef042ef01" + integrity sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw== + +tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: + version "1.14.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" + integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== + +tsutils@^3.21.0: + version "3.21.0" + resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" + integrity sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA== + dependencies: + tslib "^1.8.1" + +type-check@^0.4.0, type-check@~0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/type-check/-/type-check-0.4.0.tgz#07b8203bfa7056c0657050e3ccd2c37730bab8f1" + integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== + dependencies: + prelude-ls "^1.2.1" + +type-fest@^0.20.2: + version "0.20.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.20.2.tgz#1bf207f4b28f91583666cb5fbd327887301cd5f4" + integrity sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ== + +type-fest@^0.21.3: + version "0.21.3" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.21.3.tgz#d260a24b0198436e133fa26a524a6d65fa3b2e37" + integrity sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w== + +typescript@*, typescript@^4.5.4: + version "4.5.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.5.5.tgz#d8c953832d28924a9e3d37c73d729c846c5896f3" + integrity sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA== + +unbox-primitive@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.0.1.tgz#085e215625ec3162574dc8859abee78a59b14471" + integrity sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw== + dependencies: + function-bind "^1.1.1" + has-bigints "^1.0.1" + has-symbols "^1.0.2" + which-boxed-primitive "^1.0.2" + +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + +uri-js@^4.2.2: + version "4.4.1" + resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.4.1.tgz#9b1a52595225859e55f669d928f88c6c57f2a77e" + integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== + dependencies: + punycode "^2.1.0" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8= + +uuid@8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + +v8-compile-cache@^2.0.3: + version "2.3.0" + resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" + integrity sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA== + +vite-plugin-checker@^0.4.2: + version "0.4.2" + resolved "https://registry.yarnpkg.com/vite-plugin-checker/-/vite-plugin-checker-0.4.2.tgz#7912cadd8581656d2642a54145311a03779247cc" + integrity sha512-qMre3nYXAv11fZTQt+zQjVvNMweye36vZLnUqCCC7BJXYjHYeBml3zox4N6UiBufKoiF3XX0w/kwTvXHQLvflQ== + dependencies: + "@babel/code-frame" "^7.12.13" + ansi-escapes "^4.3.0" + chalk "^4.1.1" + chokidar "^3.5.1" + commander "^8.0.0" + fast-glob "^3.2.7" + lodash.debounce "^4.0.8" + lodash.pick "^4.4.0" + npm-run-path "^4.0.1" + strip-ansi "^6.0.0" + tiny-invariant "^1.1.0" + vscode-languageclient "^7.0.0" + vscode-languageserver "^7.0.0" + vscode-languageserver-textdocument "^1.0.1" + vscode-uri "^3.0.2" + +vite-tsconfig-paths@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/vite-tsconfig-paths/-/vite-tsconfig-paths-3.4.0.tgz#bcb6033198e530c3fa5ab14bdb1fe1d0d1d8ec0c" + integrity sha512-os+oAdJxkZvNLmisVQ76eDdCWC3aH4bKTy3EXI5oJi//zQ0G+qJfUeFR6Need4iyzL/Xus9R7AECF/YfGS0ZEw== + dependencies: + debug "^4.1.1" + globrex "^0.1.2" + recrawl-sync "^2.0.3" + tsconfig-paths "^3.9.0" + +vite@^2.8.0: + version "2.8.4" + resolved "https://registry.yarnpkg.com/vite/-/vite-2.8.4.tgz#4e52a534289b7b4e94e646df2fc5556ceaa7336b" + integrity sha512-GwtOkkaT2LDI82uWZKcrpRQxP5tymLnC7hVHHqNkhFNknYr0hJUlDLfhVRgngJvAy3RwypkDCWtTKn1BjO96Dw== + dependencies: + esbuild "^0.14.14" + postcss "^8.4.6" + resolve "^1.22.0" + rollup "^2.59.0" + optionalDependencies: + fsevents "~2.3.2" + +vscode-jsonrpc@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-6.0.0.tgz#108bdb09b4400705176b957ceca9e0880e9b6d4e" + integrity sha512-wnJA4BnEjOSyFMvjZdpiOwhSq9uDoK8e/kpRJDTaMYzwlkrhG1fwDIZI94CLsLzlCK5cIbMMtFlJlfR57Lavmg== + +vscode-languageclient@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-7.0.0.tgz#b505c22c21ffcf96e167799757fca07a6bad0fb2" + integrity sha512-P9AXdAPlsCgslpP9pRxYPqkNYV7Xq8300/aZDpO35j1fJm/ncize8iGswzYlcvFw5DQUx4eVk+KvfXdL0rehNg== + dependencies: + minimatch "^3.0.4" + semver "^7.3.4" + vscode-languageserver-protocol "3.16.0" + +vscode-languageserver-protocol@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.16.0.tgz#34135b61a9091db972188a07d337406a3cdbe821" + integrity sha512-sdeUoAawceQdgIfTI+sdcwkiK2KU+2cbEYA0agzM2uqaUy2UpnnGHtWTHVEtS0ES4zHU0eMFRGN+oQgDxlD66A== + dependencies: + vscode-jsonrpc "6.0.0" + vscode-languageserver-types "3.16.0" + +vscode-languageserver-textdocument@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.4.tgz#3cd56dd14cec1d09e86c4bb04b09a246cb3df157" + integrity sha512-/xhqXP/2A2RSs+J8JNXpiiNVvvNM0oTosNVmQnunlKvq9o4mupHOBAnnzH0lwIPKazXKvAKsVp1kr+H/K4lgoQ== + +vscode-languageserver-types@3.16.0: + version "3.16.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.16.0.tgz#ecf393fc121ec6974b2da3efb3155644c514e247" + integrity sha512-k8luDIWJWyenLc5ToFQQMaSrqCHiLwyKPHKPQZ5zz21vM+vIVUSvsRpcbiECH4WR88K2XZqc4ScRcZ7nk/jbeA== + +vscode-languageserver@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/vscode-languageserver/-/vscode-languageserver-7.0.0.tgz#49b068c87cfcca93a356969d20f5d9bdd501c6b0" + integrity sha512-60HTx5ID+fLRcgdHfmz0LDZAXYEV68fzwG0JWwEPBode9NuMYTIxuYXPg4ngO8i8+Ou0lM7y6GzaYWbiDL0drw== + dependencies: + vscode-languageserver-protocol "3.16.0" + +vscode-uri@^3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.3.tgz#a95c1ce2e6f41b7549f86279d19f47951e4f4d84" + integrity sha512-EcswR2S8bpR7fD0YPeS7r2xXExrScVMxg4MedACaWHEtx9ftCF/qHG1xGkolzTPcEmjTavCQgbVzHUIdTMzFGA== + +wcwidth@>=1.0.1, wcwidth@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/wcwidth/-/wcwidth-1.0.1.tgz#f0b0dcf915bc5ff1528afadb2c0e17b532da2fe8" + integrity sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g= + dependencies: + defaults "^1.0.3" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha1-JFNCdeKnvGvnvIZhHMFq4KVlSHE= + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha1-lmRU6HZUYuN2RNNib2dCzotwll0= + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +which-boxed-primitive@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz#13757bc89b209b049fe5d86430e21cf40a89a8e6" + integrity sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg== + dependencies: + is-bigint "^1.0.1" + is-boolean-object "^1.1.0" + is-number-object "^1.0.4" + is-string "^1.0.5" + is-symbol "^1.0.3" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +word-wrap@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.3.tgz#610636f6b1f703891bd34771ccb17fb93b47079c" + integrity sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ== + +wrap-ansi@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= + +y18n@^5.0.5: + version "5.0.8" + resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55" + integrity sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== + +yallist@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-4.0.0.tgz#9bb92790d9c0effec63be73519e11a35019a3a72" + integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== + +yargs-parser@^20.2.2: + version "20.2.9" + resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" + integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== + +yargs@^16.2.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" diff --git a/warpgate-admin/src/api/auth.rs b/warpgate-admin/src/api/auth.rs new file mode 100644 index 0000000..b7b93d5 --- /dev/null +++ b/warpgate-admin/src/api/auth.rs @@ -0,0 +1,74 @@ +use crate::helpers::ApiResult; +use poem::session::Session; +use poem::web::Data; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, Object, OpenApi}; +use std::sync::Arc; +use tokio::sync::Mutex; +use warpgate_common::{AuthCredential, AuthResult, ConfigProvider, Secret}; + +pub struct Api; + +#[derive(Object)] +struct LoginRequest { + username: String, + password: String, +} + +#[derive(ApiResponse)] +enum LoginResponse { + #[oai(status = 201)] + Success, + + #[oai(status = 401)] + Failure, +} + +#[derive(ApiResponse)] +enum LogoutResponse { + #[oai(status = 201)] + Success, +} + +#[OpenApi] +impl Api { + #[oai(path = "/auth/login", method = "post", operation_id = "login")] + async fn api_auth_login( + &self, + session: &Session, + config_provider: Data<&Arc>>, + body: Json, + ) -> ApiResult { + let mut config_provider = config_provider.lock().await; + let result = config_provider + .authorize( + &body.username, + &[AuthCredential::Password(Secret::new(body.password.clone()))], + ) + .await + .map_err(|e| e.context("Failed to authorize user"))?; + match result { + AuthResult::Accepted { username } => { + let targets = config_provider.list_targets().await?; + for target in targets { + if target.web_admin.is_some() + && config_provider + .authorize_target(&username, &target.name) + .await? + { + session.set("username", username); + return Ok(LoginResponse::Success); + } + } + Ok(LoginResponse::Failure) + } + AuthResult::Rejected => Ok(LoginResponse::Failure), + } + } + + #[oai(path = "/auth/logout", method = "post", operation_id = "logout")] + async fn api_auth_logout(&self, session: &Session) -> ApiResult { + session.clear(); + Ok(LogoutResponse::Success) + } +} diff --git a/warpgate-admin/src/api/info.rs b/warpgate-admin/src/api/info.rs new file mode 100644 index 0000000..226cc86 --- /dev/null +++ b/warpgate-admin/src/api/info.rs @@ -0,0 +1,30 @@ +use crate::helpers::ApiResult; +use poem::session::Session; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, Object, OpenApi}; +use serde::Serialize; + +pub struct Api; + +#[derive(Serialize, Object)] +pub struct Info { + version: String, + username: Option, +} + +#[derive(ApiResponse)] +enum InstanceInfoResponse { + #[oai(status = 200)] + Ok(Json), +} + +#[OpenApi] +impl Api { + #[oai(path = "/info", method = "get", operation_id = "get_info")] + async fn api_get_info(&self, session: &Session) -> ApiResult { + Ok(InstanceInfoResponse::Ok(Json(Info { + version: env!("CARGO_PKG_VERSION").to_string(), + username: session.get::("username"), + }))) + } +} diff --git a/warpgate-admin/src/api/known_hosts_detail.rs b/warpgate-admin/src/api/known_hosts_detail.rs new file mode 100644 index 0000000..d69f532 --- /dev/null +++ b/warpgate-admin/src/api/known_hosts_detail.rs @@ -0,0 +1,56 @@ +use crate::helpers::{authorized, ApiResult}; +use poem::session::Session; +use poem::web::Data; +use poem_openapi::param::Path; +use poem_openapi::{ApiResponse, OpenApi}; +use sea_orm::{DatabaseConnection, EntityTrait, ModelTrait}; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; +pub struct Api; + +#[derive(ApiResponse)] +enum DeleteSSHKnownHostResponse { + #[oai(status = 204)] + Deleted, + + #[oai(status = 404)] + NotFound, +} + +#[OpenApi] +impl Api { + #[oai( + path = "/ssh/known-hosts/:id", + method = "delete", + operation_id = "delete_ssh_known_host" + )] + async fn api_ssh_delete_known_host( + &self, + db: Data<&Arc>>, + id: Path, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + use warpgate_db_entities::KnownHost; + let db = db.lock().await; + + let known_host = KnownHost::Entity::find_by_id(id.0) + .one(&*db) + .await + .map_err(poem::error::InternalServerError)?; + + match known_host { + Some(known_host) => { + known_host + .delete(&*db) + .await + .map_err(poem::error::InternalServerError)?; + Ok(DeleteSSHKnownHostResponse::Deleted) + } + None => Ok(DeleteSSHKnownHostResponse::NotFound), + } + }) + .await + } +} diff --git a/warpgate-admin/src/api/known_hosts_list.rs b/warpgate-admin/src/api/known_hosts_list.rs new file mode 100644 index 0000000..be41e9a --- /dev/null +++ b/warpgate-admin/src/api/known_hosts_list.rs @@ -0,0 +1,43 @@ +use crate::helpers::{authorized, ApiResult}; +use poem::session::Session; +use poem::web::Data; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, OpenApi}; +use sea_orm::{DatabaseConnection, EntityTrait}; +use std::sync::Arc; +use tokio::sync::Mutex; +use warpgate_db_entities::KnownHost; + +pub struct Api; + +#[derive(ApiResponse)] +enum GetSSHKnownHostsResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[OpenApi] +impl Api { + #[oai( + path = "/ssh/known-hosts", + method = "get", + operation_id = "get_ssh_known_hosts" + )] + async fn api_ssh_get_all_known_hosts( + &self, + db: Data<&Arc>>, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + use warpgate_db_entities::KnownHost; + + let db = db.lock().await; + let hosts = KnownHost::Entity::find() + .all(&*db) + .await + .map_err(poem::error::InternalServerError)?; + Ok(GetSSHKnownHostsResponse::Ok(Json(hosts))) + }) + .await + } +} diff --git a/warpgate-admin/src/api/mod.rs b/warpgate-admin/src/api/mod.rs new file mode 100644 index 0000000..d779e87 --- /dev/null +++ b/warpgate-admin/src/api/mod.rs @@ -0,0 +1,12 @@ +pub mod auth; +pub mod info; +pub mod known_hosts_detail; +pub mod known_hosts_list; +pub mod recordings_detail; +pub mod sessions_detail; +pub mod sessions_list; +pub mod ssh_keys; +pub mod targets_list; +pub mod tickets_detail; +pub mod tickets_list; +pub mod users_list; diff --git a/warpgate-admin/src/api/recordings_detail.rs b/warpgate-admin/src/api/recordings_detail.rs new file mode 100644 index 0000000..ad193e2 --- /dev/null +++ b/warpgate-admin/src/api/recordings_detail.rs @@ -0,0 +1,191 @@ +use crate::helpers::{authorized, ApiResult}; +use bytes::Bytes; +use poem::error::{InternalServerError, NotFoundError}; +use poem::handler; +use poem::session::Session; +use poem::web::Data; +use poem_openapi::param::Path; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, OpenApi}; +use sea_orm::{DatabaseConnection, EntityTrait}; +use serde::Serialize; +use std::sync::Arc; +use tokio::fs::File; +use tokio::io::{AsyncBufReadExt, BufReader}; +use tokio::sync::Mutex; +use uuid::Uuid; +use warpgate_common::recordings::{SessionRecordings, TerminalRecordingItem}; +use warpgate_db_entities::Recording::{self, RecordingKind}; + +pub struct Api; + +#[derive(ApiResponse)] +enum GetRecordingResponse { + #[oai(status = 200)] + Ok(Json), + #[oai(status = 404)] + NotFound, +} + +#[OpenApi] +impl Api { + #[oai( + path = "/recordings/:id", + method = "get", + operation_id = "get_recording" + )] + async fn api_get_recording( + &self, + db: Data<&Arc>>, + id: Path, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + let db = db.lock().await; + + let recording = Recording::Entity::find_by_id(id.0) + .one(&*db) + .await + .map_err(InternalServerError)?; + + match recording { + Some(recording) => Ok(GetRecordingResponse::Ok(Json(recording))), + None => Ok(GetRecordingResponse::NotFound), + } + }) + .await + } +} + +#[handler] +pub async fn api_get_recording_cast( + db: Data<&Arc>>, + recordings: Data<&Arc>>, + id: poem::web::Path, + session: &Session, +) -> ApiResult { + authorized(session, || async move { + let db = db.lock().await; + + let recording = Recording::Entity::find_by_id(id.0) + .one(&*db) + .await + .map_err(InternalServerError)?; + + let Some(recording) = recording else { + return Err(NotFoundError.into()) + }; + + if recording.kind != RecordingKind::Terminal { + return Err(NotFoundError.into()); + } + + let path = { + recordings + .lock() + .await + .path_for(&recording.session_id, &recording.name) + }; + + let mut response = vec![]; //String::new(); + + let mut last_size = (0, 0); + let file = File::open(&path).await.map_err(InternalServerError)?; + let reader = BufReader::new(file); + let mut lines = reader.lines(); + while let Some(line) = lines.next_line().await.map_err(InternalServerError)? { + let entry: TerminalRecordingItem = + serde_json::from_str(&line[..]).map_err(InternalServerError)?; + match entry { + TerminalRecordingItem::Data { time, data } => { + response.push( + serde_json::to_string(&Cast::Output( + time, + "o".to_string(), + String::from_utf8_lossy(&data[..]).to_string(), + )) + .map_err(InternalServerError)?, + ); + } + TerminalRecordingItem::PtyResize { cols, rows, .. } => { + last_size = (cols, rows); + } + } + } + + response.insert( + 0, + serde_json::to_string(&Cast::Header { + version: 2, + width: last_size.0, + height: last_size.1, + title: recording.name, + }) + .map_err(InternalServerError)?, + ); + + Ok(response.join("\n")) + }) + .await +} + +#[handler] +pub async fn api_get_recording_tcpdump( + db: Data<&Arc>>, + recordings: Data<&Arc>>, + id: poem::web::Path, + session: &Session, +) -> ApiResult { + authorized(session, || async move { + let db = db.lock().await; + + let recording = Recording::Entity::find_by_id(id.0) + .one(&*db) + .await + .map_err(poem::error::InternalServerError)?; + + let Some(recording) = recording else { + return Err(NotFoundError.into()) + }; + + if recording.kind != RecordingKind::Traffic { + return Err(NotFoundError.into()); + } + + let path = { + recordings + .lock() + .await + .path_for(&recording.session_id, &recording.name) + }; + + let content = std::fs::read(path).map_err(InternalServerError)?; + + Ok(Bytes::from(content)) + }) + .await +} + +#[derive(Serialize)] +#[serde(untagged)] +enum Cast { + Header { + version: u32, + width: u32, + height: u32, + title: String, + }, + Output(f32, String, String), +} + +// #[handler] +// pub async fn api_get_recording_stream( +// ws: WebSocket, +// db: Data<&Arc>>, +// state: Data<&Arc>>, +// id: poem::web::Path, +// ) -> impl IntoResponse { +// ws.on_upgrade(|socket| async move { + +// }) +// } diff --git a/warpgate-admin/src/api/sessions_detail.rs b/warpgate-admin/src/api/sessions_detail.rs new file mode 100644 index 0000000..45a5df0 --- /dev/null +++ b/warpgate-admin/src/api/sessions_detail.rs @@ -0,0 +1,111 @@ +use crate::helpers::{authorized, ApiResult}; +use poem::web::Data; +use poem_openapi::param::Path; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, OpenApi}; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QueryOrder}; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; +use warpgate_common::{SessionSnapshot, State}; +use warpgate_db_entities::{Recording, Session}; + +pub struct Api; + +#[allow(clippy::large_enum_variant)] +#[derive(ApiResponse)] +enum GetSessionResponse { + #[oai(status = 200)] + Ok(Json), + #[oai(status = 404)] + NotFound, +} + +#[derive(ApiResponse)] +enum GetSessionRecordingsResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[derive(ApiResponse)] +enum CloseSessionResponse { + #[oai(status = 201)] + Ok, + #[oai(status = 404)] + NotFound, +} + +#[OpenApi] +impl Api { + #[oai(path = "/sessions/:id", method = "get", operation_id = "get_session")] + async fn api_get_session( + &self, + db: Data<&Arc>>, + id: Path, + session: &poem::session::Session, + ) -> ApiResult { + authorized(session, || async move { + let db = db.lock().await; + + let session = Session::Entity::find_by_id(id.0) + .one(&*db) + .await + .map_err(poem::error::InternalServerError)?; + + match session { + Some(session) => Ok(GetSessionResponse::Ok(Json(session.into()))), + None => Ok(GetSessionResponse::NotFound), + } + }) + .await + } + + #[oai( + path = "/sessions/:id/recordings", + method = "get", + operation_id = "get_session_recordings" + )] + async fn api_get_session_recordings( + &self, + db: Data<&Arc>>, + id: Path, + session: &poem::session::Session, + ) -> ApiResult { + authorized(session, || async move { + let db = db.lock().await; + let recordings: Vec = Recording::Entity::find() + .order_by_desc(Recording::Column::Started) + .filter(Recording::Column::SessionId.eq(id.0)) + .all(&*db) + .await + .map_err(poem::error::InternalServerError)?; + Ok(GetSessionRecordingsResponse::Ok(Json(recordings))) + }) + .await + } + + #[oai( + path = "/sessions/:id/close", + method = "post", + operation_id = "close_session" + )] + async fn api_close_session( + &self, + state: Data<&Arc>>, + id: Path, + session: &poem::session::Session, + ) -> ApiResult { + authorized(session, || async move { + let state = state.lock().await; + + if let Some(s) = state.sessions.get(&id) { + let mut session = s.lock().await; + session.handle.close(); + Ok(CloseSessionResponse::Ok) + } else { + Ok(CloseSessionResponse::NotFound) + } + }) + .await + } +} diff --git a/warpgate-admin/src/api/sessions_list.rs b/warpgate-admin/src/api/sessions_list.rs new file mode 100644 index 0000000..44a4461 --- /dev/null +++ b/warpgate-admin/src/api/sessions_list.rs @@ -0,0 +1,73 @@ +use crate::helpers::{authorized, ApiResult}; +use poem::session::Session; +use poem::web::Data; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, OpenApi}; +use sea_orm::{DatabaseConnection, EntityTrait, QueryOrder}; +use std::sync::Arc; +use tokio::sync::Mutex; +use warpgate_common::{SessionSnapshot, State}; + +pub struct Api; + +#[derive(ApiResponse)] +enum GetSessionsResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[derive(ApiResponse)] +enum CloseAllSessionsResponse { + #[oai(status = 201)] + Ok, +} + +#[OpenApi] +impl Api { + #[oai(path = "/sessions", method = "get", operation_id = "get_sessions")] + async fn api_get_all_sessions( + &self, + db: Data<&Arc>>, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + use warpgate_db_entities::Session; + + let db = db.lock().await; + let sessions = Session::Entity::find() + .order_by_desc(Session::Column::Started) + .all(&*db) + .await + .map_err(poem::error::InternalServerError)?; + let sessions = sessions + .into_iter() + .map(Into::into) + .collect::>(); + Ok(GetSessionsResponse::Ok(Json(sessions))) + }) + .await + } + + #[oai( + path = "/sessions", + method = "delete", + operation_id = "close_all_sessions" + )] + async fn api_close_all_sessions( + &self, + state: Data<&Arc>>, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + let state = state.lock().await; + + for s in state.sessions.values() { + let mut session = s.lock().await; + session.handle.close(); + } + + Ok(CloseAllSessionsResponse::Ok) + }) + .await + } +} diff --git a/warpgate-admin/src/api/ssh_keys.rs b/warpgate-admin/src/api/ssh_keys.rs new file mode 100644 index 0000000..f06836a --- /dev/null +++ b/warpgate-admin/src/api/ssh_keys.rs @@ -0,0 +1,54 @@ +use crate::helpers::{authorized, ApiResult}; +use poem::session::Session; +use poem::web::Data; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, Object, OpenApi}; +use russh_keys::PublicKeyBase64; +use serde::Serialize; +use std::sync::Arc; +use tokio::sync::Mutex; +use warpgate_common::WarpgateConfig; + +pub struct Api; + +#[derive(Serialize, Object)] +struct SSHKey { + pub kind: String, + pub public_key_base64: String, +} + +#[derive(ApiResponse)] +enum GetSSHOwnKeysResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[OpenApi] +impl Api { + #[oai( + path = "/ssh/own-keys", + method = "get", + operation_id = "get_ssh_own_keys" + )] + async fn api_ssh_get_own_keys( + &self, + config: Data<&Arc>>, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + let config = config.lock().await; + let keys = warpgate_protocol_ssh::load_client_keys(&config) + .map_err(poem::error::InternalServerError)?; + + let keys = keys + .into_iter() + .map(|k| SSHKey { + kind: k.name().to_owned(), + public_key_base64: k.public_key_base64(), + }) + .collect(); + Ok(GetSSHOwnKeysResponse::Ok(Json(keys))) + }) + .await + } +} diff --git a/warpgate-admin/src/api/targets_list.rs b/warpgate-admin/src/api/targets_list.rs new file mode 100644 index 0000000..5339929 --- /dev/null +++ b/warpgate-admin/src/api/targets_list.rs @@ -0,0 +1,33 @@ +use crate::helpers::{authorized, ApiResult}; +use poem::session::Session; +use poem::web::Data; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, OpenApi}; +use std::sync::Arc; +use tokio::sync::Mutex; +use warpgate_common::{ConfigProvider, Target}; + +pub struct Api; + +#[derive(ApiResponse)] +enum GetTargetsResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[OpenApi] +impl Api { + #[oai(path = "/targets", method = "get", operation_id = "get_targets")] + async fn api_get_all_targets( + &self, + config_provider: Data<&Arc>>, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + let mut targets = config_provider.lock().await.list_targets().await?; + targets.sort_by(|a, b| a.name.cmp(&b.name)); + Ok(GetTargetsResponse::Ok(Json(targets))) + }) + .await + } +} diff --git a/warpgate-admin/src/api/tickets_detail.rs b/warpgate-admin/src/api/tickets_detail.rs new file mode 100644 index 0000000..e982425 --- /dev/null +++ b/warpgate-admin/src/api/tickets_detail.rs @@ -0,0 +1,57 @@ +use crate::helpers::{authorized, ApiResult}; +use poem::session::Session; +use poem::web::Data; +use poem_openapi::param::Path; +use poem_openapi::{ApiResponse, OpenApi}; +use sea_orm::{DatabaseConnection, EntityTrait, ModelTrait}; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; + +pub struct Api; + +#[derive(ApiResponse)] +enum DeleteTicketResponse { + #[oai(status = 204)] + Deleted, + + #[oai(status = 404)] + NotFound, +} + +#[OpenApi] +impl Api { + #[oai( + path = "/tickets/:id", + method = "delete", + operation_id = "delete_ticket" + )] + async fn api_delete_ticket( + &self, + db: Data<&Arc>>, + id: Path, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + use warpgate_db_entities::Ticket; + let db = db.lock().await; + + let ticket = Ticket::Entity::find_by_id(id.0) + .one(&*db) + .await + .map_err(poem::error::InternalServerError)?; + + match ticket { + Some(ticket) => { + ticket + .delete(&*db) + .await + .map_err(poem::error::InternalServerError)?; + Ok(DeleteTicketResponse::Deleted) + } + None => Ok(DeleteTicketResponse::NotFound), + } + }) + .await + } +} diff --git a/warpgate-admin/src/api/tickets_list.rs b/warpgate-admin/src/api/tickets_list.rs new file mode 100644 index 0000000..fbffd18 --- /dev/null +++ b/warpgate-admin/src/api/tickets_list.rs @@ -0,0 +1,106 @@ +use crate::helpers::{authorized, ApiResult}; +use anyhow::Context; +use poem::session::Session; +use poem::web::Data; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, Object, OpenApi}; +use sea_orm::ActiveValue::Set; +use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait}; +use std::sync::Arc; +use tokio::sync::Mutex; +use uuid::Uuid; +use warpgate_common::hash::generate_ticket_secret; +use warpgate_db_entities::Ticket; + +pub struct Api; + +#[derive(ApiResponse)] +enum GetTicketsResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[derive(Object)] +struct CreateTicketRequest { + username: String, + target_name: String, +} + +#[derive(Object)] +struct TicketAndSecret { + ticket: Ticket::Model, + secret: String, +} + +#[derive(ApiResponse)] +enum CreateTicketResponse { + #[oai(status = 201)] + Created(Json), + + #[oai(status = 400)] + BadRequest(Json), +} + +#[OpenApi] +impl Api { + #[oai(path = "/tickets", method = "get", operation_id = "get_tickets")] + async fn api_get_all_tickets( + &self, + db: Data<&Arc>>, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + use warpgate_db_entities::Ticket; + + let db = db.lock().await; + let tickets = Ticket::Entity::find() + .all(&*db) + .await + .map_err(poem::error::InternalServerError)?; + let tickets = tickets + .into_iter() + .map(Into::into) + .collect::>(); + Ok(GetTicketsResponse::Ok(Json(tickets))) + }) + .await + } + + #[oai(path = "/tickets", method = "post", operation_id = "create_ticket")] + async fn api_create_ticket( + &self, + db: Data<&Arc>>, + body: Json, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + use warpgate_db_entities::Ticket; + + if body.username.is_empty() { + return Ok(CreateTicketResponse::BadRequest(Json("username".into()))); + } + if body.target_name.is_empty() { + return Ok(CreateTicketResponse::BadRequest(Json("target_name".into()))); + } + + let db = db.lock().await; + let secret = generate_ticket_secret(); + let values = Ticket::ActiveModel { + id: Set(Uuid::new_v4()), + secret: Set(secret.expose_secret().to_string()), + username: Set(body.username.clone()), + target: Set(body.target_name.clone()), + created: Set(chrono::Utc::now()), + ..Default::default() + }; + + let ticket = values.insert(&*db).await.context("Error saving ticket")?; + + Ok(CreateTicketResponse::Created(Json(TicketAndSecret { + secret: secret.expose_secret().to_string(), + ticket, + }))) + }) + .await + } +} diff --git a/warpgate-admin/src/api/users_list.rs b/warpgate-admin/src/api/users_list.rs new file mode 100644 index 0000000..d9ccae0 --- /dev/null +++ b/warpgate-admin/src/api/users_list.rs @@ -0,0 +1,33 @@ +use crate::helpers::{authorized, ApiResult}; +use poem::session::Session; +use poem::web::Data; +use poem_openapi::payload::Json; +use poem_openapi::{ApiResponse, OpenApi}; +use std::sync::Arc; +use tokio::sync::Mutex; +use warpgate_common::{ConfigProvider, UserSnapshot}; + +pub struct Api; + +#[derive(ApiResponse)] +enum GetUsersResponse { + #[oai(status = 200)] + Ok(Json>), +} + +#[OpenApi] +impl Api { + #[oai(path = "/users", method = "get", operation_id = "get_users")] + async fn api_get_all_users( + &self, + config_provider: Data<&Arc>>, + session: &Session, + ) -> ApiResult { + authorized(session, || async move { + let mut users = config_provider.lock().await.list_users().await?; + users.sort_by(|a, b| a.username.cmp(&b.username)); + Ok(GetUsersResponse::Ok(Json(users))) + }) + .await + } +} diff --git a/warpgate-admin/src/embed.rs b/warpgate-admin/src/embed.rs new file mode 100644 index 0000000..4b3d4ae --- /dev/null +++ b/warpgate-admin/src/embed.rs @@ -0,0 +1,96 @@ +//! Usage: +//! +//! ``` +//! #[derive(RustEmbed)] +//! #[folder = "app/dist"] +//! pub struct Assets; +//! +//! Route::new() +//! .at("/", EmbeddedFileEndpoint::::new("index.html")) +//! .nest_no_strip("/assets", EmbeddedFilesEndpoint::::new()) +//! ``` + +use async_trait::async_trait; +use poem::http::{header, Method, StatusCode}; +use poem::{Endpoint, Request, Response}; +use rust_embed::RustEmbed; +use std::marker::PhantomData; + +pub struct EmbeddedFileEndpoint { + _embed: PhantomData, + path: String, +} + +impl EmbeddedFileEndpoint { + pub fn new(path: &str) -> Self { + EmbeddedFileEndpoint { + _embed: PhantomData, + path: path.to_owned(), + } + } +} + +#[async_trait] +impl Endpoint for EmbeddedFileEndpoint { + type Output = Response; + + async fn call(&self, req: Request) -> Result { + if req.method() != Method::GET { + return Err(StatusCode::METHOD_NOT_ALLOWED.into()); + } + + match E::get(&self.path) { + Some(content) => { + let hash = hex::encode(content.metadata.sha256_hash()); + if req + .headers() + .get(header::IF_NONE_MATCH) + .map(|etag| etag.to_str().unwrap_or("000000").eq(&hash)) + .unwrap_or(false) + { + return Err(StatusCode::NOT_MODIFIED.into()); + } + + // otherwise, return 200 with etag hash + let body: Vec = content.data.into(); + let mime = mime_guess::from_path(&self.path).first_or_octet_stream(); + Ok(Response::builder() + .header(header::CONTENT_TYPE, mime.as_ref()) + .header(header::ETAG, hash) + .body(body)) + } + None => Err(StatusCode::NOT_FOUND.into()), + } + } +} + +pub struct EmbeddedFilesEndpoint { + _embed: PhantomData, +} + +impl EmbeddedFilesEndpoint { + pub fn new() -> Self { + EmbeddedFilesEndpoint { + _embed: PhantomData, + } + } +} + +#[async_trait] +impl Endpoint for EmbeddedFilesEndpoint { + type Output = Response; + + async fn call(&self, req: Request) -> Result { + let mut path = req + .uri() + .path() + .trim_start_matches('/') + .trim_end_matches('/') + .to_string(); + if path.is_empty() { + path = "index.html".to_string(); + } + let path = path.as_ref(); + EmbeddedFileEndpoint::::new(path).call(req).await + } +} diff --git a/warpgate-admin/src/helpers.rs b/warpgate-admin/src/helpers.rs new file mode 100644 index 0000000..4e5ee74 --- /dev/null +++ b/warpgate-admin/src/helpers.rs @@ -0,0 +1,28 @@ +use poem::http::StatusCode; +use poem::session::Session; + +pub type ApiResult = poem::Result; + +pub trait SessionExt { + fn is_authorized(&self) -> bool; +} + +impl SessionExt for Session { + fn is_authorized(&self) -> bool { + self.get::("username").is_some() + } +} + +pub async fn authorized(session: &Session, f: FN) -> ApiResult +where + FN: FnOnce() -> FT, + FT: futures::Future>, +{ + if !session.is_authorized() { + return Err(poem::Error::from_string( + "Unauthorized", + StatusCode::UNAUTHORIZED, + )); + } + f().await +} diff --git a/warpgate-admin/src/lib.rs b/warpgate-admin/src/lib.rs new file mode 100644 index 0000000..de73514 --- /dev/null +++ b/warpgate-admin/src/lib.rs @@ -0,0 +1,113 @@ +#![feature(decl_macro, proc_macro_hygiene, let_else)] +mod api; +mod embed; +mod helpers; +use crate::embed::{EmbeddedFileEndpoint, EmbeddedFilesEndpoint}; +use anyhow::{Context, Result}; +use poem::listener::{Listener, RustlsConfig, TcpListener}; +use poem::middleware::{AddData, SetHeader}; +use poem::session::{CookieConfig, MemoryStorage, ServerSession}; +use poem::{EndpointExt, Route, Server}; +use poem_openapi::OpenApiService; +use rust_embed::RustEmbed; +use std::net::SocketAddr; +use tracing::*; +use warpgate_common::Services; + +#[derive(RustEmbed)] +#[folder = "../warpgate-admin/app/dist"] +pub struct Assets; + +pub struct AdminServer { + services: Services, +} + +impl AdminServer { + pub fn new(services: &Services) -> Self { + AdminServer { + services: services.clone(), + } + } + + pub async fn run(self, address: SocketAddr) -> Result<()> { + let state = self.services.state.clone(); + let api_service = OpenApiService::new( + ( + crate::api::sessions_list::Api, + crate::api::sessions_detail::Api, + crate::api::recordings_detail::Api, + crate::api::users_list::Api, + crate::api::targets_list::Api, + crate::api::tickets_list::Api, + crate::api::tickets_detail::Api, + crate::api::known_hosts_list::Api, + crate::api::known_hosts_detail::Api, + crate::api::info::Api, + crate::api::auth::Api, + crate::api::ssh_keys::Api, + ), + "Warpgate", + env!("CARGO_PKG_VERSION"), + ) + .server("/api"); + let ui = api_service.swagger_ui(); + let spec = api_service.spec_endpoint(); + let db = self.services.db.clone(); + let config = self.services.config.clone(); + let config_provider = self.services.config_provider.clone(); + let recordings = self.services.recordings.clone(); + + let app = Route::new() + .nest("/api/swagger", ui) + .nest("/api", api_service) + .nest("/api/openapi.json", spec) + .nest_no_strip("/assets", EmbeddedFilesEndpoint::::new()) + .at("/", EmbeddedFileEndpoint::::new("index.html")) + .at( + "/api/recordings/:id/cast", + crate::api::recordings_detail::api_get_recording_cast, + ) + .at( + "/api/recordings/:id/tcpdump", + crate::api::recordings_detail::api_get_recording_tcpdump, + ) + .with(ServerSession::new( + CookieConfig::default().secure(false), + MemoryStorage::default(), + )) + .with(SetHeader::new().overriding("Strict-Transport-Security", "max-age=31536000")) + .with(AddData::new(db)) + .with(AddData::new(config_provider)) + .with(AddData::new(state)) + .with(AddData::new(recordings)) + .with(AddData::new(config.clone())); + + let (certificate, key) = { + let config = config.lock().await; + let certificate_path = config + .paths_relative_to + .join(&config.store.web_admin.certificate); + let key_path = config.paths_relative_to.join(&config.store.web_admin.key); + + ( + std::fs::read(&certificate_path).with_context(|| { + format!( + "reading SSL certificate from '{}'", + certificate_path.display() + ) + })?, + std::fs::read(&key_path).with_context(|| { + format!("reading SSL private key from '{}'", key_path.display()) + })?, + ) + }; + + info!(?address, "Listening"); + Server::new( + TcpListener::bind(address).rustls(RustlsConfig::new().cert(certificate).key(key)), + ) + .run(app) + .await + .context("Failed to start admin server") + } +} diff --git a/warpgate-common/Cargo.toml b/warpgate-common/Cargo.toml new file mode 100644 index 0000000..a4b5a84 --- /dev/null +++ b/warpgate-common/Cargo.toml @@ -0,0 +1,29 @@ +[package] +edition = "2021" +license = "Apache-2.0" +name = "warpgate-common" +version = "0.1.0" + +[dependencies] +anyhow = "1.0" +argon2 = "0.3" +async-trait = "0.1" +bytes = "1.1" +chrono = {version = "0.4", features = ["serde"]} +data-encoding = "2.3" +humantime-serde = "1.1" +packet = "0.1" +password-hash = "0.3" +poem-openapi = {version = "1.3", features = ["swagger-ui", "chrono", "uuid", "static-files"]} +rand = "0.8" +rand_core = {version = "0.6", features = ["std"]} +sea-orm = {version = "^0.6", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"], default-features = false} +serde = "1.0" +serde_json = "1.0" +thiserror = "1.0" +tokio = {version = "1.17", features = ["tracing"]} +tracing = "0.1" +url = "2.2" +uuid = {version = "0.8", features = ["v4", "serde"]} +warpgate-db-entities = {version = "*", path = "../warpgate-db-entities"} +warpgate-db-migrations = {version = "*", path = "../warpgate-db-migrations"} diff --git a/warpgate-common/src/auth.rs b/warpgate-common/src/auth.rs new file mode 100644 index 0000000..228c5ba --- /dev/null +++ b/warpgate-common/src/auth.rs @@ -0,0 +1,42 @@ +use crate::consts::TICKET_SELECTOR_PREFIX; +use crate::Secret; +use std::fmt::Debug; + +pub enum AuthSelector { + User { + username: String, + target_name: String, + }, + Ticket { + secret: Secret, + }, +} + +impl From<&String> for AuthSelector { + fn from(selector: &String) -> Self { + if let Some(secret) = selector.strip_prefix(TICKET_SELECTOR_PREFIX) { + let secret = Secret::new(secret.into()); + return AuthSelector::Ticket { secret }; + } + + let mut parts = selector.splitn(2, ':'); + let username = parts.next().unwrap_or("").to_string(); + let target_name = parts.next().unwrap_or("").to_string(); + AuthSelector::User { + username, + target_name, + } + } +} + +impl Debug for AuthSelector { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + AuthSelector::User { + username, + target_name, + } => write!(f, "<{} for {}>", username, target_name), + AuthSelector::Ticket { .. } => write!(f, ""), + } + } +} diff --git a/warpgate-common/src/config.rs b/warpgate-common/src/config.rs new file mode 100644 index 0000000..945081d --- /dev/null +++ b/warpgate-common/src/config.rs @@ -0,0 +1,227 @@ +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use std::time::Duration; + +use crate::Secret; + +const fn _default_true() -> bool { + true +} + +const fn _default_false() -> bool { + false +} + +const fn _default_port() -> u16 { + 22 +} + +fn _default_username() -> String { + "root".to_owned() +} + +fn _default_recordings_path() -> String { + "./data/recordings".to_owned() +} + +fn _default_database_url() -> Secret { + Secret::new("sqlite:data/db".to_owned()) +} + +fn _default_web_admin_listen() -> String { + "0.0.0.0:8888".to_owned() +} + +fn _default_retention() -> Duration { + Duration::SECOND * 60 * 60 * 24 * 7 +} + +fn _default_empty_string_vec() -> Vec { + vec![] +} + +#[derive(Debug, Deserialize, Serialize, Clone, Object)] +pub struct TargetSSHOptions { + pub host: String, + #[serde(default = "_default_port")] + pub port: u16, + #[serde(default = "_default_username")] + pub username: String, + #[serde(default)] + #[oai(skip)] + pub auth: SSHTargetAuth, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(untagged)] +pub enum SSHTargetAuth { + #[serde(rename = "password")] + Password { password: Secret }, + #[serde(rename = "publickey")] + PublicKey, +} + +impl Default for SSHTargetAuth { + fn default() -> Self { + SSHTargetAuth::PublicKey + } +} + +#[derive(Debug, Deserialize, Serialize, Clone, Object, Default)] +pub struct TargetWebAdminOptions {} + +#[derive(Debug, Deserialize, Serialize, Clone, Object)] +pub struct Target { + pub name: String, + #[serde(default = "_default_empty_string_vec")] + pub allow_roles: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub ssh: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub web_admin: Option, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq)] +#[serde(tag = "type")] +pub enum UserAuthCredential { + #[serde(rename = "password")] + Password { hash: Secret }, + #[serde(rename = "publickey")] + PublicKey { key: Secret }, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct User { + pub username: String, + pub credentials: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub require: Option>, + pub roles: Vec, +} + +#[derive(Debug, Deserialize, Serialize, Clone, PartialEq, Eq, Hash)] +pub struct Role { + pub name: String, +} + +fn _default_ssh_listen() -> String { + "0.0.0.0:2222".to_owned() +} + +fn _default_ssh_client_key() -> String { + "./client_key".to_owned() +} + +fn _default_ssh_keys_path() -> String { + "./data/keys".to_owned() +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct SSHConfig { + #[serde(default = "_default_ssh_listen")] + pub listen: String, + + #[serde(default = "_default_ssh_keys_path")] + pub keys: String, + + #[serde(default = "_default_ssh_client_key")] + pub client_key: String, +} + +impl Default for SSHConfig { + fn default() -> Self { + SSHConfig { + listen: _default_ssh_listen(), + keys: _default_ssh_keys_path(), + client_key: _default_ssh_client_key(), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct WebAdminConfig { + #[serde(default = "_default_false")] + pub enable: bool, + + #[serde(default = "_default_web_admin_listen")] + pub listen: String, + + #[serde(default)] + pub certificate: String, + + #[serde(default)] + pub key: String, +} + +impl Default for WebAdminConfig { + fn default() -> Self { + WebAdminConfig { + enable: true, + listen: _default_web_admin_listen(), + certificate: "".to_owned(), + key: "".to_owned(), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct RecordingsConfig { + #[serde(default = "_default_false")] + pub enable: bool, + + #[serde(default = "_default_recordings_path")] + pub path: String, +} + +impl Default for RecordingsConfig { + fn default() -> Self { + Self { + enable: false, + path: _default_recordings_path(), + } + } +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct WarpgateConfigStore { + pub targets: Vec, + pub users: Vec, + pub roles: Vec, + + #[serde(default)] + pub recordings: RecordingsConfig, + + #[serde(default)] + pub web_admin: WebAdminConfig, + + #[serde(default = "_default_database_url")] + pub database_url: Secret, + + #[serde(default)] + pub ssh: SSHConfig, + + #[serde(default = "_default_retention", with = "humantime_serde")] + pub retention: Duration, +} + +impl Default for WarpgateConfigStore { + fn default() -> Self { + Self { + targets: vec![], + users: vec![], + roles: vec![], + recordings: RecordingsConfig::default(), + web_admin: WebAdminConfig::default(), + database_url: _default_database_url(), + ssh: SSHConfig::default(), + retention: _default_retention(), + } + } +} + +#[derive(Debug, Clone)] +pub struct WarpgateConfig { + pub store: WarpgateConfigStore, + pub paths_relative_to: PathBuf, +} diff --git a/warpgate-common/src/config_providers/file.rs b/warpgate-common/src/config_providers/file.rs new file mode 100644 index 0000000..1dee5a6 --- /dev/null +++ b/warpgate-common/src/config_providers/file.rs @@ -0,0 +1,226 @@ +use super::ConfigProvider; +use crate::hash::verify_password_hash; +use crate::{ + AuthCredential, AuthResult, Target, User, UserAuthCredential, UserSnapshot, WarpgateConfig, +}; +use anyhow::Result; +use async_trait::async_trait; +use data_encoding::BASE64_MIME; +use sea_orm::ActiveValue::Set; +use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait}; +use std::collections::HashSet; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::*; +use uuid::Uuid; +use warpgate_db_entities::Ticket; + +pub struct FileConfigProvider { + db: Arc>, + config: Arc>, +} + +impl FileConfigProvider { + pub async fn new( + db: &Arc>, + config: &Arc>, + ) -> Self { + Self { + db: db.clone(), + config: config.clone(), + } + } +} + +fn credential_is_type(c: &UserAuthCredential, k: &str) -> bool { + match c { + UserAuthCredential::Password { .. } => k == "password", + UserAuthCredential::PublicKey { .. } => k == "publickey", + } +} + +#[async_trait] +impl ConfigProvider for FileConfigProvider { + async fn list_users(&mut self) -> Result> { + Ok(self + .config + .lock() + .await + .store + .users + .iter() + .map(UserSnapshot::new) + .collect::>()) + } + + async fn list_targets(&mut self) -> Result> { + Ok(self + .config + .lock() + .await + .store + .targets + .iter() + .map(|x| x.to_owned()) + .collect::>()) + } + + async fn authorize( + &mut self, + username: &str, + credentials: &[AuthCredential], + ) -> Result { + if credentials.is_empty() { + return Ok(AuthResult::Rejected); + } + + let user = { + self.config + .lock() + .await + .store + .users + .iter() + .find(|x| x.username == username) + .map(User::to_owned) + }; + let Some(user) = user else { + error!("Selected user not found: {}", username); + return Ok(AuthResult::Rejected); + }; + + let mut valid_credentials = vec![]; + + for client_credential in credentials { + if let AuthCredential::PublicKey { + kind, + public_key_bytes, + } = client_credential + { + let mut base64_bytes = BASE64_MIME.encode(public_key_bytes); + base64_bytes.pop(); + base64_bytes.pop(); + + let client_key = format!("{} {}", kind, base64_bytes); + debug!(username=%user.username, "Client key: {}", client_key); + + for credential in user.credentials.iter() { + if let UserAuthCredential::PublicKey { key: ref user_key } = credential { + if &client_key == user_key.expose_secret() { + valid_credentials.push(credential); + break; + } + } + } + } + } + + for client_credential in credentials { + if let AuthCredential::Password(client_password) = client_credential { + for credential in user.credentials.iter() { + if let UserAuthCredential::Password { + hash: ref user_password_hash, + } = credential + { + match verify_password_hash( + client_password.expose_secret(), + user_password_hash.expose_secret(), + ) { + Ok(true) => { + valid_credentials.push(credential); + break; + } + Ok(false) => continue, + Err(e) => { + error!(username=%user.username, "Error verifying password hash: {}", e); + continue; + } + } + } + } + } + } + + if !valid_credentials.is_empty() { + match user.require { + Some(ref required_kinds) => { + for kind in required_kinds { + if !valid_credentials + .iter() + .any(|x| credential_is_type(x, kind)) + { + return Ok(AuthResult::Rejected); + } + } + return Ok(AuthResult::Accepted { + username: user.username.clone(), + }); + } + None => { + return Ok(AuthResult::Accepted { + username: user.username.clone(), + }) + } + } + } + + warn!(username=%user.username, "Client credentials did not match"); + Ok(AuthResult::Rejected) + } + + async fn authorize_target(&mut self, username: &str, target_name: &str) -> Result { + let config = self.config.lock().await; + let user = config + .store + .users + .iter() + .find(|x| x.username == username) + .map(User::to_owned); + let target = config.store.targets.iter().find(|x| x.name == target_name); + + let Some(user) = user else { + error!("Selected user not found: {}", username); + return Ok(false); + }; + + let Some(target) = target else { + error!("Selected target not found: {}", target_name); + return Ok(false); + }; + + let user_roles = user + .roles + .iter() + .map(|x| config.store.roles.iter().find(|y| &y.name == x)) + .filter(|x| x.is_some()) + .map(|x| x.unwrap().to_owned()) + .collect::>(); + let target_roles = target + .allow_roles + .iter() + .map(|x| config.store.roles.iter().find(|y| &y.name == x)) + .filter(|x| x.is_some()) + .map(|x| x.unwrap().to_owned()) + .collect::>(); + + let intersect = user_roles.intersection(&target_roles).count() > 0; + + Ok(intersect) + } + + async fn consume_ticket(&mut self, ticket_id: &Uuid) -> Result<()> { + let db = self.db.lock().await; + let ticket = Ticket::Entity::find_by_id(*ticket_id).one(&*db).await?; + let Some(ticket) = ticket else { + anyhow::bail!("Ticket not found: {}", ticket_id); + }; + + if let Some(uses_left) = ticket.uses_left { + let mut model: Ticket::ActiveModel = ticket.into(); + model.uses_left = Set(Some(uses_left - 1)); + model.update(&*db).await?; + } + + Ok(()) + } +} diff --git a/warpgate-common/src/config_providers/mod.rs b/warpgate-common/src/config_providers/mod.rs new file mode 100644 index 0000000..18012c7 --- /dev/null +++ b/warpgate-common/src/config_providers/mod.rs @@ -0,0 +1,77 @@ +mod file; +use crate::{Secret, Target, UserSnapshot}; +use anyhow::Result; +use async_trait::async_trait; +use bytes::Bytes; +pub use file::FileConfigProvider; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::*; +use uuid::Uuid; +use warpgate_db_entities::Ticket; + +pub enum AuthResult { + Accepted { username: String }, + Rejected, +} + +pub enum AuthCredential { + Password(Secret), + PublicKey { + kind: String, + public_key_bytes: Bytes, + }, +} + +#[async_trait] +pub trait ConfigProvider { + async fn list_users(&mut self) -> Result>; + + async fn list_targets(&mut self) -> Result>; + + async fn authorize( + &mut self, + username: &str, + credentials: &[AuthCredential], + ) -> Result; + + async fn authorize_target(&mut self, username: &str, target: &str) -> Result; + + async fn consume_ticket(&mut self, ticket_id: &Uuid) -> Result<()>; +} + +//TODO: move this somewhere +pub async fn authorize_ticket( + db: &Arc>, + secret: &Secret, +) -> Result> { + let ticket = { + let db = db.lock().await; + Ticket::Entity::find() + .filter(Ticket::Column::Secret.eq(&secret.expose_secret()[..])) + .one(&*db) + .await? + }; + match ticket { + Some(ticket) => { + if let Some(0) = ticket.uses_left { + warn!("Ticket is used up: {}", &ticket.id); + return Ok(None); + } + + if let Some(datetime) = ticket.expiry { + if datetime < chrono::Utc::now() { + warn!("Ticket has expired: {}", &ticket.id); + return Ok(None); + } + } + + Ok(Some(ticket)) + } + None => { + warn!("Ticket not found: {}", &secret.expose_secret()); + Ok(None) + } + } +} diff --git a/warpgate-common/src/consts.rs b/warpgate-common/src/consts.rs new file mode 100644 index 0000000..8ab9d1c --- /dev/null +++ b/warpgate-common/src/consts.rs @@ -0,0 +1 @@ +pub const TICKET_SELECTOR_PREFIX: &str = "ticket-"; diff --git a/warpgate-common/src/data.rs b/warpgate-common/src/data.rs new file mode 100644 index 0000000..7168ab4 --- /dev/null +++ b/warpgate-common/src/data.rs @@ -0,0 +1,45 @@ +use chrono::{DateTime, Utc}; +use poem_openapi::Object; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use warpgate_db_entities::Session; + +use crate::{SessionId, Target, User}; + +#[derive(Serialize, Deserialize, Object)] +pub struct SessionSnapshot { + pub id: SessionId, + pub username: Option, + pub target: Option, + pub started: DateTime, + pub ended: Option>, + pub ticket_id: Option, +} + +impl From for SessionSnapshot { + fn from(model: Session::Model) -> Self { + Self { + id: model.id, + username: model.username, + target: model + .target_snapshot + .and_then(|s| serde_json::from_str(&s).ok()), + started: model.started, + ended: model.ended, + ticket_id: model.ticket_id, + } + } +} + +#[derive(Serialize, Deserialize, Object)] +pub struct UserSnapshot { + pub username: String, +} + +impl UserSnapshot { + pub fn new(user: &User) -> Self { + Self { + username: user.username.clone(), + } + } +} diff --git a/warpgate-common/src/db/mod.rs b/warpgate-common/src/db/mod.rs new file mode 100644 index 0000000..a4da537 --- /dev/null +++ b/warpgate-common/src/db/mod.rs @@ -0,0 +1,96 @@ +use anyhow::Result; +use sea_orm::sea_query::Expr; +use sea_orm::{ + ConnectOptions, Database, DatabaseConnection, EntityTrait, QueryFilter, TransactionTrait, +}; +use std::time::Duration; +use warpgate_db_migrations::{Migrator, MigratorTrait}; + +use crate::helpers::fs::secure_file; +use crate::WarpgateConfig; + +pub async fn connect_to_db(config: &WarpgateConfig) -> Result { + let mut url = url::Url::parse(&config.store.database_url.expose_secret()[..])?; + if url.scheme() == "sqlite" { + let path = url.path(); + let mut abs_path = config.paths_relative_to.clone(); + abs_path.push(path); + abs_path.push("db.sqlite3"); + + if let Some(parent) = abs_path.parent() { + std::fs::create_dir_all(parent)? + } + + url.set_path( + abs_path + .to_str() + .ok_or_else(|| anyhow::anyhow!("Failed to convert database path to string"))?, + ); + + url.set_query(Some("mode=rwc")); + + let db = Database::connect(ConnectOptions::new(url.to_string())).await?; + db.begin().await?.commit().await?; + drop(db); + + secure_file(&abs_path)?; + } + + let mut opt = ConnectOptions::new(url.to_string()); + opt.max_connections(100) + .min_connections(5) + .connect_timeout(Duration::from_secs(8)) + .idle_timeout(Duration::from_secs(8)) + .max_lifetime(Duration::from_secs(8)) + .sqlx_logging(true); + + let connection = Database::connect(opt).await?; + + Migrator::up(&connection, None).await?; + + Ok(connection) +} + +pub async fn sanitize_db(db: &mut DatabaseConnection) -> Result<()> { + use sea_orm::ActiveValue::Set; + use warpgate_db_entities::{Recording, Session}; + + Recording::Entity::update_many() + .set(Recording::ActiveModel { + ended: Set(Some(chrono::Utc::now())), + ..Default::default() + }) + .filter(Expr::col(Recording::Column::Ended).is_null()) + .exec(db) + .await?; + + Session::Entity::update_many() + .set(Session::ActiveModel { + ended: Set(Some(chrono::Utc::now())), + ..Default::default() + }) + .filter(Expr::col(Session::Column::Ended).is_null()) + .exec(db) + .await?; + + Ok(()) +} + +pub async fn cleanup_db(db: &mut DatabaseConnection, retention: &Duration) -> Result<()> { + use warpgate_db_entities::{Recording, Session}; + let cutoff = chrono::Utc::now() - chrono::Duration::from_std(*retention)?; + + Recording::Entity::delete_many() + .filter(Expr::col(Session::Column::Ended).is_not_null()) + .filter(Expr::col(Session::Column::Ended).lt(cutoff)) + .exec(db) + .await?; + + Session::Entity::delete_many() + .filter(Expr::col(Session::Column::Ended).is_not_null()) + .filter(Expr::col(Session::Column::Ended).lt(cutoff)) + .exec(db) + .await?; + + Ok(()) +} diff --git a/warpgate-common/src/db/uuid.rs b/warpgate-common/src/db/uuid.rs new file mode 100644 index 0000000..4225fd2 --- /dev/null +++ b/warpgate-common/src/db/uuid.rs @@ -0,0 +1,45 @@ +use std::io::Write; + +use crate::UUID; + +impl FromSql for UUID +where + Vec: FromSql, +{ + fn from_sql(bytes: Option<&B::RawValue>) -> diesel::deserialize::Result { + let value = >::from_sql(bytes)?; + Ok(UUID::from_bytes(&value)?) + } +} + +impl ToSql for UUID +where + [u8]: ToSql, +{ + fn to_sql( + &self, + out: &mut diesel::serialize::Output, + ) -> diesel::serialize::Result { + let bytes = self.0.as_bytes(); + <[u8] as ToSql>::to_sql(bytes, out) + } +} + +impl AsExpression for UUID { + type Expression = Bound; + + fn as_expression(self) -> Self::Expression { + Bound::new(self) + } +} + +impl<'a> AsExpression for &'a UUID { + type Expression = Bound; + + fn as_expression(self) -> Self::Expression { + Bound::new(self) + } +} +// impl Expression for UUID { +// type SqlType = diesel::sql_types::Binary; +// } diff --git a/warpgate-common/src/eventhub.rs b/warpgate-common/src/eventhub.rs new file mode 100644 index 0000000..c5f0417 --- /dev/null +++ b/warpgate-common/src/eventhub.rs @@ -0,0 +1,94 @@ +use std::sync::Arc; +use tokio::sync::mpsc::error::SendError; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use tokio::sync::{Mutex, MutexGuard}; + +pub struct EventSender { + subscriptions: SubscriptionStore, +} + +impl Clone for EventSender { + fn clone(&self) -> Self { + EventSender { + subscriptions: self.subscriptions.clone(), + } + } +} + +impl EventSender { + async fn cleanup_subscriptions(&self) -> MutexGuard<'_, SubscriptionStoreInner> { + let mut subscriptions = self.subscriptions.lock().await; + subscriptions + .drain_filter(|(_, ref s)| s.is_closed()) + .for_each(drop); + subscriptions + } +} + +impl<'h, E: Clone + 'h> EventSender { + pub async fn send_all(&'h self, event: E) -> Result<(), SendError> { + let mut subscriptions = self.cleanup_subscriptions().await; + + for (ref f, ref mut s) in subscriptions.iter_mut().rev() { + if f(&event) { + let _ = s.send(event.clone()); + } + } + if subscriptions.is_empty() { + Err(SendError(event)) + } else { + Ok(()) + } + } +} + +impl<'h, E: 'h> EventSender { + pub async fn send_once(&'h self, event: E) -> Result<(), SendError> { + let mut subscriptions = self.cleanup_subscriptions().await; + + for (ref f, ref mut s) in subscriptions.iter_mut().rev() { + if f(&event) { + return s.send(event); + } + } + + Err(SendError(event)) + } +} + +pub struct EventSubscription(UnboundedReceiver); + +impl EventSubscription { + pub async fn recv(&mut self) -> Option { + self.0.recv().await + } +} + +type SubscriptionStoreInner = Vec<(Box bool + Send>, UnboundedSender)>; +type SubscriptionStore = Arc>>; + +pub struct EventHub { + subscriptions: SubscriptionStore, +} + +impl<'h, E: Send> EventHub { + pub fn setup() -> (Self, EventSender) { + let subscriptions = Arc::new(Mutex::new(vec![])); + ( + Self { + subscriptions: subscriptions.clone(), + }, + EventSender { subscriptions }, + ) + } + + pub async fn subscribe bool + Send + 'static>( + &'h self, + filter: F, + ) -> EventSubscription { + let (sender, receiver) = unbounded_channel(); + let mut subscriptions = self.subscriptions.lock().await; + subscriptions.push((Box::new(filter), sender)); + EventSubscription(receiver) + } +} diff --git a/warpgate-common/src/hash.rs b/warpgate-common/src/hash.rs new file mode 100644 index 0000000..2ceceea --- /dev/null +++ b/warpgate-common/src/hash.rs @@ -0,0 +1,32 @@ +use crate::Secret; +use anyhow::Result; +use argon2::password_hash::rand_core::OsRng; +use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; +use argon2::Argon2; +use data_encoding::HEXLOWER; +use password_hash::errors::Error; +use rand::Rng; + +pub fn hash_password(password: &str) -> String { + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + argon2 + .hash_password(password.as_bytes(), &salt) + .unwrap() + .to_string() +} + +pub fn verify_password_hash(password: &str, hash: &str) -> Result { + let parsed_hash = PasswordHash::new(hash).map_err(|e| anyhow::anyhow!(e))?; + match Argon2::default().verify_password(password.as_bytes(), &parsed_hash) { + Ok(()) => Ok(true), + Err(Error::Password) => Ok(false), + Err(e) => Err(anyhow::anyhow!(e)), + } +} + +pub fn generate_ticket_secret() -> Secret { + let mut bytes = [0; 32]; + rand::thread_rng().fill(&mut bytes[..]); + Secret::new(HEXLOWER.encode(&bytes)) +} diff --git a/warpgate-common/src/helpers/fs.rs b/warpgate-common/src/helpers/fs.rs new file mode 100644 index 0000000..e4d5ca1 --- /dev/null +++ b/warpgate-common/src/helpers/fs.rs @@ -0,0 +1,10 @@ +use std::os::unix::prelude::PermissionsExt; +use std::path::Path; + +pub fn secure_directory>(path: P) -> std::io::Result<()> { + std::fs::set_permissions(path.as_ref(), std::fs::Permissions::from_mode(0o700)) +} + +pub fn secure_file>(path: P) -> std::io::Result<()> { + std::fs::set_permissions(path.as_ref(), std::fs::Permissions::from_mode(0o600)) +} diff --git a/warpgate-common/src/helpers/mod.rs b/warpgate-common/src/helpers/mod.rs new file mode 100644 index 0000000..952aa13 --- /dev/null +++ b/warpgate-common/src/helpers/mod.rs @@ -0,0 +1,2 @@ +pub mod fs; +pub mod serde_base64; diff --git a/warpgate-common/src/helpers/serde_base64.rs b/warpgate-common/src/helpers/serde_base64.rs new file mode 100644 index 0000000..4507da0 --- /dev/null +++ b/warpgate-common/src/helpers/serde_base64.rs @@ -0,0 +1,21 @@ +use bytes::Bytes; +use data_encoding::BASE64; +use serde::{Deserialize, Serializer}; + +pub fn serialize(bytes: &Bytes, serializer: S) -> Result +where + S: Serializer, +{ + serializer.serialize_str(&BASE64.encode(bytes)) +} + +pub fn deserialize<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + Ok(BASE64 + .decode(s.as_bytes()) + .map_err(serde::de::Error::custom)? + .into()) +} diff --git a/warpgate-common/src/lib.rs b/warpgate-common/src/lib.rs new file mode 100644 index 0000000..3a88ccd --- /dev/null +++ b/warpgate-common/src/lib.rs @@ -0,0 +1,23 @@ +#![feature(let_else, drain_filter, duration_constants)] +pub mod auth; +mod config; +mod config_providers; +pub mod consts; +mod data; +pub mod db; +pub mod eventhub; +pub mod hash; +pub mod helpers; +mod protocols; +pub mod recordings; +mod services; +mod state; +mod types; + +pub use config::*; +pub use config_providers::*; +pub use data::*; +pub use protocols::*; +pub use services::*; +pub use state::{SessionState, State}; +pub use types::*; diff --git a/warpgate-common/src/protocols/handle.rs b/warpgate-common/src/protocols/handle.rs new file mode 100644 index 0000000..fbc717c --- /dev/null +++ b/warpgate-common/src/protocols/handle.rs @@ -0,0 +1,90 @@ +use crate::{SessionId, SessionState, State, Target}; +use anyhow::{Context, Result}; +use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; +use std::sync::Arc; +use tokio::sync::Mutex; +use warpgate_db_entities::Session; + +pub trait SessionHandle { + fn close(&mut self); +} + +pub struct WarpgateServerHandle { + id: SessionId, + db: Arc>, + state: Arc>, + session_state: Arc>, +} + +impl WarpgateServerHandle { + pub fn new( + id: SessionId, + db: Arc>, + state: Arc>, + session_state: Arc>, + ) -> Self { + WarpgateServerHandle { + id, + db, + state, + session_state, + } + } + + pub fn id(&self) -> SessionId { + self.id + } + + pub async fn set_username(&mut self, username: String) -> Result<()> { + use sea_orm::ActiveValue::Set; + + { + self.session_state.lock().await.username = Some(username.clone()) + } + + let db = self.db.lock().await; + + Session::Entity::update_many() + .set(Session::ActiveModel { + username: Set(Some(username)), + ..Default::default() + }) + .filter(Session::Column::Id.eq(self.id)) + .exec(&*db) + .await?; + + Ok(()) + } + + pub async fn set_target(&mut self, target: &Target) -> Result<()> { + use sea_orm::ActiveValue::Set; + { + self.session_state.lock().await.target = Some(target.clone()); + } + + let db = self.db.lock().await; + + Session::Entity::update_many() + .set(Session::ActiveModel { + target_snapshot: Set(Some( + serde_json::to_string(&target).context("Error serializing target")?, + )), + ..Default::default() + }) + .filter(Session::Column::Id.eq(self.id)) + .exec(&*db) + .await?; + + Ok(()) + } +} + +impl Drop for WarpgateServerHandle { + fn drop(&mut self) { + let id = self.id; + let state = self.state.clone(); + tokio::spawn(async move { + state.lock().await.remove_session(id).await; + }); + } +} diff --git a/warpgate-common/src/protocols/mod.rs b/warpgate-common/src/protocols/mod.rs new file mode 100644 index 0000000..d204cad --- /dev/null +++ b/warpgate-common/src/protocols/mod.rs @@ -0,0 +1,26 @@ +mod handle; +use crate::Target; +use anyhow::Result; +use async_trait::async_trait; +pub use handle::{SessionHandle, WarpgateServerHandle}; +use std::net::SocketAddr; + +#[derive(Debug, thiserror::Error)] +pub enum TargetTestError { + #[error("unreachable")] + Unreachable, + #[error("authentication failed")] + AuthenticationError, + #[error("connection error")] + ConnectionError(String), + #[error("misconfigured")] + Misconfigured(String), + #[error("I/O")] + Io(#[from] std::io::Error), +} + +#[async_trait] +pub trait ProtocolServer { + async fn run(self, address: SocketAddr) -> Result<()>; + async fn test_target(self, target: Target) -> Result<(), TargetTestError>; +} diff --git a/warpgate-common/src/recordings/mod.rs b/warpgate-common/src/recordings/mod.rs new file mode 100644 index 0000000..9889048 --- /dev/null +++ b/warpgate-common/src/recordings/mod.rs @@ -0,0 +1,94 @@ +use sea_orm::{ActiveModelTrait, DatabaseConnection}; +use std::path::PathBuf; +use std::sync::Arc; +use tokio::sync::Mutex; +use tracing::*; +use uuid::Uuid; +use warpgate_db_entities::Recording::{self, RecordingKind}; + +use crate::{RecordingsConfig, SessionId, WarpgateConfig}; +mod terminal; +mod traffic; +mod writer; +pub use terminal::*; +pub use traffic::*; +use writer::RecordingWriter; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("I/O")] + Io(#[from] std::io::Error), + + #[error("Database")] + Database(#[from] sea_orm::DbErr), + + #[error("Writer is closed")] + Closed, + + #[error("Disabled")] + Disabled, +} + +pub type Result = std::result::Result; + +pub trait Recorder { + fn kind() -> RecordingKind; + fn new(writer: RecordingWriter) -> Self; +} + +pub struct SessionRecordings { + db: Arc>, + path: PathBuf, + config: RecordingsConfig, +} + +impl SessionRecordings { + pub fn new(db: Arc>, config: &WarpgateConfig) -> Result { + let mut path = config.paths_relative_to.clone(); + path.push(&config.store.recordings.path); + if config.store.recordings.enable { + std::fs::create_dir_all(&path)?; + crate::helpers::fs::secure_directory(&path)?; + } + Ok(Self { + db, + config: config.store.recordings.clone(), + path, + }) + } + + pub async fn start(&self, id: &SessionId, name: String) -> Result + where + T: Recorder, + { + if !self.config.enable { + return Err(Error::Disabled); + } + + let path = self.path_for(id, &name); + tokio::fs::create_dir_all(&path.parent().unwrap()).await?; + info!(%name, path=?path, "Recording session {}", id); + + let model = { + use sea_orm::ActiveValue::Set; + let values = Recording::ActiveModel { + id: Set(Uuid::new_v4()), + started: Set(chrono::Utc::now()), + session_id: Set(*id), + name: Set(name), + kind: Set(T::kind()), + ..Default::default() + }; + + let db = self.db.lock().await; + values.insert(&*db).await.map_err(Error::Database)? + }; + + let writer = RecordingWriter::new(path, model, self.db.clone()).await?; + Ok(T::new(writer)) + } + + pub fn path_for(&self, session_id: &SessionId, name: &dyn AsRef) -> PathBuf { + self.path.join(session_id.to_string()).join(&name) + } +} diff --git a/warpgate-common/src/recordings/terminal.rs b/warpgate-common/src/recordings/terminal.rs new file mode 100644 index 0000000..7e644eb --- /dev/null +++ b/warpgate-common/src/recordings/terminal.rs @@ -0,0 +1,71 @@ +use anyhow::Result; +use bytes::{Bytes, BytesMut}; +use serde::{Deserialize, Serialize}; +use tokio::time::Instant; +use warpgate_db_entities::Recording::RecordingKind; + +use super::writer::RecordingWriter; +use super::Recorder; + +#[derive(Serialize, Deserialize, Debug)] +#[serde(untagged)] +pub enum TerminalRecordingItem { + Data { + time: f32, + #[serde(with = "crate::helpers::serde_base64")] + data: Bytes, + }, + PtyResize { + time: f32, + cols: u32, + rows: u32, + }, +} + +pub struct TerminalRecorder { + writer: RecordingWriter, + started_at: Instant, +} + +impl TerminalRecorder { + fn get_time(&self) -> f32 { + self.started_at.elapsed().as_secs_f32() + } + + async fn write_item(&mut self, item: &TerminalRecordingItem) -> Result<()> { + let serialized_item = serde_json::to_vec(&item)?; + self.writer.write(&serialized_item).await?; + self.writer.write(b"\n").await?; + Ok(()) + } + + pub async fn write(&mut self, data: &[u8]) -> Result<()> { + self.write_item(&TerminalRecordingItem::Data { + time: self.get_time(), + data: BytesMut::from(data).freeze(), + }) + .await + } + + pub async fn write_pty_resize(&mut self, cols: u32, rows: u32) -> Result<()> { + self.write_item(&TerminalRecordingItem::PtyResize { + time: self.get_time(), + rows, + cols, + }) + .await + } +} + +impl Recorder for TerminalRecorder { + fn kind() -> RecordingKind { + RecordingKind::Terminal + } + + fn new(writer: RecordingWriter) -> Self { + TerminalRecorder { + writer, + started_at: Instant::now(), + } + } +} diff --git a/warpgate-common/src/recordings/traffic.rs b/warpgate-common/src/recordings/traffic.rs new file mode 100644 index 0000000..dd81645 --- /dev/null +++ b/warpgate-common/src/recordings/traffic.rs @@ -0,0 +1,207 @@ +use std::net::Ipv4Addr; + +use anyhow::Result; +use bytes::Bytes; +use packet::Builder; +use rand::Rng; +use tokio::time::Instant; +use tracing::*; +use warpgate_db_entities::Recording::RecordingKind; + +use super::writer::RecordingWriter; +use super::Recorder; + +pub struct TrafficRecorder { + writer: RecordingWriter, + started_at: Instant, +} + +#[derive(Debug)] +pub struct TrafficConnectionParams { + pub src_addr: Ipv4Addr, + pub src_port: u16, + pub dst_addr: Ipv4Addr, + pub dst_port: u16, +} + +impl TrafficRecorder { + pub fn connection(&mut self, params: TrafficConnectionParams) -> ConnectionRecorder { + ConnectionRecorder::new(params, self.writer.clone(), self.started_at) + } +} + +impl Recorder for TrafficRecorder { + fn kind() -> RecordingKind { + RecordingKind::Traffic + } + + fn new(writer: RecordingWriter) -> Self { + TrafficRecorder { + writer, + started_at: Instant::now(), + } + } +} + +pub struct ConnectionRecorder { + params: TrafficConnectionParams, + seq_tx: u32, + seq_rx: u32, + writer: RecordingWriter, + started_at: Instant, +} + +impl ConnectionRecorder { + fn new(params: TrafficConnectionParams, writer: RecordingWriter, started_at: Instant) -> Self { + Self { + params, + writer, + started_at, + seq_rx: rand::thread_rng().gen(), + seq_tx: rand::thread_rng().gen(), + } + } + + pub async fn write_connection_setup(&mut self) -> Result<()> { + self.writer + .write(&[ + 0xd4, 0xc3, 0xb2, 0xa1, 0x02, 0, 0x04, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff, 0, 0, + 101, 0, 0, 0, + ]) + .await?; + let init = self.tcp_init()?; + self.write_packet(init.0).await?; + self.write_packet(init.1).await?; + self.write_packet(init.2).await?; + Ok(()) + } + + async fn write_packet(&mut self, data: Bytes) -> Result<()> { + let ms = Instant::now().duration_since(self.started_at).as_micros(); + self.writer + .write(&u32::to_le_bytes((ms / 10u128.pow(6)) as u32)) + .await?; + self.writer + .write(&u32::to_le_bytes((ms % 10u128.pow(6)) as u32)) + .await?; + self.writer + .write(&u32::to_le_bytes(data.len() as u32)) + .await?; + self.writer + .write(&u32::to_le_bytes(data.len() as u32)) + .await?; + self.writer.write(&data).await?; + debug!("connection {:?} data {:?}", self.params, data); + Ok(()) + } + + pub async fn write_rx(&mut self, data: &[u8]) -> Result<()> { + debug!("connection {:?} data tx {:?}", self.params, data); + let seq_rx = self.seq_rx; + self.seq_rx = self.seq_rx.wrapping_add(data.len() as u32); + self.write_packet( + self.tcp_packet_rx(|b| Ok(b.sequence(seq_rx)?.payload(data)?.build()?.into()))?, + ) + .await?; + self.write_packet(self.tcp_packet_tx(|b| { + Ok(b.sequence(self.seq_tx)? + .acknowledgment(seq_rx + 1)? + .flags(packet::tcp::Flags::ACK)? + .build()? + .into()) + })?) + .await?; + Ok(()) + } + + pub async fn write_tx(&mut self, data: &[u8]) -> Result<()> { + debug!("connection {:?} data tx {:?}", self.params, data); + let seq_tx = self.seq_tx; + self.seq_tx = self.seq_tx.wrapping_add(data.len() as u32); + self.write_packet( + self.tcp_packet_tx(|b| Ok(b.sequence(seq_tx)?.payload(data)?.build()?.into()))?, + ) + .await?; + self.write_packet(self.tcp_packet_rx(|b| { + Ok(b.sequence(self.seq_rx)? + .acknowledgment(seq_tx + 1)? + .flags(packet::tcp::Flags::ACK)? + .build()? + .into()) + })?) + .await?; + Ok(()) + } + + fn ip_packet_tx(&self, f: F) -> Result + where + F: FnOnce(packet::ip::v4::Builder) -> Result, + { + f(packet::ip::v4::Builder::default() + .protocol(packet::ip::Protocol::Tcp)? + .source(self.params.src_addr)? + .destination(self.params.dst_addr)?) + } + + fn ip_packet_rx(&self, f: F) -> Result + where + F: FnOnce(packet::ip::v4::Builder) -> Result, + { + f(packet::ip::v4::Builder::default() + .protocol(packet::ip::Protocol::Tcp)? + .source(self.params.dst_addr)? + .destination(self.params.src_addr)?) + } + + fn tcp_packet_tx(&self, f: F) -> Result + where + F: FnOnce(packet::tcp::Builder) -> Result, + { + self.ip_packet_tx(|b| { + f(b.tcp()? + .source(self.params.src_port)? + .destination(self.params.dst_port)?) + }) + } + + fn tcp_packet_rx(&self, f: F) -> Result + where + F: FnOnce(packet::tcp::Builder) -> Result, + { + self.ip_packet_rx(|b| { + f(b.tcp()? + .source(self.params.dst_port)? + .destination(self.params.src_port)?) + }) + } + + fn tcp_init(&mut self) -> Result<(Bytes, Bytes, Bytes)> { + let seq_tx = self.seq_tx; + self.seq_tx = self.seq_tx.wrapping_add(1); + let seq_rx = self.seq_rx; + self.seq_rx = self.seq_rx.wrapping_add(1); + + Ok(( + self.tcp_packet_tx(|b| { + Ok(b.sequence(seq_tx)? + .flags(packet::tcp::Flags::SYN)? + .build()? + .into()) + })?, + self.tcp_packet_rx(|b| { + Ok(b.sequence(seq_rx)? + .acknowledgment(seq_tx + 1)? + .flags(packet::tcp::Flags::SYN | packet::tcp::Flags::ACK)? + .build()? + .into()) + })?, + self.tcp_packet_tx(|b| { + Ok(b.sequence(seq_tx + 1)? + .acknowledgment(seq_rx + 1)? + .flags(packet::tcp::Flags::ACK)? + .build()? + .into()) + })?, + )) + } +} diff --git a/warpgate-common/src/recordings/writer.rs b/warpgate-common/src/recordings/writer.rs new file mode 100644 index 0000000..d9ee55a --- /dev/null +++ b/warpgate-common/src/recordings/writer.rs @@ -0,0 +1,86 @@ +use crate::helpers::fs::secure_file; + +use super::{Error, Result}; +use bytes::{Bytes, BytesMut}; +use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait}; +use std::path::PathBuf; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::fs::File; +use tokio::io::{AsyncWriteExt, BufWriter}; +use tokio::sync::{mpsc, Mutex}; +use tracing::*; +use warpgate_db_entities::Recording; + +#[derive(Clone)] +pub struct RecordingWriter { + sender: mpsc::Sender, +} + +impl RecordingWriter { + pub(crate) async fn new( + path: PathBuf, + model: Recording::Model, + db: Arc>, + ) -> Result { + let file = File::create(&path).await?; + secure_file(&path)?; + let mut writer = BufWriter::new(file); + let (sender, mut receiver) = mpsc::channel::(1024); + tokio::spawn(async move { + if let Err(error) = async { + let mut last_flush = Instant::now(); + loop { + if Instant::now() - last_flush > Duration::from_secs(5) { + last_flush = Instant::now(); + writer.flush().await?; + } + tokio::select! { + data = receiver.recv() => match data { + Some(bytes) => { + writer.write_all(&bytes).await?; + } + None => break, + }, + _ = tokio::time::sleep(Duration::from_millis(5000)) => () + } + } + Ok::<(), anyhow::Error>(()) + } + .await + { + error!(%error, ?path, "Failed to write recording"); + } + + if let Err(error) = async { + writer.flush().await?; + + use sea_orm::ActiveValue::Set; + let id = model.id; + let db = db.lock().await; + let recording = Recording::Entity::find_by_id(id) + .one(&*db) + .await? + .ok_or_else(|| anyhow::anyhow!("Recording not found"))?; + let mut model: Recording::ActiveModel = recording.into(); + model.ended = Set(Some(chrono::Utc::now())); + model.update(&*db).await?; + Ok::<(), anyhow::Error>(()) + } + .await + { + error!(%error, ?path, "Failed to write recording"); + } + }); + + Ok(RecordingWriter { sender }) + } + + pub async fn write(&mut self, data: &[u8]) -> Result<()> { + self.sender + .send(BytesMut::from(data).freeze()) + .await + .map_err(|_| Error::Closed)?; + Ok(()) + } +} diff --git a/warpgate-common/src/services.rs b/warpgate-common/src/services.rs new file mode 100644 index 0000000..12a97fd --- /dev/null +++ b/warpgate-common/src/services.rs @@ -0,0 +1,40 @@ +use std::sync::Arc; + +use anyhow::Result; +use sea_orm::DatabaseConnection; +use tokio::sync::Mutex; + +use crate::db::{connect_to_db, sanitize_db}; +use crate::recordings::SessionRecordings; +use crate::{ConfigProvider, FileConfigProvider, State, WarpgateConfig}; + +#[derive(Clone)] +pub struct Services { + pub db: Arc>, + pub recordings: Arc>, + pub config: Arc>, + pub state: Arc>, + pub config_provider: Arc>, +} + +impl Services { + pub async fn new(config: WarpgateConfig) -> Result { + let mut db = connect_to_db(&config).await?; + sanitize_db(&mut db).await?; + let db = Arc::new(Mutex::new(db)); + + let recordings = SessionRecordings::new(db.clone(), &config)?; + let recordings = Arc::new(Mutex::new(recordings)); + + let config = Arc::new(Mutex::new(config)); + let config_provider = Arc::new(Mutex::new(FileConfigProvider::new(&db, &config).await)); + + Ok(Self { + db: db.clone(), + recordings, + config: config.clone(), + state: State::new(&db), + config_provider, + }) + } +} diff --git a/warpgate-common/src/state.rs b/warpgate-common/src/state.rs new file mode 100644 index 0000000..8417007 --- /dev/null +++ b/warpgate-common/src/state.rs @@ -0,0 +1,102 @@ +use crate::{SessionHandle, SessionId, Target, WarpgateServerHandle}; +use anyhow::{Context, Result}; +use sea_orm::{ActiveModelTrait, DatabaseConnection, EntityTrait}; +use std::collections::HashMap; +use std::net::SocketAddr; +use std::sync::{Arc, Weak}; +use tokio::sync::Mutex; +use tracing::*; +use uuid::Uuid; +use warpgate_db_entities::Session; + +pub struct State { + pub sessions: HashMap>>, + db: Arc>, + this: Weak>, +} + +impl State { + pub fn new(db: &Arc>) -> Arc> { + Arc::>::new_cyclic(|me| { + Mutex::new(Self { + sessions: HashMap::new(), + db: db.clone(), + this: me.clone(), + }) + }) + } + + pub async fn register_session( + &mut self, + session: &Arc>, + ) -> Result { + let id = uuid::Uuid::new_v4(); + self.sessions.insert(id, session.clone()); + + { + use sea_orm::ActiveValue::Set; + + let values = Session::ActiveModel { + id: Set(id), + started: Set(chrono::Utc::now()), + remote_address: Set(session.lock().await.remote_address.to_string()), + ..Default::default() + }; + + let db = self.db.lock().await; + values + .insert(&*db) + .await + .context("Error inserting session")?; + } + + match self.this.upgrade() { + Some(this) => Ok(WarpgateServerHandle::new( + id, + self.db.clone(), + this, + session.clone(), + )), + None => anyhow::bail!("State is being detroyed"), + } + } + + pub async fn remove_session(&mut self, id: SessionId) { + self.sessions.remove(&id); + + if let Err(error) = self.mark_session_complete(id).await { + error!(%error, %id, "Could not update session in the DB"); + } + } + + async fn mark_session_complete(&mut self, id: Uuid) -> Result<()> { + use sea_orm::ActiveValue::Set; + let db = self.db.lock().await; + let session = Session::Entity::find_by_id(id) + .one(&*db) + .await? + .ok_or_else(|| anyhow::anyhow!("Session not found"))?; + let mut model: Session::ActiveModel = session.into(); + model.ended = Set(Some(chrono::Utc::now())); + model.update(&*db).await?; + Ok(()) + } +} + +pub struct SessionState { + pub remote_address: SocketAddr, + pub username: Option, + pub target: Option, + pub handle: Box, +} + +impl SessionState { + pub fn new(remote_address: SocketAddr, handle: Box) -> Self { + SessionState { + remote_address, + username: None, + target: None, + handle, + } + } +} diff --git a/warpgate-common/src/types.rs b/warpgate-common/src/types.rs new file mode 100644 index 0000000..6a7971f --- /dev/null +++ b/warpgate-common/src/types.rs @@ -0,0 +1,49 @@ +use serde::{Deserialize, Serialize}; +use std::fmt::Debug; +use uuid::Uuid; + +pub type SessionId = Uuid; + +#[derive(PartialEq, Clone)] +pub struct Secret(T); + +impl Secret { + pub const fn new(v: T) -> Self { + Self(v) + } + + pub fn expose_secret(&self) -> &T { + &self.0 + } +} + +impl<'de, T> Deserialize<'de> for Secret +where + T: Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let v = Deserialize::deserialize::(deserializer)?; + Ok(Self::new(v)) + } +} + +impl Serialize for Secret +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +impl Debug for Secret { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "") + } +} diff --git a/warpgate-db-entities/Cargo.toml b/warpgate-db-entities/Cargo.toml new file mode 100644 index 0000000..a858b75 --- /dev/null +++ b/warpgate-db-entities/Cargo.toml @@ -0,0 +1,12 @@ +[package] +edition = "2021" +license = "Apache-2.0" +name = "warpgate-db-entities" +version = "0.1.0" + +[dependencies] +chrono = {version = "0.4", features = ["serde"]} +poem-openapi = {version = "1.3", features = ["chrono", "uuid"]} +sea-orm = {version = "^0.6", features = ["macros", "with-chrono", "with-uuid"], default-features = false} +serde = "1.0" +uuid = {version = "0.8", features = ["v4", "serde"]} diff --git a/warpgate-db-entities/src/KnownHost.rs b/warpgate-db-entities/src/KnownHost.rs new file mode 100644 index 0000000..3008949 --- /dev/null +++ b/warpgate-db-entities/src/KnownHost.rs @@ -0,0 +1,21 @@ +use poem_openapi::Object; +use sea_orm::entity::prelude::*; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Object, Serialize)] +#[sea_orm(table_name = "known_hosts")] +#[oai(rename = "SSHKnownHost")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub host: String, + pub port: u16, + pub key_type: String, + pub key_base64: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation {} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/warpgate-db-entities/src/Recording.rs b/warpgate-db-entities/src/Recording.rs new file mode 100644 index 0000000..50f0e0f --- /dev/null +++ b/warpgate-db-entities/src/Recording.rs @@ -0,0 +1,63 @@ +use chrono::{DateTime, Utc}; +use poem_openapi::{Enum, Object}; +use sea_orm::entity::prelude::*; +use sea_orm::sea_query::ForeignKeyAction; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Debug, Clone, PartialEq, EnumIter, Enum, DeriveActiveEnum, Serialize)] +#[sea_orm(rs_type = "String", db_type = "String(Some(16))")] +pub enum RecordingKind { + #[sea_orm(string_value = "terminal")] + Terminal, + #[sea_orm(string_value = "traffic")] + Traffic, +} + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Object)] +#[sea_orm(table_name = "recordings")] +#[oai(rename = "Recording")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub started: DateTime, + pub ended: Option>, + pub session_id: Uuid, + pub kind: RecordingKind, +} + +// #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +// pub enum Relation { +// #[sea_orm( +// belongs_to = "super::Session::Entity", +// from = "Column::SessionId", +// to = "super::Session::Column::Id" +// )] +// Session, +// } + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Session, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Session => Entity::belongs_to(super::Session::Entity) + .from(Column::SessionId) + .to(super::Session::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Session.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/warpgate-db-entities/src/Session.rs b/warpgate-db-entities/src/Session.rs new file mode 100644 index 0000000..16f33be --- /dev/null +++ b/warpgate-db-entities/src/Session.rs @@ -0,0 +1,46 @@ +use chrono::{DateTime, Utc}; +use sea_orm::entity::prelude::*; +use uuid::Uuid; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel)] +#[sea_orm(table_name = "sessions")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub target_snapshot: Option, + pub username: Option, + pub remote_address: String, + pub started: DateTime, + pub ended: Option>, + pub ticket_id: Option, +} + +#[derive(Copy, Clone, Debug, EnumIter)] +pub enum Relation { + Recordings, + Ticket, +} + +impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Recordings => Entity::has_many(super::Recording::Entity) + .from(Column::Id) + .to(super::Recording::Column::SessionId) + .into(), + Self::Ticket => Entity::belongs_to(super::Ticket::Entity) + .from(Column::TicketId) + .to(super::Ticket::Column::Id) + .on_delete(ForeignKeyAction::SetNull) + .into(), + } + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Ticket.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/warpgate-db-entities/src/Ticket.rs b/warpgate-db-entities/src/Ticket.rs new file mode 100644 index 0000000..fe45ece --- /dev/null +++ b/warpgate-db-entities/src/Ticket.rs @@ -0,0 +1,28 @@ +use chrono::{DateTime, Utc}; +use poem_openapi::Object; +use sea_orm::entity::prelude::*; +use serde::Serialize; +use uuid::Uuid; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Serialize, Object)] +#[sea_orm(table_name = "tickets")] +#[oai(rename = "Ticket")] +pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + #[oai(skip)] + pub secret: String, + pub username: String, + pub target: String, + pub uses_left: Option, + pub expiry: Option>, + pub created: DateTime, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm(has_many = "super::Session::Entity")] + Sessions, +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/warpgate-db-entities/src/lib.rs b/warpgate-db-entities/src/lib.rs new file mode 100644 index 0000000..f8bb323 --- /dev/null +++ b/warpgate-db-entities/src/lib.rs @@ -0,0 +1,6 @@ +#![allow(non_snake_case)] + +pub mod KnownHost; +pub mod Recording; +pub mod Session; +pub mod Ticket; diff --git a/warpgate-db-migrations/Cargo.toml b/warpgate-db-migrations/Cargo.toml new file mode 100644 index 0000000..1338f6d --- /dev/null +++ b/warpgate-db-migrations/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "warpgate-db-migrations" +version = "0.1.0" +edition = "2021" +publish = false +license = "Apache-2.0" + +[lib] + +[dependencies] +sea-schema = { version = "0.5", default-features = false, features = [ "migration", "debug-print" ] } +uuid = {version = "0.8", features = ["v4", "serde"]} +chrono = "0.4" +sea-orm = {version = "^0.6", features = ["sqlx-sqlite", "runtime-tokio-native-tls", "macros"], default-features = false} diff --git a/warpgate-db-migrations/README.md b/warpgate-db-migrations/README.md new file mode 100644 index 0000000..963caae --- /dev/null +++ b/warpgate-db-migrations/README.md @@ -0,0 +1,37 @@ +# Running Migrator CLI + +- Apply all pending migrations + ```sh + cargo run + ``` + ```sh + cargo run -- up + ``` +- Apply first 10 pending migrations + ```sh + cargo run -- up -n 10 + ``` +- Rollback last applied migrations + ```sh + cargo run -- down + ``` +- Rollback last 10 applied migrations + ```sh + cargo run -- down -n 10 + ``` +- Drop all tables from the database, then reapply all migrations + ```sh + cargo run -- fresh + ``` +- Rollback all applied migrations, then reapply all migrations + ```sh + cargo run -- refresh + ``` +- Rollback all applied migrations + ```sh + cargo run -- reset + ``` +- Check the status of all migrations + ```sh + cargo run -- status + ``` diff --git a/warpgate-db-migrations/src/lib.rs b/warpgate-db-migrations/src/lib.rs new file mode 100644 index 0000000..b8935c3 --- /dev/null +++ b/warpgate-db-migrations/src/lib.rs @@ -0,0 +1,19 @@ +pub use sea_schema::migration::*; + +mod m00001_create_ticket; +mod m00002_create_session; +mod m00003_create_recording; +mod m00004_create_known_host; +pub struct Migrator; + +#[async_trait::async_trait] +impl MigratorTrait for Migrator { + fn migrations() -> Vec> { + vec![ + Box::new(m00001_create_ticket::Migration), + Box::new(m00002_create_session::Migration), + Box::new(m00003_create_recording::Migration), + Box::new(m00004_create_known_host::Migration), + ] + } +} diff --git a/warpgate-db-migrations/src/m00001_create_ticket.rs b/warpgate-db-migrations/src/m00001_create_ticket.rs new file mode 100644 index 0000000..218010f --- /dev/null +++ b/warpgate-db-migrations/src/m00001_create_ticket.rs @@ -0,0 +1,51 @@ +use sea_schema::migration::sea_orm::Schema; +use sea_schema::migration::sea_query::*; +use sea_schema::migration::*; + +pub mod ticket { + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "tickets")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub secret: String, + pub username: String, + pub target: String, + pub uses_left: Option, + pub expiry: Option, + pub created: DateTimeUtc, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m00001_create_ticket" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let builder = manager.get_database_backend(); + let schema = Schema::new(builder); + manager + .create_table(schema.create_table_from_entity(ticket::Entity)) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(ticket::Entity).to_owned()) + .await + } +} diff --git a/warpgate-db-migrations/src/m00002_create_session.rs b/warpgate-db-migrations/src/m00002_create_session.rs new file mode 100644 index 0000000..04a9062 --- /dev/null +++ b/warpgate-db-migrations/src/m00002_create_session.rs @@ -0,0 +1,72 @@ +use sea_schema::migration::sea_orm::Schema; +use sea_schema::migration::sea_query::*; +use sea_schema::migration::*; + +pub mod session { + use crate::m00001_create_ticket::ticket; + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "sessions")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub target_snapshot: Option, + pub username: Option, + pub remote_address: String, + pub started: DateTimeUtc, + pub ended: Option, + pub ticket_id: Option, + } + + #[derive(Copy, Clone, Debug, EnumIter)] + pub enum Relation { + Ticket, + } + + impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Ticket => Entity::belongs_to(ticket::Entity) + .from(Column::TicketId) + .to(ticket::Column::Id) + .on_delete(ForeignKeyAction::SetNull) + .into(), + } + } + } + + impl Related for Entity { + fn to() -> RelationDef { + Relation::Ticket.def() + } + } + + impl ActiveModelBehavior for ActiveModel {} +} + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m00002_create_session" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let builder = manager.get_database_backend(); + let schema = Schema::new(builder); + manager + .create_table(schema.create_table_from_entity(session::Entity)) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(session::Entity).to_owned()) + .await + } +} diff --git a/warpgate-db-migrations/src/m00003_create_recording.rs b/warpgate-db-migrations/src/m00003_create_recording.rs new file mode 100644 index 0000000..7e13ea3 --- /dev/null +++ b/warpgate-db-migrations/src/m00003_create_recording.rs @@ -0,0 +1,98 @@ +use sea_schema::migration::sea_orm::Schema; +use sea_schema::migration::sea_query::*; +use sea_schema::migration::*; + +pub mod recording { + use crate::m00002_create_session::session; + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + #[derive(Debug, Clone, PartialEq, EnumIter, DeriveActiveEnum)] + #[sea_orm(rs_type = "String", db_type = "String(Some(16))")] + pub enum RecordingKind { + #[sea_orm(string_value = "terminal")] + Terminal, + #[sea_orm(string_value = "traffic")] + Traffic, + } + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "recordings")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub name: String, + pub started: DateTimeUtc, + pub ended: Option, + pub session_id: Uuid, + pub kind: RecordingKind, + } + + #[derive(Copy, Clone, Debug, EnumIter)] + pub enum Relation { + Session, + } + + impl RelationTrait for Relation { + fn def(&self) -> RelationDef { + match self { + Self::Session => Entity::belongs_to(session::Entity) + .from(Column::SessionId) + .to(session::Column::Id) + .on_delete(ForeignKeyAction::Cascade) + .into(), + } + } + } + + impl Related for Entity { + fn to() -> RelationDef { + Relation::Session.def() + } + } + + impl ActiveModelBehavior for ActiveModel {} +} + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m00003_create_recording" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let builder = manager.get_database_backend(); + let schema = Schema::new(builder); + manager + .create_table(schema.create_table_from_entity(recording::Entity)) + .await?; + manager + .create_index( + Index::create() + .table(recording::Entity) + .name("recording__unique__session_id__name") + .unique() + .col(recording::Column::SessionId) + .col(recording::Column::Name) + .to_owned(), + ) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_index( + Index::drop() + .name("recording__unique__session_id__name") + .to_owned(), + ) + .await?; + manager + .drop_table(Table::drop().table(recording::Entity).to_owned()) + .await + } +} diff --git a/warpgate-db-migrations/src/m00004_create_known_host.rs b/warpgate-db-migrations/src/m00004_create_known_host.rs new file mode 100644 index 0000000..fbde1a0 --- /dev/null +++ b/warpgate-db-migrations/src/m00004_create_known_host.rs @@ -0,0 +1,49 @@ +use sea_schema::migration::sea_orm::Schema; +use sea_schema::migration::sea_query::*; +use sea_schema::migration::*; + +pub mod known_host { + use sea_orm::entity::prelude::*; + use uuid::Uuid; + + #[derive(Clone, Debug, PartialEq, DeriveEntityModel)] + #[sea_orm(table_name = "known_hosts")] + pub struct Model { + #[sea_orm(primary_key, auto_increment = false)] + pub id: Uuid, + pub host: String, + pub port: u16, + pub key_type: String, + pub key_base64: String, + } + + #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] + pub enum Relation {} + + impl ActiveModelBehavior for ActiveModel {} +} + +pub struct Migration; + +impl MigrationName for Migration { + fn name(&self) -> &str { + "m00004_create_known_host" + } +} + +#[async_trait::async_trait] +impl MigrationTrait for Migration { + async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { + let builder = manager.get_database_backend(); + let schema = Schema::new(builder); + manager + .create_table(schema.create_table_from_entity(known_host::Entity)) + .await + } + + async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { + manager + .drop_table(Table::drop().table(known_host::Entity).to_owned()) + .await + } +} diff --git a/warpgate-db-migrations/src/main.rs b/warpgate-db-migrations/src/main.rs new file mode 100644 index 0000000..cb9a47a --- /dev/null +++ b/warpgate-db-migrations/src/main.rs @@ -0,0 +1,7 @@ +use sea_schema::migration::*; +use warpgate_db_migrations::Migrator; + +#[async_std::main] +async fn main() { + cli::run_cli(Migrator).await; +} diff --git a/warpgate-protocol-ssh/Cargo.toml b/warpgate-protocol-ssh/Cargo.toml new file mode 100644 index 0000000..94ffec5 --- /dev/null +++ b/warpgate-protocol-ssh/Cargo.toml @@ -0,0 +1,24 @@ +[package] +edition = "2021" +license = "Apache-2.0" +name = "warpgate-protocol-ssh" +version = "0.1.0" + +[dependencies] +ansi_term = "0.12" +anyhow = "1.0" +async-trait = "0.1" +bytes = "1.1" +dialoguer = "0.10" +futures = "0.3" +russh = {version = "0.34.0-beta.2", features = ["openssl"] } +russh-keys = {version = "0.22.0-beta.1", features = ["openssl"] } +sea-orm = {version = "^0.6", features = ["runtime-tokio-native-tls"], default-features = false} +thiserror = "1.0" +time = "0.3" +tokio = {version = "1.17", features = ["tracing", "signal"]} +tracing = "0.1" +uuid = {version = "0.8", features = ["v4"]} +warpgate-common = {version = "*", path = "../warpgate-common"} +warpgate-db-entities = {version = "*", path = "../warpgate-db-entities"} +bimap = "0.6" diff --git a/warpgate-protocol-ssh/src/client/channel_direct_tcpip.rs b/warpgate-protocol-ssh/src/client/channel_direct_tcpip.rs new file mode 100644 index 0000000..073b38b --- /dev/null +++ b/warpgate-protocol-ssh/src/client/channel_direct_tcpip.rs @@ -0,0 +1,90 @@ +use anyhow::{Context, Result}; +use bytes::{Bytes, BytesMut}; +use russh::client::Channel; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tracing::*; +use uuid::Uuid; + +use crate::{ChannelOperation, RCEvent}; + +pub struct DirectTCPIPChannel { + client_channel: Channel, + channel_id: Uuid, + ops_rx: UnboundedReceiver, + events_tx: UnboundedSender, + session_tag: String, +} + +impl DirectTCPIPChannel { + pub fn new( + client_channel: Channel, + channel_id: Uuid, + ops_rx: UnboundedReceiver, + events_tx: UnboundedSender, + session_tag: String, + ) -> Self { + DirectTCPIPChannel { + client_channel, + channel_id, + ops_rx, + events_tx, + session_tag, + } + } + + pub async fn run(mut self) -> Result<()> { + loop { + tokio::select! { + incoming_data = self.ops_rx.recv() => { + match incoming_data { + Some(ChannelOperation::Data(data)) => { + self.client_channel.data(&*data).await.context("data")?; + } + Some(ChannelOperation::Eof) => { + self.client_channel.eof().await.context("eof")?; + }, + Some(ChannelOperation::Close) => break, + None => break, + Some(operation) => { + warn!(client_channel=%self.channel_id, ?operation, session=%self.session_tag, "unexpected client_channel operation"); + } + } + } + channel_event = self.client_channel.wait() => { + match channel_event { + Some(russh::ChannelMsg::Data { data }) => { + let bytes: &[u8] = &data; + self.events_tx.send(RCEvent::Output( + self.channel_id, + Bytes::from(BytesMut::from(bytes)), + ))?; + } + Some(russh::ChannelMsg::Close) => { + self.events_tx.send(RCEvent::Close(self.channel_id))?; + }, + Some(russh::ChannelMsg::Success) => { + self.events_tx.send(RCEvent::Success(self.channel_id))?; + }, + Some(russh::ChannelMsg::Eof) => { + self.events_tx.send(RCEvent::Eof(self.channel_id))?; + } + None => { + self.events_tx.send(RCEvent::Close(self.channel_id))?; + break + }, + Some(operation) => { + warn!(client_channel=%self.channel_id, ?operation, session=%self.session_tag, "unexpected client_channel operation"); + } + } + } + } + } + Ok(()) + } +} + +impl Drop for DirectTCPIPChannel { + fn drop(&mut self) { + info!(client_channel=%self.channel_id, session=%self.session_tag, "Closed"); + } +} diff --git a/warpgate-protocol-ssh/src/client/channel_session.rs b/warpgate-protocol-ssh/src/client/channel_session.rs new file mode 100644 index 0000000..79abc22 --- /dev/null +++ b/warpgate-protocol-ssh/src/client/channel_session.rs @@ -0,0 +1,154 @@ +use anyhow::{Context, Result}; +use bytes::{Bytes, BytesMut}; +use russh::client::Channel; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tracing::*; +use uuid::Uuid; + +use crate::{ChannelOperation, RCEvent}; + +pub struct SessionChannel { + client_channel: Channel, + channel_id: Uuid, + ops_rx: UnboundedReceiver, + events_tx: UnboundedSender, + session_tag: String, +} + +impl SessionChannel { + pub fn new( + client_channel: Channel, + channel_id: Uuid, + ops_rx: UnboundedReceiver, + events_tx: UnboundedSender, + session_tag: String, + ) -> Self { + SessionChannel { + client_channel, + channel_id, + ops_rx, + events_tx, + session_tag, + } + } + + pub async fn run(mut self) -> Result<()> { + loop { + tokio::select! { + incoming_data = self.ops_rx.recv() => { + match incoming_data { + Some(ChannelOperation::Data(data)) => { + self.client_channel.data(&*data).await.context("data")?; + } + Some(ChannelOperation::ExtendedData { ext, data }) => { + self.client_channel.extended_data(ext, &*data).await.context("extended data")?; + } + Some(ChannelOperation::RequestPty(request)) => { + self.client_channel.request_pty( + true, + &request.term, + request.col_width, + request.row_height, + request.pix_width, + request.pix_height, + &request.modes, + ).await.context("request_pty")?; + } + Some(ChannelOperation::ResizePty(request)) => { + self.client_channel.window_change( + request.col_width, + request.row_height, + request.pix_width, + request.pix_height, + ).await.context("resize_pty")?; + }, + Some(ChannelOperation::RequestShell) => { + self.client_channel.request_shell(true).await.context("request_shell")?; + }, + Some(ChannelOperation::RequestEnv(name, value)) => { + self.client_channel.set_env(true, name, value).await.context("request_env")?; + }, + Some(ChannelOperation::RequestExec(command)) => { + self.client_channel.exec(true, command).await.context("request_exec")?; + }, + Some(ChannelOperation::RequestSubsystem(name)) => { + self.client_channel.request_subsystem(true, &name).await.context("request_subsystem")?; + }, + Some(ChannelOperation::Eof) => { + self.client_channel.eof().await.context("eof")?; + }, + Some(ChannelOperation::Signal(signal)) => { + self.client_channel.signal(signal).await.context("signal")?; + }, + Some(ChannelOperation::OpenShell) => unreachable!(), + Some(ChannelOperation::OpenDirectTCPIP { .. }) => unreachable!(), + Some(ChannelOperation::OpenX11 { .. }) => unreachable!(), + Some(ChannelOperation::RequestX11(request)) => { + self.client_channel.request_x11( + true, + request.single_conection, + request.x11_auth_protocol, + request.x11_auth_cookie, + request.x11_screen_number, + ).await.context("data")?; + } + Some(ChannelOperation::Close) => break, + None => break, + } + } + channel_event = self.client_channel.wait() => { + match channel_event { + Some(russh::ChannelMsg::Data { data }) => { + let bytes: &[u8] = &data; + self.events_tx.send(RCEvent::Output( + self.channel_id, + Bytes::from(BytesMut::from(bytes)), + ))?; + } + Some(russh::ChannelMsg::Close) => { + self.events_tx.send(RCEvent::Close(self.channel_id))?; + }, + Some(russh::ChannelMsg::Success) => { + self.events_tx.send(RCEvent::Success(self.channel_id))?; + }, + Some(russh::ChannelMsg::Eof) => { + self.events_tx.send(RCEvent::Eof(self.channel_id))?; + } + Some(russh::ChannelMsg::ExitStatus { exit_status }) => { + self.events_tx.send(RCEvent::ExitStatus(self.channel_id, exit_status))?; + } + Some(russh::ChannelMsg::WindowAdjusted { .. }) => { }, + Some(russh::ChannelMsg::ExitSignal { + core_dumped, error_message, lang_tag, signal_name + }) => { + self.events_tx.send(RCEvent::ExitSignal { + channel: self.channel_id, core_dumped, error_message, lang_tag, signal_name + })?; + }, + Some(russh::ChannelMsg::XonXoff { client_can_do: _ }) => { + } + Some(russh::ChannelMsg::ExtendedData { data, ext }) => { + let data: &[u8] = &data; + self.events_tx.send(RCEvent::ExtendedData { + channel: self.channel_id, + data: Bytes::from(BytesMut::from(data)), + ext, + })?; + } + None => { + self.events_tx.send(RCEvent::Close(self.channel_id))?; + break + }, + } + } + } + } + Ok::<(), anyhow::Error>(()) + } +} + +impl Drop for SessionChannel { + fn drop(&mut self) { + info!(channel=%self.channel_id, session=%self.session_tag, "Closed"); + } +} diff --git a/warpgate-protocol-ssh/src/client/handler.rs b/warpgate-protocol-ssh/src/client/handler.rs new file mode 100644 index 0000000..faebd79 --- /dev/null +++ b/warpgate-protocol-ssh/src/client/handler.rs @@ -0,0 +1,133 @@ +use std::pin::Pin; + +use crate::known_hosts::{KnownHostValidationResult, KnownHosts}; +use crate::ConnectionError; +use futures::FutureExt; +use russh::client::Session; + +use russh_keys::key::PublicKey; +use russh_keys::PublicKeyBase64; +use tokio::sync::mpsc::UnboundedSender; +use tokio::sync::oneshot; +use tracing::*; +use warpgate_common::{Services, TargetSSHOptions}; + +#[derive(Debug)] +pub enum ClientHandlerEvent { + HostKeyReceived(PublicKey), + HostKeyUnknown(PublicKey, oneshot::Sender), + // ForwardedTCPIP(ChannelId, DirectTCPIPParams), + Disconnect, +} + +pub struct ClientHandler { + pub ssh_options: TargetSSHOptions, + pub event_tx: UnboundedSender, + pub services: Services, + pub session_tag: String, +} + +#[derive(Debug, thiserror::Error)] +pub enum ClientHandlerError { + #[error("Connection error")] + ConnectionError(ConnectionError), + + #[error("SSH")] + Ssh(#[from] russh::Error), + + #[error("Internal error")] + Internal, +} + +impl russh::client::Handler for ClientHandler { + type Error = ClientHandlerError; + type FutureUnit = Pin< + Box> + Send>, + >; + type FutureBool = Pin< + Box> + Send>, + >; + + fn finished_bool(self, b: bool) -> Self::FutureBool { + async move { Ok((self, b)) }.boxed() + } + + fn finished(self, session: Session) -> Self::FutureUnit { + async move { Ok((self, session)) }.boxed() + } + + fn check_server_key(self, server_public_key: &PublicKey) -> Self::FutureBool { + let mut known_hosts = KnownHosts::new(&self.services.db); + let server_public_key = server_public_key.clone(); + async move { + self.event_tx + .send(ClientHandlerEvent::HostKeyReceived( + server_public_key.clone(), + )) + .map_err(|_| ClientHandlerError::ConnectionError(ConnectionError::Internal))?; + match known_hosts + .validate( + &self.ssh_options.host, + self.ssh_options.port, + &server_public_key, + ) + .await + { + Ok(KnownHostValidationResult::Valid) => Ok((self, true)), + Ok(KnownHostValidationResult::Invalid { + key_type, + key_base64, + }) => { + warn!(session=%self.session_tag, "Host key is invalid!"); + return Err(ClientHandlerError::ConnectionError( + ConnectionError::HostKeyMismatch { + received_key_type: server_public_key.name().to_owned(), + received_key_base64: server_public_key.public_key_base64(), + known_key_type: key_type, + known_key_base64: key_base64, + }, + )); + } + Ok(KnownHostValidationResult::Unknown) => { + warn!(session=%self.session_tag, "Host key is unknown"); + + let (tx, rx) = oneshot::channel(); + self.event_tx + .send(ClientHandlerEvent::HostKeyUnknown( + server_public_key.clone(), + tx, + )) + .map_err(|_| ClientHandlerError::Internal)?; + let accepted = rx.await.map_err(|_| ClientHandlerError::Internal)?; + if accepted { + if let Err(error) = known_hosts + .trust( + &self.ssh_options.host, + self.ssh_options.port, + &server_public_key, + ) + .await + { + error!(?error, session=%self.session_tag, "Failed to save host key"); + } + Ok((self, true)) + } else { + Ok((self, false)) + } + } + Err(error) => { + error!(?error, session=%self.session_tag, "Failed to verify the host key"); + Err(ClientHandlerError::Internal) + } + } + } + .boxed() + } +} + +impl Drop for ClientHandler { + fn drop(&mut self) { + let _ = self.event_tx.send(ClientHandlerEvent::Disconnect); + debug!(session=%self.session_tag, "Dropped"); + } +} diff --git a/warpgate-protocol-ssh/src/client/mod.rs b/warpgate-protocol-ssh/src/client/mod.rs new file mode 100644 index 0000000..6169cc8 --- /dev/null +++ b/warpgate-protocol-ssh/src/client/mod.rs @@ -0,0 +1,515 @@ +mod channel_direct_tcpip; +mod channel_session; +mod handler; +use self::handler::ClientHandlerEvent; +use super::{ChannelOperation, DirectTCPIPParams}; +use crate::client::handler::ClientHandlerError; +use crate::helpers::PublicKeyAsOpenSSH; +use crate::keys::load_client_keys; +use anyhow::{Context, Result}; +use bytes::Bytes; +use channel_direct_tcpip::DirectTCPIPChannel; +use channel_session::SessionChannel; +use futures::pin_mut; +use handler::ClientHandler; +use russh::client::Handle; +use russh::Sig; +use russh_keys::key::PublicKey; +use std::collections::HashMap; +use std::net::ToSocketAddrs; +use std::sync::Arc; +use tokio::sync::mpsc::error::SendError; +use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}; +use tokio::sync::{oneshot, Mutex}; +use tokio::task::JoinHandle; +use tracing::*; +use uuid::Uuid; +use warpgate_common::{SSHTargetAuth, Services, SessionId, TargetSSHOptions}; + +#[derive(Debug, thiserror::Error)] +pub enum ConnectionError { + #[error("Host key mismatch")] + HostKeyMismatch { + received_key_type: String, + received_key_base64: String, + known_key_type: String, + known_key_base64: String, + }, + + #[error(transparent)] + Io(#[from] std::io::Error), + + #[error(transparent)] + Key(#[from] russh_keys::Error), + + #[error(transparent)] + SSH(#[from] russh::Error), + + #[error("Could not resolve address")] + Resolve, + + #[error("Internal error")] + Internal, + + #[error("Aborted")] + Aborted, + + #[error("Authentication failed")] + Authentication, +} + +#[derive(Debug)] +pub enum RCEvent { + State(RCState), + Output(Uuid, Bytes), + Success(Uuid), + Eof(Uuid), + Close(Uuid), + ExitStatus(Uuid, u32), + ExitSignal { + channel: Uuid, + signal_name: Sig, + core_dumped: bool, + error_message: String, + lang_tag: String, + }, + ExtendedData { + channel: Uuid, + data: Bytes, + ext: u32, + }, + ConnectionError(ConnectionError), + HostKeyReceived(PublicKey), + HostKeyUnknown(PublicKey, oneshot::Sender), + // ForwardedTCPIP(Uuid, DirectTCPIPParams), + Done, +} + +#[derive(Clone, Debug)] +pub enum RCCommand { + Connect(TargetSSHOptions), + Channel(Uuid, ChannelOperation), + // ForwardTCPIP(String, u32), + // CancelTCPIPForward(String, u32), + Disconnect, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum RCState { + NotInitialized, + Connecting, + Connected, + Disconnected, +} + +#[derive(Debug)] +enum InnerEvent { + RCCommand(RCCommand), + ClientHandlerEvent(ClientHandlerEvent), +} + +pub struct RemoteClient { + id: SessionId, + tx: UnboundedSender, + session: Option>>>, + channel_pipes: Arc>>>, + pending_ops: Vec<(Uuid, ChannelOperation)>, + state: RCState, + abort_rx: UnboundedReceiver<()>, + inner_event_rx: UnboundedReceiver, + inner_event_tx: UnboundedSender, + child_tasks: Vec>>, + services: Services, + session_tag: String, +} + +pub struct RemoteClientHandles { + pub event_rx: UnboundedReceiver, + pub command_tx: UnboundedSender, + pub abort_tx: UnboundedSender<()>, +} + +impl RemoteClient { + pub fn create(id: SessionId, session_tag: String, services: Services) -> RemoteClientHandles { + let (event_tx, event_rx) = unbounded_channel(); + let (command_tx, mut command_rx) = unbounded_channel(); + let (abort_tx, abort_rx) = unbounded_channel(); + + let (inner_event_tx, inner_event_rx) = unbounded_channel(); + + let this = Self { + id, + tx: event_tx, + session: None, + channel_pipes: Arc::new(Mutex::new(HashMap::new())), + pending_ops: vec![], + state: RCState::NotInitialized, + inner_event_rx, + inner_event_tx: inner_event_tx.clone(), + child_tasks: vec![], + session_tag, + services, + abort_rx, + }; + + tokio::spawn({ + async move { + while let Some(e) = command_rx.recv().await { + inner_event_tx.send(InnerEvent::RCCommand(e))? + } + Ok::<(), anyhow::Error>(()) + } + }); + + this.start(); + + RemoteClientHandles { + event_rx, + command_tx, + abort_tx, + } + } + + fn set_disconnected(&mut self) { + self.session = None; + for (id, op) in self.pending_ops.drain(..) { + if let ChannelOperation::OpenShell = op { + let _ = self.tx.send(RCEvent::Close(id)); + } + if let ChannelOperation::OpenDirectTCPIP { .. } = op { + let _ = self.tx.send(RCEvent::Close(id)); + } + } + let _ = self.set_state(RCState::Disconnected); + let _ = self.tx.send(RCEvent::Done); + } + + fn set_state(&mut self, state: RCState) -> Result<()> { + self.state = state.clone(); + self.tx.send(RCEvent::State(state))?; + Ok(()) + } + + // fn map_channel(&self, ch: &ChannelId) -> Result { + // self.channel_map + // .get_by_left(ch) + // .cloned() + // .ok_or_else(|| anyhow::anyhow!("Channel not known")) + // } + + // fn map_channel_reverse(&self, ch: &Uuid) -> Result { + // self.channel_map + // .get_by_right(ch) + // .cloned() + // .ok_or_else(|| anyhow::anyhow!("Channel not known")) + // } + + async fn apply_channel_op(&mut self, channel_id: Uuid, op: ChannelOperation) -> Result<()> { + if self.state != RCState::Connected { + self.pending_ops.push((channel_id, op)); + return Ok(()); + } + + match op { + ChannelOperation::OpenShell => { + self.open_shell(channel_id) + .await + .context("failed to open shell")?; + } + ChannelOperation::OpenDirectTCPIP(params) => { + self.open_direct_tcpip(channel_id, params) + .await + .context("failed to open direct tcp/ip channel")?; + } + op => { + let mut channel_pipes = self.channel_pipes.lock().await; + match channel_pipes.get(&channel_id) { + Some(tx) => match tx.send(op) { + Ok(_) => {} + Err(SendError(_)) => { + channel_pipes.remove(&channel_id); + } + }, + None => { + debug!(channel=%channel_id, session=%self.session_tag, "operation for unknown channel") + } + } + } + } + Ok(()) + } + + pub fn start(mut self) { + let name = format!("SSH {} client commands", self.id); + tokio::task::Builder::new().name(&name).spawn(async move { + async { + loop { + tokio::select! { + Some(event) = self.inner_event_rx.recv() => { + match event { + InnerEvent::RCCommand(cmd) => { + match cmd { + RCCommand::Connect(options) => match self.connect(options).await { + Ok(_) => { + self.set_state(RCState::Connected)?; + let ops = self.pending_ops.drain(..).collect::>(); + for (id, op) in ops { + self.apply_channel_op(id, op).await?; + } + // let forwards = self.pending_forwards.drain(..).collect::>(); + // for (address, port) in forwards { + // self.tcpip_forward(address, port).await?; + // } + } + Err(e) => { + debug!(session=%self.session_tag, "Connect error: {}", e); + let _ = self.tx.send(RCEvent::ConnectionError(e)); + self.set_disconnected(); + break + } + }, + RCCommand::Channel(ch, op) => { + self.apply_channel_op(ch, op).await?; + } + RCCommand::Disconnect => { + self.disconnect().await?; + break + } + } + } + InnerEvent::ClientHandlerEvent(client_event) => { + debug!(session=%self.session_tag, "Client handler event: {:?}", client_event); + match client_event { + ClientHandlerEvent::Disconnect => { + self._on_disconnect().await?; + } + event => { + error!(session=%self.session_tag, ?event, "Unhandled client handler event"); + }, + } + } + } + } + Some(_) = self.abort_rx.recv() => { + debug!(session=%self.session_tag, "Abort requested"); + self.disconnect().await?; + break + } + }; + } + Ok::<(), anyhow::Error>(()) + } + .await + .map_err(|error| { + error!(?error, session=%self.session_tag, "error in command loop"); + anyhow::anyhow!("Error in command loop: {error}") + })?; + debug!(session=%self.session_tag, "No more commmands"); + Ok::<(), anyhow::Error>(()) + }); + } + + async fn connect(&mut self, ssh_options: TargetSSHOptions) -> Result<(), ConnectionError> { + let address_str = format!("{}:{}", ssh_options.host, ssh_options.port); + let address = match address_str + .to_socket_addrs() + .map_err(ConnectionError::Io) + .and_then(|mut x| x.next().ok_or(ConnectionError::Resolve)) + { + Ok(address) => address, + Err(error) => { + error!(?error, "Cannot resolve target address"); + self.set_disconnected(); + return Err(error); + } + }; + + info!(?address, username=?ssh_options.username, session=%self.session_tag, "Connecting"); + let config = russh::client::Config { + ..Default::default() + }; + let config = Arc::new(config); + + let (event_tx, mut event_rx) = unbounded_channel(); + let handler = ClientHandler { + ssh_options: ssh_options.clone(), + event_tx, + services: self.services.clone(), + session_tag: self.session_tag.clone(), + }; + + let fut_connect = russh::client::connect(config, address, handler); + pin_mut!(fut_connect); + + loop { + tokio::select! { + Some(event) = event_rx.recv() => { + match event { + ClientHandlerEvent::HostKeyReceived(key) => { + self.tx.send(RCEvent::HostKeyReceived(key)).map_err(|_| ConnectionError::Internal)?; + } + ClientHandlerEvent::HostKeyUnknown(key, reply) => { + self.tx.send(RCEvent::HostKeyUnknown(key, reply)).map_err(|_| ConnectionError::Internal)?; + } + _ => {} + } + } + Some(_) = self.abort_rx.recv() => { + info!(session=%self.session_tag, "Abort requested"); + self.set_disconnected(); + return Err(ConnectionError::Aborted) + } + session = &mut fut_connect => { + if let Err(error) = session { + let connection_error = match error { + ClientHandlerError::ConnectionError(e) => e, + ClientHandlerError::Ssh(e) => ConnectionError::SSH(e), + ClientHandlerError::Internal => ConnectionError::Internal, + }; + error!(error=?connection_error, session=%self.session_tag, "Connection error"); + return Err(connection_error); + } + + #[allow(clippy::unwrap_used)] + let mut session = session.unwrap(); + + let mut auth_result = false; + match ssh_options.auth { + SSHTargetAuth::Password { password } => { + auth_result = session + .authenticate_password(ssh_options.username, password.expose_secret()) + .await?; + if auth_result { + debug!(session=%self.session_tag, "Authenticated with password"); + } + } + SSHTargetAuth::PublicKey => { + let keys = load_client_keys(&*self.services.config.lock().await)?; + for key in keys.into_iter() { + let key_str = key.as_openssh(); + auth_result = session + .authenticate_publickey(ssh_options.username.clone(), Arc::new(key)) + .await?; + if auth_result { + debug!(session=%self.session_tag, key=%key_str, "Authenticated with key"); + break; + } + } + } + } + + if !auth_result { + error!(session=%self.session_tag, "Auth rejected"); + let _ = session + .disconnect(russh::Disconnect::ByApplication, "", "") + .await; + return Err(ConnectionError::Authentication); + } + + self.session = Some(Arc::new(Mutex::new(session))); + + info!(?address, session=%self.session_tag, "Connected"); + + tokio::spawn({ + let inner_event_tx = self.inner_event_tx.clone(); + async move { + while let Some(e) = event_rx.recv().await { + info!("{:?}", e); + inner_event_tx.send(InnerEvent::ClientHandlerEvent(e))? + } + Ok::<(), anyhow::Error>(()) + } + }); + + return Ok(()) + } + } + } + } + + async fn open_shell(&mut self, channel_id: Uuid) -> Result<()> { + if let Some(session) = &self.session { + let mut session = session.lock().await; + let channel = session.channel_open_session().await?; + + let (tx, rx) = unbounded_channel(); + self.channel_pipes.lock().await.insert(channel_id, tx); + + let channel = SessionChannel::new( + channel, + channel_id, + rx, + self.tx.clone(), + self.session_tag.clone(), + ); + self.child_tasks.push( + tokio::task::Builder::new() + .name(&format!("SSH {} {:?} ops", self.id, channel_id)) + .spawn(channel.run()), + ); + } + Ok(()) + } + + async fn open_direct_tcpip( + &mut self, + channel_id: Uuid, + params: DirectTCPIPParams, + ) -> Result<()> { + if let Some(session) = &self.session { + let mut session = session.lock().await; + let channel = session + .channel_open_direct_tcpip( + params.host_to_connect, + params.port_to_connect, + params.originator_address, + params.originator_port, + ) + .await?; + + let (tx, rx) = unbounded_channel(); + self.channel_pipes.lock().await.insert(channel_id, tx); + + let channel = DirectTCPIPChannel::new( + channel, + channel_id, + rx, + self.tx.clone(), + self.session_tag.clone(), + ); + self.child_tasks.push( + tokio::task::Builder::new() + .name(&format!("SSH {} {:?} ops", self.id, channel_id)) + .spawn(channel.run()), + ); + } + Ok(()) + } + + async fn disconnect(&mut self) -> Result<()> { + if let Some(session) = &mut self.session { + let _ = session + .lock() + .await + .disconnect(russh::Disconnect::ByApplication, "", "") + .await; + self.set_disconnected(); + } + Ok(()) + } + + async fn _on_disconnect(&mut self) -> Result<()> { + self.set_disconnected(); + Ok(()) + } +} + +impl Drop for RemoteClient { + fn drop(&mut self) { + for task in self.child_tasks.drain(..) { + let _ = task.abort(); + } + info!(session=%self.session_tag, "Closed connection"); + debug!(session=%self.session_tag, "Dropped"); + } +} diff --git a/warpgate-protocol-ssh/src/common.rs b/warpgate-protocol-ssh/src/common.rs new file mode 100644 index 0000000..b28560b --- /dev/null +++ b/warpgate-protocol-ssh/src/common.rs @@ -0,0 +1,58 @@ +use std::fmt::{Display, Formatter}; + +use bytes::Bytes; +use russh::{ChannelId, Pty, Sig}; + +#[derive(Clone, Debug)] +pub struct PtyRequest { + pub term: String, + pub col_width: u32, + pub row_height: u32, + pub pix_width: u32, + pub pix_height: u32, + pub modes: Vec<(Pty, u32)>, +} + +#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] +pub struct ServerChannelId(pub ChannelId); + +impl Display for ServerChannelId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Clone, Debug)] +pub struct DirectTCPIPParams { + pub host_to_connect: String, + pub port_to_connect: u32, + pub originator_address: String, + pub originator_port: u32, +} + +#[derive(Clone, Debug)] +pub struct X11Request { + pub single_conection: bool, + pub x11_auth_protocol: String, + pub x11_auth_cookie: String, + pub x11_screen_number: u32, +} + +#[derive(Clone, Debug)] +pub enum ChannelOperation { + OpenShell, + OpenDirectTCPIP(DirectTCPIPParams), + OpenX11(String, u32), + RequestPty(PtyRequest), + ResizePty(PtyRequest), + RequestShell, + RequestEnv(String, String), + RequestExec(String), + RequestX11(X11Request), + RequestSubsystem(String), + Data(Bytes), + ExtendedData { data: Bytes, ext: u32 }, + Close, + Eof, + Signal(Sig), +} diff --git a/warpgate-protocol-ssh/src/compat.rs b/warpgate-protocol-ssh/src/compat.rs new file mode 100644 index 0000000..f27e11e --- /dev/null +++ b/warpgate-protocol-ssh/src/compat.rs @@ -0,0 +1,14 @@ +use std::fmt::Display; + +pub trait ContextExt { + fn context(self, context: C) -> anyhow::Result; +} + +impl ContextExt for Result +where + C: Display + Send + Sync + 'static, +{ + fn context(self, context: C) -> anyhow::Result { + self.map_err(|_| anyhow::anyhow!("unspecified error").context(context)) + } +} diff --git a/warpgate-protocol-ssh/src/helpers.rs b/warpgate-protocol-ssh/src/helpers.rs new file mode 100644 index 0000000..0104098 --- /dev/null +++ b/warpgate-protocol-ssh/src/helpers.rs @@ -0,0 +1,16 @@ +use russh_keys::key::KeyPair; +use russh_keys::PublicKeyBase64; + +pub trait PublicKeyAsOpenSSH { + fn as_openssh(&self) -> String; +} + +impl PublicKeyAsOpenSSH for KeyPair { + fn as_openssh(&self) -> String { + let mut buf = String::new(); + buf.push_str(self.name()); + buf.push(' '); + buf.push_str(&self.public_key_base64().replace("\r\n", "")); + buf + } +} diff --git a/warpgate-protocol-ssh/src/keys.rs b/warpgate-protocol-ssh/src/keys.rs new file mode 100644 index 0000000..811cf37 --- /dev/null +++ b/warpgate-protocol-ssh/src/keys.rs @@ -0,0 +1,88 @@ +use anyhow::Result; +use russh_keys::key::{KeyPair, SignatureHash}; +use russh_keys::{encode_pkcs8_pem, load_secret_key}; +use std::fs::{create_dir_all, File}; +use std::path::PathBuf; +use tracing::*; +use warpgate_common::helpers::fs::secure_directory; +use warpgate_common::WarpgateConfig; + +fn get_keys_path(config: &WarpgateConfig) -> PathBuf { + let mut path = config.paths_relative_to.clone(); + path.push(&config.store.ssh.keys); + path +} + +pub fn generate_host_keys(config: &WarpgateConfig) -> Result<()> { + let path = get_keys_path(config); + create_dir_all(&path)?; + secure_directory(&path)?; + + let key_path = path.join("host-ed25519"); + if !key_path.exists() { + info!("Generating Ed25519 host key"); + let key = KeyPair::generate_ed25519().unwrap(); + let f = File::create(key_path)?; + encode_pkcs8_pem(&key, f)?; + } + + let key_path = path.join("host-rsa"); + if !key_path.exists() { + info!("Generating RSA host key"); + let key = KeyPair::generate_rsa(4096, SignatureHash::SHA2_512).unwrap(); + let f = File::create(key_path)?; + encode_pkcs8_pem(&key, f)?; + } + + Ok(()) +} + +pub fn load_host_keys(config: &WarpgateConfig) -> Result, russh_keys::Error> { + let path = get_keys_path(config); + let mut keys = Vec::new(); + + let key_path = path.join("host-ed25519"); + keys.push(load_secret_key(key_path, None)?); + + let key_path = path.join("host-rsa"); + keys.push(load_secret_key(key_path, None)?); + + Ok(keys) +} + +pub fn generate_client_keys(config: &WarpgateConfig) -> Result<()> { + let path = get_keys_path(config); + create_dir_all(&path)?; + secure_directory(&path)?; + + let key_path = path.join("client-ed25519"); + if !key_path.exists() { + info!("Generating Ed25519 client key"); + let key = KeyPair::generate_ed25519().unwrap(); + let f = File::create(key_path)?; + encode_pkcs8_pem(&key, f)?; + } + + let key_path = path.join("client-rsa"); + if !key_path.exists() { + info!("Generating RSA client key"); + let key = KeyPair::generate_rsa(4096, SignatureHash::SHA2_512).unwrap(); + let f = File::create(key_path)?; + encode_pkcs8_pem(&key, f)?; + } + + Ok(()) +} + +pub fn load_client_keys(config: &WarpgateConfig) -> Result, russh_keys::Error> { + let path = get_keys_path(config); + let mut keys = Vec::new(); + + let key_path = path.join("client-ed25519"); + keys.push(load_secret_key(key_path, None)?); + + let key_path = path.join("client-rsa"); + keys.push(load_secret_key(key_path, None)?); + + Ok(keys) +} diff --git a/warpgate-protocol-ssh/src/known_hosts.rs b/warpgate-protocol-ssh/src/known_hosts.rs new file mode 100644 index 0000000..129143a --- /dev/null +++ b/warpgate-protocol-ssh/src/known_hosts.rs @@ -0,0 +1,76 @@ +use std::sync::Arc; + +use russh_keys::key::PublicKey; +use russh_keys::PublicKeyBase64; +use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; +use tokio::sync::Mutex; +use uuid::Uuid; +use warpgate_db_entities::KnownHost; + +pub struct KnownHosts { + db: Arc>, +} + +pub enum KnownHostValidationResult { + Valid, + Invalid { + key_type: String, + key_base64: String, + }, + Unknown, +} + +impl KnownHosts { + pub fn new(db: &Arc>) -> Self { + Self { db: db.clone() } + } + + pub async fn validate( + &mut self, + host: &str, + port: u16, + key: &PublicKey, + ) -> Result { + let db = self.db.lock().await; + let entries = KnownHost::Entity::find() + .filter(KnownHost::Column::Host.eq(host)) + .filter(KnownHost::Column::Port.eq(port)) + .filter(KnownHost::Column::KeyType.eq(key.name())) + .all(&*db) + .await?; + + let key_base64 = key.public_key_base64(); + if entries.iter().any(|x| x.key_base64 == key_base64) { + return Ok(KnownHostValidationResult::Valid); + } + if let Some(first) = entries.first() { + return Ok(KnownHostValidationResult::Invalid { + key_type: first.key_type.clone(), + key_base64: first.key_base64.clone(), + }); + } + Ok(KnownHostValidationResult::Unknown) + } + + pub async fn trust( + &mut self, + host: &str, + port: u16, + key: &PublicKey, + ) -> Result<(), sea_orm::DbErr> { + use sea_orm::ActiveValue::Set; + + let values = KnownHost::ActiveModel { + id: Set(Uuid::new_v4()), + host: Set(host.to_owned()), + port: Set(port), + key_type: Set(key.name().to_owned()), + key_base64: Set(key.public_key_base64()), + }; + + let db = self.db.lock().await; + values.insert(&*db).await?; + + Ok(()) + } +} diff --git a/warpgate-protocol-ssh/src/lib.rs b/warpgate-protocol-ssh/src/lib.rs new file mode 100644 index 0000000..48aa4ad --- /dev/null +++ b/warpgate-protocol-ssh/src/lib.rs @@ -0,0 +1,112 @@ +#![feature(type_alias_impl_trait, let_else)] +mod client; +mod common; +mod compat; +pub mod helpers; +mod keys; +mod known_hosts; +mod server; + +use crate::client::{RCCommand, RemoteClient}; +use anyhow::Result; +use async_trait::async_trait; +pub use client::*; +pub use common::*; +pub use keys::*; +use russh_keys::PublicKeyBase64; +pub use server::run_server; +use std::fmt::Debug; +use std::net::SocketAddr; +use uuid::Uuid; +use warpgate_common::{ProtocolServer, Services, Target, TargetTestError}; + +#[derive(Clone)] +pub struct SSHProtocolServer { + services: Services, +} + +impl SSHProtocolServer { + pub async fn new(services: &Services) -> Result { + let config = services.config.lock().await; + generate_host_keys(&config)?; + generate_client_keys(&config)?; + Ok(SSHProtocolServer { + services: services.clone(), + }) + } +} + +#[async_trait] +impl ProtocolServer for SSHProtocolServer { + async fn run(self, address: SocketAddr) -> Result<()> { + run_server(self.services, address).await + } + + async fn test_target(self, target: Target) -> Result<(), TargetTestError> { + let Some(ssh_options) = target.ssh else { + return Err(TargetTestError::Misconfigured("Not an SSH target".to_owned())); + }; + + let mut handles = + RemoteClient::create(Uuid::new_v4(), "test".to_owned(), self.services.clone()); + + let _ = handles.command_tx.send(RCCommand::Connect(ssh_options)); + + while let Some(event) = handles.event_rx.recv().await { + match event { + RCEvent::ConnectionError(err) => { + if let ConnectionError::HostKeyMismatch { + ref received_key_type, + ref received_key_base64, + ref known_key_type, + ref known_key_base64, + } = err + { + println!("\n"); + println!("Stored key ({}): {}", known_key_type, known_key_base64); + println!( + "Received key ({}): {}", + received_key_type, received_key_base64 + ); + println!("Host key doesn't match the stored one."); + println!("If you know that the key is correct (e.g. it has been changed),"); + println!("you can remove the old key in the Warpgate management UI and try again"); + } + return Err(TargetTestError::ConnectionError(format!("{:?}", err))); + } + RCEvent::HostKeyUnknown(key, reply) => { + println!("\nHost key ({}): {}", key.name(), key.public_key_base64()); + println!("There is no trusted {} key for this host.", key.name()); + if dialoguer::Confirm::new() + .with_prompt("Trust this key?") + .interact()? + { + let _ = reply.send(true); + } else { + let _ = reply.send(false); + } + } + RCEvent::State(state) => match state { + RCState::Connected => { + return Ok(()); + } + RCState::Disconnected => { + return Err(TargetTestError::ConnectionError( + "Connection failed".to_owned(), + )); + } + _ => {} + }, + _ => {} + } + } + + Ok(()) + } +} + +impl Debug for SSHProtocolServer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "SSHProtocolServer") + } +} diff --git a/warpgate-protocol-ssh/src/server/mod.rs b/warpgate-protocol-ssh/src/server/mod.rs new file mode 100644 index 0000000..c4646e2 --- /dev/null +++ b/warpgate-protocol-ssh/src/server/mod.rs @@ -0,0 +1,83 @@ +mod russh_handler; +mod service_output; +mod session; +mod session_handle; +use crate::keys::load_host_keys; +use crate::server::session_handle::SSHSessionHandle; +use anyhow::Result; +use russh::MethodSet; +pub use russh_handler::ServerHandler; +pub use session::ServerSession; +use std::fmt::Debug; +use std::net::SocketAddr; +use std::sync::Arc; +use tokio::io::{AsyncRead, AsyncWrite}; +use tokio::net::TcpListener; +use tokio::sync::Mutex; +use tracing::*; +use warpgate_common::{Services, SessionState}; + +pub async fn run_server(services: Services, address: SocketAddr) -> Result<()> { + let russh_config = { + let config = services.config.lock().await; + russh::server::Config { + auth_rejection_time: std::time::Duration::from_secs(1), + methods: MethodSet::PUBLICKEY | MethodSet::PASSWORD, + keys: load_host_keys(&config)?, + ..Default::default() + } + }; + + let russh_config = Arc::new(russh_config); + + let socket = TcpListener::bind(&address).await?; + info!(?address, "Listening"); + while let Ok((socket, remote_address)) = socket.accept().await { + let russh_config = russh_config.clone(); + + let (session_handle, session_handle_rx) = SSHSessionHandle::new(); + let session_state = Arc::new(Mutex::new(SessionState::new( + remote_address, + Box::new(session_handle), + ))); + + let server_handle = services + .state + .lock() + .await + .register_session(&session_state) + .await?; + + let id = server_handle.id(); + + let session = + match ServerSession::new(remote_address, &services, server_handle, session_handle_rx) + .await + { + Ok(session) => session, + Err(error) => { + error!(%error, "Error setting up session"); + continue; + } + }; + + let handler = ServerHandler { id, session }; + + tokio::task::Builder::new() + .name(&format!("SSH {id} protocol")) + .spawn(_run_stream(russh_config, socket, handler)); + } + Ok(()) +} + +async fn _run_stream( + config: Arc, + socket: R, + handler: ServerHandler, +) -> Result<()> +where + R: AsyncRead + AsyncWrite + Unpin + Debug, +{ + russh::server::run_stream(config, socket, handler).await?; + Ok(()) +} diff --git a/warpgate-protocol-ssh/src/server/russh_handler.rs b/warpgate-protocol-ssh/src/server/russh_handler.rs new file mode 100644 index 0000000..2999635 --- /dev/null +++ b/warpgate-protocol-ssh/src/server/russh_handler.rs @@ -0,0 +1,381 @@ +use std::fmt::Debug; +use std::pin::Pin; +use std::sync::Arc; + +use bytes::BytesMut; +use futures::FutureExt; +use russh::server::{Auth, Session}; +use russh::{ChannelId, Pty}; +use tokio::sync::Mutex; +use tracing::*; +use warpgate_common::{Secret, SessionId}; + +use super::session::ServerSession; +use crate::common::{PtyRequest, ServerChannelId}; +use crate::{DirectTCPIPParams, X11Request}; + +pub struct ServerHandler { + pub id: SessionId, + pub session: Arc>, +} + +impl russh::server::Handler for ServerHandler { + type Error = anyhow::Error; + type FutureAuth = + Pin> + Send>>; + type FutureUnit = + Pin> + Send>>; + type FutureBool = + Pin> + Send>>; + + fn finished_auth(self, auth: Auth) -> Self::FutureAuth { + async { Ok((self, auth)) }.boxed() + } + + fn finished_bool(self, b: bool, s: Session) -> Self::FutureBool { + async move { Ok((self, s, b)) }.boxed() + } + + fn finished(self, s: Session) -> Self::FutureUnit { + async { Ok((self, s)) }.boxed() + } + + fn channel_open_session(self, channel: ChannelId, mut session: Session) -> Self::FutureUnit { + async move { + self.session + .lock() + .await + ._channel_open_session(ServerChannelId(channel), &mut session) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn subsystem_request( + self, + channel: ChannelId, + name: &str, + session: Session, + ) -> Self::FutureUnit { + let name = name.to_string(); + async move { + self.session + .lock() + .await + ._channel_subsystem_request(ServerChannelId(channel), name) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn pty_request( + self, + channel: ChannelId, + term: &str, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + modes: &[(Pty, u32)], + session: Session, + ) -> Self::FutureUnit { + let term = term.to_string(); + let modes = modes.to_vec(); + async move { + self.session + .lock() + .await + ._channel_pty_request( + ServerChannelId(channel), + PtyRequest { + term, + col_width, + row_height, + pix_width, + pix_height, + modes, + }, + ) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn shell_request(self, channel: ChannelId, session: Session) -> Self::FutureUnit { + async move { + self.session + .lock() + .await + ._channel_shell_request(ServerChannelId(channel)) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn auth_publickey(self, user: &str, key: &russh_keys::key::PublicKey) -> Self::FutureAuth { + let user = user.to_string(); + let key = key.clone(); + async move { + let result = self.session.lock().await._auth_publickey(user, &key).await; + Ok((self, result)) + } + .boxed() + } + + fn auth_password(self, user: &str, password: &str) -> Self::FutureAuth { + let user = user.to_string(); + let password = password.to_string(); + async move { + let result = self + .session + .lock() + .await + ._auth_password(Secret::new(user), Secret::new(password)) + .await; + Ok((self, result)) + } + .boxed() + } + + fn data(self, channel: ChannelId, data: &[u8], session: Session) -> Self::FutureUnit { + let data = BytesMut::from(data).freeze(); + async move { + self.session + .lock() + .await + ._data(ServerChannelId(channel), data) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn extended_data( + self, + channel: ChannelId, + code: u32, + data: &[u8], + session: Session, + ) -> Self::FutureUnit { + let data = BytesMut::from(data); + async move { + self.session + .lock() + .await + ._extended_data(ServerChannelId(channel), code, data) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn channel_close(self, channel: ChannelId, session: Session) -> Self::FutureUnit { + async move { + self.session + .lock() + .await + ._channel_close(ServerChannelId(channel)) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn window_change_request( + self, + channel: ChannelId, + col_width: u32, + row_height: u32, + pix_width: u32, + pix_height: u32, + session: Session, + ) -> Self::FutureUnit { + async move { + self.session + .lock() + .await + ._window_change_request( + ServerChannelId(channel), + PtyRequest { + term: "".to_string(), + col_width, + row_height, + pix_width, + pix_height, + modes: vec![], + }, + ) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn channel_eof(self, channel: ChannelId, session: Session) -> Self::FutureUnit { + async move { + self.session + .lock() + .await + ._channel_eof(ServerChannelId(channel)) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn signal( + self, + channel: ChannelId, + signal_name: russh::Sig, + session: Session, + ) -> Self::FutureUnit { + async move { + self.session + .lock() + .await + ._channel_signal(ServerChannelId(channel), signal_name) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn exec_request(self, channel: ChannelId, data: &[u8], session: Session) -> Self::FutureUnit { + let data = BytesMut::from(data); + async move { + self.session + .lock() + .await + ._channel_exec_request(ServerChannelId(channel), data.freeze()) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn env_request( + self, + channel: ChannelId, + variable_name: &str, + variable_value: &str, + session: Session, + ) -> Self::FutureUnit { + let variable_name = variable_name.to_string(); + let variable_value = variable_value.to_string(); + async move { + self.session.lock().await._channel_env_request( + ServerChannelId(channel), + variable_name, + variable_value, + )?; + Ok((self, session)) + } + .boxed() + } + + fn channel_open_direct_tcpip( + self, + channel: ChannelId, + host_to_connect: &str, + port_to_connect: u32, + originator_address: &str, + originator_port: u32, + mut session: Session, + ) -> Self::FutureUnit { + let host_to_connect = host_to_connect.to_string(); + let originator_address = originator_address.to_string(); + async move { + self.session + .lock() + .await + ._channel_open_direct_tcpip( + ServerChannelId(channel), + DirectTCPIPParams { + host_to_connect, + port_to_connect, + originator_address, + originator_port, + }, + &mut session, + ) + .await?; + Ok((self, session)) + } + .boxed() + } + + fn x11_request( + self, + channel: ChannelId, + single_conection: bool, + x11_auth_protocol: &str, + x11_auth_cookie: &str, + x11_screen_number: u32, + session: Session, + ) -> Self::FutureUnit { + let x11_auth_protocol = x11_auth_protocol.to_string(); + let x11_auth_cookie = x11_auth_cookie.to_string(); + async move { + self.session + .lock() + .await + ._channel_x11_request( + ServerChannelId(channel), + X11Request { + single_conection, + x11_auth_protocol, + x11_auth_cookie, + x11_screen_number, + }, + ) + .await?; + Ok((self, session)) + } + .boxed() + } + + // ----- + + // fn auth_none(self, user: &str) -> Self::FutureAuth { + // self.finished_auth(Auth::Reject) + // } + + // fn auth_keyboard_interactive( + // self, + // user: &str, + // submethods: &str, + // response: Option, + // ) -> Self::FutureAuth { + // self.finished_auth(Auth::Reject) + // } + + // fn tcpip_forward(self, address: &str, port: u32, session: Session) -> Self::FutureBool { + // self.finished_bool(false, session) + // } + + // fn cancel_tcpip_forward(self, address: &str, port: u32, session: Session) -> Self::FutureBool { + // self.finished_bool(false, session) + // } +} + +impl Drop for ServerHandler { + fn drop(&mut self) { + debug!("Dropped"); + let client = self.session.clone(); + tokio::task::Builder::new() + .name(&format!("SSH {} cleanup", self.id)) + .spawn(async move { + client.lock().await._disconnect().await; + }); + } +} + +impl Debug for ServerHandler { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "ServerHandler") + } +} diff --git a/warpgate-protocol-ssh/src/server/service_output.rs b/warpgate-protocol-ssh/src/server/service_output.rs new file mode 100644 index 0000000..d8aa118 --- /dev/null +++ b/warpgate-protocol-ssh/src/server/service_output.rs @@ -0,0 +1,74 @@ +use ansi_term::Colour; +use anyhow::Result; +use std::sync::atomic::AtomicBool; +use std::sync::Arc; +use tokio::sync::{mpsc, Mutex}; + +pub const ERASE_PROGRESS_SPINNER: &str = "\r \r"; + +pub type Callback = dyn Fn(&[u8]) -> Result<()> + Send + 'static; + +#[derive(Clone)] +pub struct ServiceOutput { + progress_visible: Arc, + callback: Arc>>, + abort_tx: mpsc::Sender<()>, +} + +impl ServiceOutput { + pub fn new(callback: Box) -> Self { + let callback = Arc::new(Mutex::new(callback)); + let progress_visible = Arc::new(AtomicBool::new(false)); + let (abort_tx, mut abort_rx) = mpsc::channel(1); + tokio::spawn({ + let progress_visible = progress_visible.clone(); + let callback = callback.clone(); + let ticks = "⠁⠁⠉⠙⠚⠒⠂⠂⠒⠲⠴⠤⠄⠄⠤⠠⠠⠤⠦⠖⠒⠐⠐⠒⠓⠋⠉⠈⠈".chars().collect::>(); + let mut tick_index = 0; + async move { + loop { + tokio::select! { + _ = abort_rx.recv() => { + return; + } + _ = tokio::time::sleep(std::time::Duration::from_millis(100)) => { + if progress_visible.load(std::sync::atomic::Ordering::Relaxed) { + tick_index = (tick_index + 1) % ticks.len(); + let tick = ticks[tick_index]; + let badge = Colour::Black.on(Colour::Blue).paint(format!(" {} Warpgate connecting ", tick)); + let output = format!("{ERASE_PROGRESS_SPINNER}{badge}"); + if callback.lock().await(output.as_bytes()).is_err() { + return; + } + } + } + } + } + } + }); + ServiceOutput { + progress_visible, + callback, + abort_tx, + } + } + + pub fn show_progress(&mut self) { + self.progress_visible + .store(true, std::sync::atomic::Ordering::Relaxed); + } + + pub async fn hide_progress(&mut self) { + self.progress_visible + .store(false, std::sync::atomic::Ordering::Relaxed); + let cb = self.callback.lock().await; + let _ = cb(ERASE_PROGRESS_SPINNER.as_bytes()); + let _ = cb("\n".as_bytes()); + } +} + +impl Drop for ServiceOutput { + fn drop(&mut self) { + let _ = self.abort_tx.send(()); + } +} diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs new file mode 100644 index 0000000..679db74 --- /dev/null +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -0,0 +1,1030 @@ +use super::service_output::ServiceOutput; +use super::session_handle::SessionHandleCommand; +use crate::compat::ContextExt; +use crate::server::service_output::ERASE_PROGRESS_SPINNER; +use crate::{ + ChannelOperation, ConnectionError, DirectTCPIPParams, PtyRequest, RCCommand, RCEvent, RCState, + RemoteClient, ServerChannelId, X11Request, +}; +use ansi_term::Colour; +use anyhow::{Context, Result}; +use bimap::BiMap; +use bytes::{Bytes, BytesMut}; +use russh::server::Session; +use russh::{CryptoVec, Sig}; +use russh_keys::key::PublicKey; +use russh_keys::PublicKeyBase64; +use std::collections::hash_map::Entry::Vacant; +use std::collections::HashMap; +use std::net::{Ipv4Addr, SocketAddr}; +use std::str::FromStr; +use std::sync::Arc; +use tokio::sync::mpsc::{UnboundedReceiver, UnboundedSender}; +use tokio::sync::{oneshot, Mutex}; +use tracing::*; +use uuid::Uuid; +use warpgate_common::auth::AuthSelector; +use warpgate_common::eventhub::{EventHub, EventSender}; +use warpgate_common::recordings::{ + ConnectionRecorder, TerminalRecorder, TrafficConnectionParams, TrafficRecorder, +}; +use warpgate_common::{ + authorize_ticket, AuthCredential, AuthResult, Secret, Services, SessionId, Target, + TargetSSHOptions, WarpgateServerHandle, +}; + +#[derive(Clone)] +enum TargetSelection { + None, + NotFound(String), + Found(Target, TargetSSHOptions), +} + +#[derive(Debug)] +enum Event { + Command(SessionHandleCommand), + ConsoleInput(Bytes), + ServiceOutput(Bytes), + Client(RCEvent), +} + +pub struct ServerSession { + pub id: SessionId, + session_handle: Option, + pty_channels: Vec, + all_channels: Vec, + channel_recorders: HashMap, + channel_map: BiMap, + rc_tx: UnboundedSender, + rc_abort_tx: UnboundedSender<()>, + rc_state: RCState, + remote_address: SocketAddr, + services: Services, + server_handle: WarpgateServerHandle, + target: TargetSelection, + traffic_recorders: HashMap<(String, u32), TrafficRecorder>, + traffic_connection_recorders: HashMap, + credentials: Vec, + hub: EventHub, + event_sender: EventSender, + service_output: ServiceOutput, +} + +fn session_debug_tag(id: &SessionId, remote_address: &SocketAddr) -> String { + format!("[{} - {}]", id, remote_address) +} + +impl std::fmt::Debug for ServerSession { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", session_debug_tag(&self.id, &self.remote_address)) + } +} + +impl ServerSession { + pub async fn new( + remote_address: SocketAddr, + services: &Services, + server_handle: WarpgateServerHandle, + mut session_handle_rx: UnboundedReceiver, + ) -> Result>> { + let id = server_handle.id(); + let mut rc_handles = RemoteClient::create( + id, + session_debug_tag(&id, &remote_address), + services.clone(), + ); + + let (hub, event_sender) = EventHub::setup(); + let mut event_sub = hub.subscribe(|_| true).await; + + let (so_tx, mut so_rx) = tokio::sync::mpsc::unbounded_channel(); + let so_sender = event_sender.clone(); + tokio::spawn(async move { + while let Some(data) = so_rx.recv().await { + if so_sender + .send_once(Event::ServiceOutput(data)) + .await + .is_err() + { + break; + } + } + }); + + let this = Self { + id: server_handle.id(), + session_handle: None, + pty_channels: vec![], + all_channels: vec![], + channel_recorders: HashMap::new(), + channel_map: BiMap::new(), + rc_tx: rc_handles.command_tx.clone(), + rc_abort_tx: rc_handles.abort_tx, + rc_state: RCState::NotInitialized, + remote_address, + services: services.clone(), + server_handle, + target: TargetSelection::None, + traffic_recorders: HashMap::new(), + traffic_connection_recorders: HashMap::new(), + credentials: vec![], + hub, + event_sender: event_sender.clone(), + service_output: ServiceOutput::new(Box::new(move |data| { + so_tx.send(BytesMut::from(data).freeze()).context("x") + })), + }; + + info!(session=?this, "New connection"); + + let session_debug_tag = format!("{:?}", this); + let this = Arc::new(Mutex::new(this)); + + let name = format!("SSH {} session control", id); + tokio::task::Builder::new().name(&name).spawn({ + let sender = event_sender.clone(); + async move { + while let Some(command) = session_handle_rx.recv().await { + if sender.send_once(Event::Command(command)).await.is_err() { + break; + } + } + } + }); + + let name = format!("SSH {} client events", id); + tokio::task::Builder::new().name(&name).spawn({ + let sender = event_sender.clone(); + async move { + while let Some(e) = rc_handles.event_rx.recv().await { + if sender.send_once(Event::Client(e)).await.is_err() { + break; + } + } + } + }); + + let name = format!("SSH {} events", id); + tokio::task::Builder::new().name(&name).spawn({ + let this = Arc::downgrade(&this); + async move { + loop { + match event_sub.recv().await { + Some(Event::Client(RCEvent::Done)) => { + break + } + Some(Event::Client(e)) => { + debug!(session=%session_debug_tag, event=?e, "Event"); + let Some(this) = this.upgrade() else { + break; + }; + let this = &mut this.lock().await; + if let Err(err) = this.handle_remote_event(e).await { + error!(session=%session_debug_tag, "Event handler error: {:?}", err); + break; + } + } + Some(Event::Command(command)) => { + debug!(session=%session_debug_tag, ?command, "Session control"); + let Some(this) = this.upgrade() else { + break; + }; + let this = &mut this.lock().await; + if let Err(err) = this.handle_session_control(command).await { + error!(session=%session_debug_tag, "Event handler error: {:?}", err); + break; + } + } + Some(Event::ServiceOutput(data)) => { + let Some(this) = this.upgrade() else { + break; + }; + let this = &mut this.lock().await; + let _ = this.emit_pty_output(&data).await; + } + Some(Event::ConsoleInput(_)) => (), + None => break, + } + } + debug!(session=%session_debug_tag, "No more events"); + } + }); + + Ok(this) + } + + fn map_channel(&self, ch: &ServerChannelId) -> Result { + self.channel_map + .get_by_left(ch) + .cloned() + .ok_or_else(|| anyhow::anyhow!("Channel not known")) + } + + fn map_channel_reverse(&self, ch: &Uuid) -> Result { + self.channel_map + .get_by_right(ch) + .cloned() + .ok_or_else(|| anyhow::anyhow!("Channel not known")) + } + + pub async fn emit_service_message(&mut self, msg: &str) -> Result<()> { + debug!(session=?self, "Service message: {}", msg); + + self.emit_pty_output( + format!( + "{}{} {}\r\n", + ERASE_PROGRESS_SPINNER, + Colour::Black.on(Colour::White).paint(" Warpgate "), + msg.replace('\n', "\r\n"), + ) + .as_bytes(), + ) + .await + } + + pub async fn emit_pty_output(&mut self, data: &[u8]) -> Result<()> { + let channels = self.pty_channels.clone(); + for channel in channels { + let channel = self.map_channel_reverse(&channel)?; + self.maybe_with_session(|session| async { + session + .data(channel.0, CryptoVec::from_slice(data)) + .await + .map_err(|_| anyhow::anyhow!("Could not send data")) + }) + .await?; + } + Ok(()) + } + + pub async fn maybe_connect_remote(&mut self) -> Result<()> { + match self.target.clone() { + TargetSelection::None => { + panic!("Target not set"); + } + TargetSelection::NotFound(name) => { + self.emit_service_message(&format!("Selected target not found: {name}")) + .await?; + self.disconnect_server().await; + anyhow::bail!("Target not found: {}", name); + } + TargetSelection::Found(target, ssh_options) => { + if self.rc_state == RCState::NotInitialized { + self.rc_state = RCState::Connecting; + self.rc_tx.send(RCCommand::Connect(ssh_options))?; + self.service_output.show_progress(); + self.emit_service_message(&format!("Selected target: {}", target.name)) + .await?; + } + } + } + Ok(()) + } + + pub async fn handle_session_control(&mut self, command: SessionHandleCommand) -> Result<()> { + match command { + SessionHandleCommand::Close => { + let _ = self.emit_service_message("Session closed by admin").await; + info!(session=?self, "Session closed by admin"); + let _ = self.request_disconnect().await; + self.disconnect_server().await; + } + } + Ok(()) + } + + pub async fn handle_remote_event(&mut self, event: RCEvent) -> Result<()> { + match event { + RCEvent::State(state) => { + self.rc_state = state; + match &self.rc_state { + RCState::Connected => { + self.service_output.hide_progress().await; + self.emit_pty_output( + format!( + "{}{}\r\n", + ERASE_PROGRESS_SPINNER, + Colour::Black + .on(Colour::Green) + .paint(" ✓ Warpgate connected ") + ) + .as_bytes(), + ) + .await?; + } + RCState::Disconnected => { + self.service_output.hide_progress().await; + self.disconnect_server().await; + } + _ => {} + } + } + RCEvent::ConnectionError(error) => { + self.service_output.hide_progress().await; + + match error { + ConnectionError::HostKeyMismatch { + received_key_type, + received_key_base64, + known_key_type, + known_key_base64, + } => { + let msg = format!( + concat!( + "Host key doesn't match the stored one.\n", + "Stored key ({}): {}\n", + "Received key ({}): {}", + ), + known_key_type, + known_key_base64, + received_key_type, + received_key_base64 + ); + self.emit_service_message(&msg).await?; + self.emit_service_message( + "If you know that the key is correct (e.g. it has been changed),", + ) + .await?; + self.emit_service_message( + "you can remove the old key in the Warpgate management UI and try again", + ) + .await?; + } + error => { + self.emit_pty_output( + format!( + "{}{} {}\r\n", + ERASE_PROGRESS_SPINNER, + Colour::Black.on(Colour::Red).paint(" Connection failed "), + error + ) + .as_bytes(), + ) + .await?; + } + } + } + RCEvent::Output(channel, data) => { + if let Some(recorder) = self.channel_recorders.get_mut(&channel) { + if let Err(error) = recorder.write(&data).await { + error!(session=?self, %channel, ?error, "Failed to record terminal data"); + self.channel_recorders.remove(&channel); + } + } + + if let Some(recorder) = self.traffic_connection_recorders.get_mut(&channel) { + if let Err(error) = recorder.write_rx(&data).await { + error!(session=?self, %channel, ?error, "Failed to record traffic data"); + self.traffic_connection_recorders.remove(&channel); + } + } + + let server_channel_id = self.map_channel_reverse(&channel)?; + self.maybe_with_session(|handle| async move { + handle + .data(server_channel_id.0, CryptoVec::from_slice(&data)) + .await + .map_err(|_| ()) + .context("failed to send data") + }) + .await?; + } + RCEvent::Success(channel) => { + let server_channel_id = self.map_channel_reverse(&channel)?; + self.maybe_with_session(|handle| async move { + handle + .channel_success(server_channel_id.0) + .await + .context("failed to send data") + }) + .await?; + } + RCEvent::Close(channel) => { + let server_channel_id = self.map_channel_reverse(&channel)?; + self.maybe_with_session(|handle| async move { + handle + .close(server_channel_id.0) + .await + .context("failed to close ch") + }) + .await?; + } + RCEvent::Eof(channel) => { + let server_channel_id = self.map_channel_reverse(&channel)?; + self.maybe_with_session(|handle| async move { + handle + .eof(server_channel_id.0) + .await + .context("failed to send eof") + }) + .await?; + } + RCEvent::ExitStatus(channel, code) => { + let server_channel_id = self.map_channel_reverse(&channel)?; + self.maybe_with_session(|handle| async move { + handle + .exit_status_request(server_channel_id.0, code) + .await + .context("failed to send exit status") + }) + .await?; + } + RCEvent::ExitSignal { + channel, + signal_name, + core_dumped, + error_message, + lang_tag, + } => { + let server_channel_id = self.map_channel_reverse(&channel)?; + self.maybe_with_session(|handle| async move { + handle + .exit_signal_request( + server_channel_id.0, + signal_name, + core_dumped, + error_message, + lang_tag, + ) + .await + .context("failed to send exit status")?; + Ok(()) + }) + .await?; + } + RCEvent::Done => {} + RCEvent::ExtendedData { channel, data, ext } => { + if let Some(recorder) = self.channel_recorders.get_mut(&channel) { + if let Err(error) = recorder.write(&data).await { + error!(session=?self, %channel, ?error, "Failed to record session data"); + self.channel_recorders.remove(&channel); + } + } + let server_channel_id = self.map_channel_reverse(&channel)?; + self.maybe_with_session(|handle| async move { + handle + .extended_data(server_channel_id.0, ext, CryptoVec::from_slice(&data)) + .await + .map_err(|_| ()) + .context("failed to send extended data")?; + Ok(()) + }) + .await?; + } + RCEvent::HostKeyReceived(key) => { + self.emit_service_message(&format!( + "Host key ({}): {}", + key.name(), + key.public_key_base64() + )) + .await?; + } + RCEvent::HostKeyUnknown(key, reply) => { + self.handle_unknown_host_key(key, reply).await?; + } + } + Ok(()) + } + + async fn handle_unknown_host_key( + &mut self, + key: PublicKey, + reply: oneshot::Sender, + ) -> Result<()> { + self.service_output.hide_progress().await; + self.emit_service_message(&format!( + "There is no trusted {} key for this host.", + key.name() + )) + .await?; + self.emit_service_message("Trust this key? (y/n)").await?; + + let mut sub = self + .hub + .subscribe(|e| matches!(e, Event::ConsoleInput(_))) + .await; + + let mut service_output = self.service_output.clone(); + tokio::spawn(async move { + loop { + match sub.recv().await { + Some(Event::ConsoleInput(data)) => { + if data == "y".as_bytes() { + let _ = reply.send(true); + break; + } else if data == "n".as_bytes() { + let _ = reply.send(false); + break; + } + } + None => break, + _ => (), + } + } + service_output.show_progress(); + }); + + Ok(()) + } + + async fn maybe_with_session<'a, FN, FT, R>(&'a mut self, f: FN) -> Result> + where + FN: FnOnce(&'a mut russh::server::Handle) -> FT + 'a, + FT: futures::Future>, + { + if let Some(handle) = &mut self.session_handle { + return Ok(Some(f(handle).await?)); + } + Ok(None) + } + + pub async fn _channel_open_session( + &mut self, + server_channel_id: ServerChannelId, + session: &mut Session, + ) -> Result<()> { + let channel = Uuid::new_v4(); + self.channel_map.insert(server_channel_id, channel); + + info!(session=?self, %channel, "Opening session channel"); + self.all_channels.push(channel); + self.session_handle = Some(session.handle()); + self.rc_tx + .send(RCCommand::Channel(channel, ChannelOperation::OpenShell))?; + Ok(()) + } + + pub async fn _channel_open_direct_tcpip( + &mut self, + channel: ServerChannelId, + params: DirectTCPIPParams, + session: &mut Session, + ) -> Result<()> { + let uuid = Uuid::new_v4(); + self.channel_map.insert(channel, uuid); + + info!(session=?self, %channel, "Opening direct TCP/IP channel from {}:{} to {}:{}", params.originator_address, params.originator_port, params.host_to_connect, params.port_to_connect); + + let recorder = self + .traffic_recorder_for(¶ms.host_to_connect, params.port_to_connect) + .await; + if let Some(recorder) = recorder { + let mut recorder = recorder.connection(TrafficConnectionParams { + dst_addr: Ipv4Addr::from_str("2.2.2.2").unwrap(), + dst_port: params.port_to_connect as u16, + src_addr: Ipv4Addr::from_str("1.1.1.1").unwrap(), + src_port: params.originator_port as u16, + }); + if let Err(error) = recorder.write_connection_setup().await { + error!(session=?self, %channel, ?error, "Failed to record connection setup"); + } + self.traffic_connection_recorders.insert(uuid, recorder); + } + + self.all_channels.push(uuid); + self.session_handle = Some(session.handle()); + self.rc_tx.send(RCCommand::Channel( + uuid, + ChannelOperation::OpenDirectTCPIP(params), + ))?; + Ok(()) + } + + pub async fn _channel_pty_request( + &mut self, + server_channel_id: ServerChannelId, + request: PtyRequest, + ) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + if let Some(recorder) = self.channel_recorders.get_mut(&channel_id) { + if let Err(error) = recorder + .write_pty_resize(request.col_width, request.row_height) + .await + { + error!(session=?self, %channel_id, ?error, "Failed to record terminal data"); + self.channel_recorders.remove(&channel_id); + } + } + self.rc_tx.send(RCCommand::Channel( + channel_id, + ChannelOperation::RequestPty(request), + ))?; + let _ = self + .session_handle + .as_mut() + .unwrap() + .channel_success(server_channel_id.0) + .await; + self.pty_channels.push(channel_id); + Ok(()) + } + + pub async fn _window_change_request( + &mut self, + server_channel_id: ServerChannelId, + request: PtyRequest, + ) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + if let Some(recorder) = self.channel_recorders.get_mut(&channel_id) { + if let Err(error) = recorder + .write_pty_resize(request.col_width, request.row_height) + .await + { + error!(session=?self, %channel_id, ?error, "Failed to record terminal data"); + self.channel_recorders.remove(&channel_id); + } + } + self.send_command(RCCommand::Channel( + channel_id, + ChannelOperation::ResizePty(request), + )); + Ok(()) + } + + pub async fn _channel_exec_request( + &mut self, + server_channel_id: ServerChannelId, + data: Bytes, + ) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + match std::str::from_utf8(&data) { + Err(e) => { + error!(session=?self, channel=%channel_id, ?data, "Requested exec - invalid UTF-8"); + anyhow::bail!(e) + } + Ok::<&str, _>(command) => { + debug!(session=?self, channel=%channel_id, %command, "Requested exec"); + let _ = self.maybe_connect_remote().await; + self.send_command(RCCommand::Channel( + channel_id, + ChannelOperation::RequestExec(command.to_string()), + )); + } + } + Ok(()) + } + + pub async fn _channel_x11_request( + &mut self, + server_channel_id: ServerChannelId, + request: X11Request, + ) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + debug!(session=?self, channel=%channel_id, "Requested X11"); + let _ = self.maybe_connect_remote().await; + self.send_command(RCCommand::Channel( + channel_id, + ChannelOperation::RequestX11(request), + )); + Ok(()) + } + + pub fn _channel_env_request( + &mut self, + server_channel_id: ServerChannelId, + name: String, + value: String, + ) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + debug!(session=?self, channel=%channel_id, %name, %value, "Environment"); + self.send_command(RCCommand::Channel( + channel_id, + ChannelOperation::RequestEnv(name, value), + )); + Ok(()) + } + + async fn traffic_recorder_for( + &mut self, + host: &String, + port: u32, + ) -> Option<&mut TrafficRecorder> { + if let Vacant(e) = self.traffic_recorders.entry((host.clone(), port)) { + match self + .services + .recordings + .lock() + .await + .start(&self.id, format!("direct-tcpip-{host}-{port}")) + .await + { + Ok(recorder) => { + e.insert(recorder); + } + Err(error) => { + error!(session=?self, %host, %port, ?error, "Failed to start recording"); + } + } + } + self.traffic_recorders.get_mut(&(host.clone(), port)) + } + + pub async fn _channel_shell_request( + &mut self, + server_channel_id: ServerChannelId, + ) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + self.rc_tx.send(RCCommand::Channel( + channel_id, + ChannelOperation::RequestShell, + ))?; + + match self + .services + .recordings + .lock() + .await + .start(&self.id, format!("shell-channel-{}", server_channel_id.0)) + .await + { + Ok(recorder) => { + self.channel_recorders.insert(channel_id, recorder); + } + Err(error) => { + error!(session=?self, channel=%channel_id, ?error, "Failed to start recording"); + } + } + + info!(session=?self, %channel_id, "Opening shell"); + let _ = self + .session_handle + .as_mut() + .unwrap() + .channel_success(server_channel_id.0) + .await; + let _ = self.maybe_connect_remote().await; + Ok(()) + } + + pub async fn _channel_subsystem_request( + &mut self, + server_channel_id: ServerChannelId, + name: String, + ) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + info!(session=?self, channel=%channel_id, "Requesting subsystem {}", &name); + self.send_command(RCCommand::Channel( + channel_id, + ChannelOperation::RequestSubsystem(name), + )); + Ok(()) + } + + pub async fn _data(&mut self, server_channel_id: ServerChannelId, data: Bytes) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + debug!(session=?self, channel=%server_channel_id.0, ?data, "Data"); + if self.rc_state == RCState::Connecting && data.get(0) == Some(&3) { + info!(session=?self, channel=%channel_id, "User requested connection abort (Ctrl-C)"); + self.request_disconnect().await; + return Ok(()); + } + + if let Some(recorder) = self.traffic_connection_recorders.get_mut(&channel_id) { + if let Err(error) = recorder.write_tx(&data).await { + error!(session=?self, channel=%channel_id, ?error, "Failed to record traffic data"); + self.traffic_connection_recorders.remove(&channel_id); + } + } + + if self.pty_channels.contains(&channel_id) { + let _ = self + .event_sender + .send_once(Event::ConsoleInput(data.clone())) + .await; + } + + self.send_command(RCCommand::Channel(channel_id, ChannelOperation::Data(data))); + Ok(()) + } + + pub async fn _extended_data( + &mut self, + server_channel_id: ServerChannelId, + code: u32, + data: BytesMut, + ) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + debug!(session=?self, channel=%server_channel_id.0, ?data, "Data"); + self.send_command(RCCommand::Channel( + channel_id, + ChannelOperation::ExtendedData { + ext: code, + data: data.freeze(), + }, + )); + Ok(()) + } + + pub async fn _auth_publickey( + &mut self, + ssh_username: String, + key: &PublicKey, + ) -> russh::server::Auth { + let selector: AuthSelector = (&ssh_username).into(); + + info!(session=?self, "Public key auth as {:?} with key FP {}", selector, key.fingerprint()); + + self.credentials.push(AuthCredential::PublicKey { + kind: key.name().to_string(), + public_key_bytes: Bytes::from(key.public_key_bytes()), + }); + + match self.try_auth(&selector).await { + Ok(AuthResult::Accepted { .. }) => russh::server::Auth::Accept, + Ok(AuthResult::Rejected) => russh::server::Auth::Reject, + Err(error) => { + error!(session=?self, ?error, "Failed to verify credentials"); + russh::server::Auth::Reject + } + } + } + + pub async fn _auth_password( + &mut self, + ssh_username: Secret, + password: Secret, + ) -> russh::server::Auth { + let selector: AuthSelector = ssh_username.expose_secret().into(); + info!(session=?self, "Password key auth as {:?}", selector); + + self.credentials.push(AuthCredential::Password(password)); + + match self.try_auth(&selector).await { + Ok(AuthResult::Accepted { .. }) => russh::server::Auth::Accept, + Ok(AuthResult::Rejected) => russh::server::Auth::Reject, + Err(error) => { + error!(session=?self, ?error, "Failed to verify credentials"); + russh::server::Auth::Reject + } + } + } + + async fn try_auth(&mut self, selector: &AuthSelector) -> Result { + match selector { + AuthSelector::User { + username, + target_name, + } => { + let user_auth_result: AuthResult = { + self.services + .config_provider + .lock() + .await + .authorize(username, &self.credentials) + .await? + }; + + match user_auth_result { + AuthResult::Accepted { username } => { + let target_auth_result = { + self.services + .config_provider + .lock() + .await + .authorize_target(&username, target_name) + .await? + }; + if !target_auth_result { + warn!( + "Target {} not authorized for user {}", + target_name, username + ); + return Ok(AuthResult::Rejected); + } + self._auth_accept(&username, target_name).await; + Ok(AuthResult::Accepted { username }) + } + AuthResult::Rejected => Ok(AuthResult::Rejected), + } + } + AuthSelector::Ticket { secret } => { + match authorize_ticket(&self.services.db, secret).await? { + Some(ticket) => { + info!(session=?self, "Authorized for {} with a ticket", ticket.target); + self.services + .config_provider + .lock() + .await + .consume_ticket(&ticket.id) + .await?; + self._auth_accept(&ticket.username, &ticket.target).await; + Ok(AuthResult::Accepted { + username: ticket.username.clone(), + }) + } + None => Ok(AuthResult::Rejected), + } + } + } + } + + async fn _auth_accept(&mut self, username: &str, target_name: &str) { + info!(session=?self, "Authenticated"); + + let _ = self.server_handle.set_username(username.to_string()).await; + + let target = { + self.services + .config + .lock() + .await + .store + .targets + .iter() + .find(|x| x.name == target_name) + .filter(|x| x.ssh.is_some()) + .map(|x| (x.clone(), x.ssh.clone().unwrap())) + }; + + let Some((target, ssh_options)) = target else { + self.target = TargetSelection::NotFound(target_name.to_string()); + info!(session=?self, "Selected target not found"); + return; + }; + + let _ = self.server_handle.set_target(&target).await; + self.target = TargetSelection::Found(target, ssh_options); + } + + pub async fn _channel_close(&mut self, server_channel_id: ServerChannelId) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + debug!(session=?self, channel=%channel_id, "Closing channel"); + self.send_command(RCCommand::Channel(channel_id, ChannelOperation::Close)); + Ok(()) + } + + pub async fn _channel_eof(&mut self, server_channel_id: ServerChannelId) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + debug!(session=?self, channel=%channel_id, "EOF"); + self.send_command(RCCommand::Channel(channel_id, ChannelOperation::Eof)); + Ok(()) + } + + // pub async fn _tcpip_forward(&mut self, address: String, port: u32) { + // info!(session=?self, %address, %port, "Remote port forwarding requested"); + // self.send_command(RCCommand::ForwardTCPIP(address, port)); + // } + + // pub async fn _cancel_tcpip_forward(&mut self, address: String, port: u32) { + // info!(session=?self, %address, %port, "Remote port forwarding cancelled"); + // self.send_command(RCCommand::CancelTCPIPForward(address, port)); + // } + + pub async fn _channel_signal( + &mut self, + server_channel_id: ServerChannelId, + signal: Sig, + ) -> Result<()> { + let channel_id = self.map_channel(&server_channel_id)?; + debug!(session=?self, channel=%channel_id, ?signal, "Signal"); + self.send_command(RCCommand::Channel( + channel_id, + ChannelOperation::Signal(signal), + )); + Ok(()) + } + + fn send_command(&mut self, command: RCCommand) { + let _ = self.rc_tx.send(command); + } + + pub async fn _disconnect(&mut self) { + debug!(session=?self, "Client disconnect requested"); + self.request_disconnect().await; + } + + async fn request_disconnect(&mut self) { + debug!(session=?self, "Disconnecting"); + let _ = self.rc_abort_tx.send(()); + if self.rc_state != RCState::NotInitialized && self.rc_state != RCState::Disconnected { + self.send_command(RCCommand::Disconnect); + } + } + + async fn disconnect_server(&mut self) { + let all_channels = std::mem::take(&mut self.all_channels); + let channels = all_channels + .into_iter() + .map(|x| self.map_channel_reverse(&x)) + .filter(|x| x.is_ok()) + .map(|x| x.unwrap()) + .collect::>(); + + let _ = self + .maybe_with_session(|handle| async move { + for ch in channels { + let _ = handle.close(ch.0).await; + } + Ok(()) + }) + .await; + drop(self.session_handle.take()); + } +} + +impl Drop for ServerSession { + fn drop(&mut self) { + info!(session=?self, "Closed connection"); + debug!("Dropped"); + } +} diff --git a/warpgate-protocol-ssh/src/server/session_handle.rs b/warpgate-protocol-ssh/src/server/session_handle.rs new file mode 100644 index 0000000..99d4d54 --- /dev/null +++ b/warpgate-protocol-ssh/src/server/session_handle.rs @@ -0,0 +1,24 @@ +use tokio::sync::mpsc; +use warpgate_common::SessionHandle; + +#[derive(Clone, Debug, PartialEq)] +pub enum SessionHandleCommand { + Close, +} + +pub struct SSHSessionHandle { + sender: mpsc::UnboundedSender, +} + +impl SSHSessionHandle { + pub fn new() -> (Self, mpsc::UnboundedReceiver) { + let (sender, receiver) = mpsc::unbounded_channel(); + (SSHSessionHandle { sender }, receiver) + } +} + +impl SessionHandle for SSHSessionHandle { + fn close(&mut self) { + let _ = self.sender.send(SessionHandleCommand::Close); + } +} diff --git a/warpgate/.gitignore b/warpgate/.gitignore new file mode 100644 index 0000000..9da4a88 --- /dev/null +++ b/warpgate/.gitignore @@ -0,0 +1 @@ +!Cargo.lock diff --git a/warpgate/Cargo.toml b/warpgate/Cargo.toml new file mode 100644 index 0000000..bb06b51 --- /dev/null +++ b/warpgate/Cargo.toml @@ -0,0 +1,35 @@ +[package] +edition = "2021" +license = "Apache-2.0" +name = "warpgate" +version = "0.1.0" + +[dependencies] +anyhow = {version = "1.0", features = ["backtrace"]} +async-trait = "0.1" +bytes = "1.1" +clap = {version = "3.1", features = ["derive"]} +config = "0.12" +console = "0.1" +console-subscriber = {version = "0.1", optional = true} +dhat = {version = "0.3", optional = true} +dialoguer = "0.10" +futures = "0.3" +notify = "^5.0.0-beta.1" +openssl = {version = "0.10", features = ["vendored"]}# Embed OpenSSL +rcgen = {version = "0.9", features = ["zeroize"]} +serde_yaml = "0.8.23" +time = "0.3" +tokio = {version = "1.17", features = ["tracing", "signal"]} +tracing = "0.1" +tracing-subscriber = {version = "0.3", features = ["env-filter", "local-time"]} +warpgate-admin = {version = "*", path = "../warpgate-admin"} +warpgate-common = {version = "*", path = "../warpgate-common"} +warpgate-protocol-ssh = {version = "*", path = "../warpgate-protocol-ssh"} + +[target.'cfg(target_os = "linux")'.dependencies] +sd-notify = "0.4" + +[features] +dhat-ad-hoc = ["dhat"] +dhat-heap = ["dhat"] diff --git a/warpgate/src/commands/check.rs b/warpgate/src/commands/check.rs new file mode 100644 index 0000000..f3a6a70 --- /dev/null +++ b/warpgate/src/commands/check.rs @@ -0,0 +1,22 @@ +use crate::config::load_config; +use anyhow::{Context, Result}; +use std::net::ToSocketAddrs; +use tracing::*; + +pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { + let config = load_config(&cli.config, true)?; + config + .store + .ssh + .listen + .to_socket_addrs() + .context("Failed to parse SSH listen address")?; + config + .store + .web_admin + .listen + .to_socket_addrs() + .context("Failed to parse admin server listen address")?; + info!("No problems found"); + Ok(()) +} diff --git a/warpgate/src/commands/client_keys.rs b/warpgate/src/commands/client_keys.rs new file mode 100644 index 0000000..6408291 --- /dev/null +++ b/warpgate/src/commands/client_keys.rs @@ -0,0 +1,15 @@ +use crate::config::load_config; +use anyhow::Result; +use warpgate_protocol_ssh::helpers::PublicKeyAsOpenSSH; + +pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { + let config = load_config(&cli.config, true)?; + let keys = warpgate_protocol_ssh::load_client_keys(&config)?; + println!("Warpgate SSH client keys:"); + println!("(add these to your target's authorized_hosts file)"); + println!(); + for key in keys { + println!("{}", key.as_openssh()); + } + Ok(()) +} diff --git a/warpgate/src/commands/hash.rs b/warpgate/src/commands/hash.rs new file mode 100644 index 0000000..93fa0b8 --- /dev/null +++ b/warpgate/src/commands/hash.rs @@ -0,0 +1,20 @@ +use anyhow::Result; +use dialoguer::theme::ColorfulTheme; +use std::io::stdin; +use warpgate_common::hash::hash_password; + +pub(crate) async fn command() -> Result<()> { + let mut input = String::new(); + + if console::user_attended() { + input = dialoguer::Password::with_theme(&ColorfulTheme::default()) + .with_prompt("Password to be hashed") + .interact()?; + } else { + stdin().read_line(&mut input)?; + } + + let hash = hash_password(&input); + println!("{}", hash); + Ok(()) +} diff --git a/warpgate/src/commands/mod.rs b/warpgate/src/commands/mod.rs new file mode 100644 index 0000000..5232d4c --- /dev/null +++ b/warpgate/src/commands/mod.rs @@ -0,0 +1,6 @@ +pub mod check; +pub mod client_keys; +pub mod hash; +pub mod run; +pub mod setup; +pub mod test_target; diff --git a/warpgate/src/commands/run.rs b/warpgate/src/commands/run.rs new file mode 100644 index 0000000..2fa594d --- /dev/null +++ b/warpgate/src/commands/run.rs @@ -0,0 +1,133 @@ +use crate::config::{load_config, watch_config}; +use anyhow::Result; +use futures::StreamExt; +use std::net::ToSocketAddrs; +use tracing::*; +use warpgate_common::db::cleanup_db; +use warpgate_common::{ProtocolServer, Services}; +use warpgate_protocol_ssh::SSHProtocolServer; + +#[cfg(target_os = "linux")] +use sd_notify::NotifyState; + +pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { + let version = env!("CARGO_PKG_VERSION"); + info!(%version, "Warpgate"); + + let config = load_config(&cli.config, true)?; + let services = Services::new(config.clone()).await?; + + let mut other_futures = futures::stream::FuturesUnordered::new(); + let mut protocol_futures = futures::stream::FuturesUnordered::new(); + + protocol_futures.push( + SSHProtocolServer::new(&services).await?.run( + config + .store + .ssh + .listen + .to_socket_addrs()? + .next() + .ok_or_else(|| anyhow::anyhow!("Failed to resolve the listen address"))?, + ), + ); + + if config.store.web_admin.enable { + let admin = warpgate_admin::AdminServer::new(&services); + let admin_future = admin.run( + config + .store + .web_admin + .listen + .to_socket_addrs()? + .next() + .ok_or_else(|| { + anyhow::anyhow!("Failed to resolve the listen address for the admin server") + })?, + ); + other_futures.push(admin_future); + } + + tokio::spawn({ + let services = services.clone(); + async move { + loop { + let retention = { services.config.lock().await.store.retention }; + let interval = retention / 10; + match cleanup_db(&mut *services.db.lock().await, &retention).await { + Err(error) => error!(?error, "Failed to cleanup the database"), + Ok(_) => debug!("Database cleaned up, next in {:?}", interval), + } + tokio::time::sleep(interval).await; + } + } + }); + + if console::user_attended() { + info!("--------------------------------------------"); + info!("Warpgate is now running."); + info!("Accepting SSH connections on {}", config.store.ssh.listen); + if config.store.web_admin.enable { + info!( + "Access admin UI on https://{}", + config.store.web_admin.listen + ); + } + info!("--------------------------------------------"); + } + + #[cfg(target_os = "linux")] + if let Ok(true) = sd_notify::booted() { + use std::time::Duration; + tokio::spawn(async { + if let Err(error) = async { + sd_notify::notify(false, &[NotifyState::Ready])?; + loop { + sd_notify::notify(false, &[NotifyState::Watchdog])?; + tokio::time::sleep(Duration::from_secs(15)).await; + } + #[allow(unreachable_code)] + Ok::<(), anyhow::Error>(()) + } + .await + { + error!(?error, "Failed to communicate with systemd"); + } + }); + } + + drop(config); + + tokio::spawn(watch_config(cli.config.clone(), services.config.clone())); + + loop { + tokio::select! { + _ = tokio::signal::ctrl_c() => { + std::process::exit(1); + } + result = protocol_futures.next() => { + match result { + Some(Err(error)) => { + error!(?error, "SSH server error"); + std::process::exit(1); + }, + None => break, + _ => (), + } + } + result = other_futures.next(), if !other_futures.is_empty() => { + match result { + Some(Err(error)) => { + error!(?error, "Error"); + std::process::exit(1); + }, + None => break, + _ => (), + } + } + } + } + + info!("Exiting"); + Ok(()) +} diff --git a/warpgate/src/commands/setup.rs b/warpgate/src/commands/setup.rs new file mode 100644 index 0000000..68c66f2 --- /dev/null +++ b/warpgate/src/commands/setup.rs @@ -0,0 +1,184 @@ +use crate::config::load_config; +use anyhow::Result; +use dialoguer::theme::ColorfulTheme; +use rcgen::generate_simple_self_signed; +use std::fs::{create_dir_all, File}; +use std::io::Write; +use std::path::{Path, PathBuf}; +use tracing::*; +use warpgate_common::hash::hash_password; +use warpgate_common::helpers::fs::{secure_directory, secure_file}; +use warpgate_common::{ + Role, SSHConfig, Secret, Services, Target, TargetWebAdminOptions, User, UserAuthCredential, + WarpgateConfigStore, WebAdminConfig, +}; + +pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { + let version = env!("CARGO_PKG_VERSION"); + info!("Welcome to Warpgate {version}"); + + if cli.config.exists() { + error!("Config file already exists at {}.", cli.config.display()); + error!("To generate a new config file, rename or delete the existing one first."); + std::process::exit(1); + } + + let mut config_dir = cli.config.parent().unwrap_or_else(|| Path::new(&".")); + if config_dir.as_os_str().is_empty() { + config_dir = Path::new(&"."); + } + create_dir_all(config_dir)?; + + info!("Let's do some basic setup first."); + info!( + "The new config will be written in {}.", + cli.config.display() + ); + + let theme = ColorfulTheme::default(); + let mut store = WarpgateConfigStore { + roles: vec![Role { + name: "warpgate:admin".to_owned(), + }], + ..Default::default() + }; + + // --- + + info!( + "* Paths can be either absolute or relative to {}.", + config_dir.canonicalize()?.display() + ); + + // --- + + let data_path: String = dialoguer::Input::with_theme(&theme) + .default("/var/lib/warpgate".into()) + .with_prompt("Directory to store app data (up to a few MB) in") + .interact_text()?; + + let db_path = PathBuf::from(&data_path).join("db"); + create_dir_all(&db_path)?; + secure_directory(&db_path)?; + + let mut db_path = db_path.to_string_lossy().to_string(); + + if let Some(x) = db_path.strip_suffix("./") { + db_path = x.to_string(); + } + + let mut database_url = "sqlite:".to_owned(); + database_url.push_str(&db_path); + store.database_url = Secret::new(database_url); + + // --- + + store.ssh.listen = dialoguer::Input::with_theme(&theme) + .default(SSHConfig::default().listen) + .with_prompt("Endpoint to listen for SSH connections on") + .interact_text()?; + + // --- + + store.web_admin.listen = dialoguer::Input::with_theme(&theme) + .default(WebAdminConfig::default().listen) + .with_prompt("Endpoint to expose admin web interface on") + .interact_text()?; + + if store.web_admin.enable { + store.targets.push(Target { + name: "web-admin".to_owned(), + allow_roles: vec!["warpgate:admin".to_owned()], + ssh: None, + web_admin: Some(TargetWebAdminOptions {}), + }); + } + + store.web_admin.certificate = PathBuf::from(&data_path) + .join("web-admin.certificate.pem") + .to_string_lossy() + .to_string(); + + store.web_admin.key = PathBuf::from(&data_path) + .join("web-admin.key.pem") + .to_string_lossy() + .to_string(); + + // --- + + store.ssh.keys = PathBuf::from(&data_path) + .join("ssh-keys") + .to_string_lossy() + .to_string(); + + // --- + + store.recordings.enable = dialoguer::Confirm::with_theme(&theme) + .default(true) + .with_prompt("Do you want to record user sessions?") + .interact()?; + store.recordings.path = PathBuf::from(&data_path) + .join("recordings") + .to_string_lossy() + .to_string(); + + // --- + + let password = dialoguer::Password::with_theme(&theme) + .with_prompt("Set a password for the Warpgate admin user") + .interact()?; + + store.users.push(User { + username: "admin".into(), + credentials: vec![UserAuthCredential::Password { + hash: Secret::new(hash_password(&password)), + }], + require: None, + roles: vec!["warpgate:admin".into()], + }); + + // --- + + info!("Generated configuration:"); + let yaml = serde_yaml::to_string(&store)?; + println!("{}", yaml); + + File::create(&cli.config)?.write_all(yaml.as_bytes())?; + info!("Saved into {}", cli.config.display()); + + let config = load_config(&cli.config, true)?; + Services::new(config.clone()).await?; + warpgate_protocol_ssh::generate_host_keys(&config)?; + warpgate_protocol_ssh::generate_client_keys(&config)?; + + { + info!("Generating HTTPS certificate"); + let cert = generate_simple_self_signed(vec![ + "warpgate.local".to_string(), + "localhost".to_string(), + ])?; + + let certificate_path = config + .paths_relative_to + .join(&config.store.web_admin.certificate); + let key_path = config.paths_relative_to.join(&config.store.web_admin.key); + std::fs::write(&certificate_path, cert.serialize_pem()?)?; + std::fs::write(&key_path, cert.serialize_private_key_pem())?; + secure_file(&certificate_path)?; + secure_file(&key_path)?; + } + + info!(""); + info!("Admin user credentials:"); + info!(" * Username: admin"); + info!(" * Password: "); + info!(""); + info!("You can now start Warpgate with:"); + info!( + " {} --config {} run", + std::env::args().next().unwrap(), + cli.config.display() + ); + + Ok(()) +} diff --git a/warpgate/src/commands/test_target.rs b/warpgate/src/commands/test_target.rs new file mode 100644 index 0000000..029c3db --- /dev/null +++ b/warpgate/src/commands/test_target.rs @@ -0,0 +1,44 @@ +use crate::config::load_config; +use anyhow::Result; +use tracing::*; +use warpgate_common::{ProtocolServer, Services, Target, TargetTestError}; + +pub(crate) async fn command(cli: &crate::Cli, target_name: &String) -> Result<()> { + let config = load_config(&cli.config, true)?; + + let Some(target) = config + .store + .targets + .iter() + .find(|x| &x.name == target_name) + .map(Target::clone) else { + error!("Target not found: {}", target_name); + return Ok(()); + }; + + let services = Services::new(config.clone()).await?; + + let s = warpgate_protocol_ssh::SSHProtocolServer::new(&services).await?; + match s.test_target(target).await { + Err(TargetTestError::AuthenticationError) => { + error!("Authentication failed"); + } + Err(TargetTestError::ConnectionError(error)) => { + error!(?error, "Connection error"); + } + Err(TargetTestError::Io(error)) => { + error!(?error, "I/O error"); + } + Err(TargetTestError::Misconfigured(error)) => { + error!(?error, "Misconfigured"); + } + Err(TargetTestError::Unreachable) => { + error!("Target is unreachable"); + } + Ok(()) => { + info!("Connection successful!"); + } + } + + Ok(()) +} diff --git a/warpgate/src/config.rs b/warpgate/src/config.rs new file mode 100644 index 0000000..726c9c7 --- /dev/null +++ b/warpgate/src/config.rs @@ -0,0 +1,66 @@ +use anyhow::{Context, Result}; +use config::{Config, Environment, File}; +use notify::{RecommendedWatcher, RecursiveMode, Watcher}; +use std::path::Path; +use std::sync::Arc; +use tokio::sync::{mpsc, Mutex}; +use tracing::*; +use warpgate_common::helpers::fs::secure_file; +use warpgate_common::{WarpgateConfig, WarpgateConfigStore}; + +pub fn load_config(path: &Path, secure: bool) -> Result { + if secure { + secure_file(path).context("Could not secure config")?; + } + + let store: WarpgateConfigStore = Config::builder() + .add_source(File::from(path)) + .add_source(Environment::with_prefix("WARPGATE")) + .build() + .context("Could not load config")? + .try_deserialize() + .context("Could not parse config")?; + + let config = WarpgateConfig { + store, + paths_relative_to: path.parent().unwrap().to_path_buf(), + }; + + info!( + "Using config: {path:?} (users: {}, targets: {}, roles: {})", + config.store.users.len(), + config.store.targets.len(), + config.store.roles.len(), + ); + Ok(config) +} + +pub async fn watch_config>( + path: P, + config: Arc>, +) -> Result<()> { + let (tx, mut rx) = mpsc::channel(1); + let mut watcher = RecommendedWatcher::new(move |res| { + tx.blocking_send(res).unwrap(); + })?; + watcher.configure(notify::Config::PreciseEvents(true))?; + watcher.watch(path.as_ref(), RecursiveMode::NonRecursive)?; + + loop { + match rx.recv().await { + Some(Ok(event)) => { + if event.kind.is_modify() { + match load_config(path.as_ref(), false) { + Ok(new_config) => { + *(config.lock().await) = new_config; + info!("Reloaded config"); + } + Err(error) => error!(?error, "Failed to reload config"), + } + } + } + Some(Err(error)) => error!(?error, "Failed to watch config"), + None => error!("Config watch failed"), + } + } +} diff --git a/warpgate/src/logging.rs b/warpgate/src/logging.rs new file mode 100644 index 0000000..e9f7f73 --- /dev/null +++ b/warpgate/src/logging.rs @@ -0,0 +1,60 @@ +use std::sync::Arc; +use time::{format_description, UtcOffset}; +use tracing_subscriber::filter::dynamic_filter_fn; +use tracing_subscriber::fmt::time::OffsetTime; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{EnvFilter, Layer}; + +pub fn init_logging() { + if std::env::var("RUST_LOG").is_err() { + std::env::set_var("RUST_LOG", "warpgate=info") + } + + let offset = UtcOffset::current_local_offset() + .unwrap_or_else(|_| UtcOffset::from_whole_seconds(0).unwrap()); + + let env_filter = Arc::new(EnvFilter::from_default_env()); + let enable_colors = console::user_attended(); + + let registry = tracing_subscriber::registry(); + + #[cfg(all(debug_assertions, feature = "console-subscriber"))] + let console_layer = console_subscriber::spawn(); + #[cfg(all(debug_assertions, feature = "console-subscriber"))] + let registry = registry.with(console_layer); + + let registry = registry + .with((!console::user_attended()).then({ + let env_filter = env_filter.clone(); + || { + tracing_subscriber::fmt::layer() + .with_ansi(enable_colors) + .with_timer(OffsetTime::new( + offset, + format_description::parse("[day].[month].[year] [hour]:[minute]:[second]") + .unwrap(), + )) + .with_filter(dynamic_filter_fn(move |m, c| { + env_filter.enabled(m, c.clone()) + })) + } + })) + .with(console::user_attended().then({ + || { + tracing_subscriber::fmt::layer() + .compact() + .with_ansi(enable_colors) + .with_target(false) + .with_timer(OffsetTime::new( + offset, + format_description::parse("[hour]:[minute]:[second]").unwrap(), + )) + .with_filter(dynamic_filter_fn(move |m, c| { + env_filter.enabled(m, c.clone()) + })) + } + })); + + registry.init(); +} diff --git a/warpgate/src/main.rs b/warpgate/src/main.rs new file mode 100644 index 0000000..24f5acd --- /dev/null +++ b/warpgate/src/main.rs @@ -0,0 +1,68 @@ +#![feature(type_alias_impl_trait, let_else)] +mod commands; +mod config; +mod logging; +use anyhow::Result; +use clap::StructOpt; +use logging::init_logging; +use std::path::PathBuf; +use tracing::*; + +#[cfg(feature = "dhat-heap")] +#[global_allocator] +static ALLOC: dhat::Alloc = dhat::Alloc; + +#[derive(clap::Parser)] +#[clap(author, version, about, long_about = None)] +#[clap(propagate_version = true)] +struct Cli { + #[clap(subcommand)] + command: Commands, + + #[clap(long, short, default_value = "/etc/warpgate.yaml")] + config: PathBuf, +} + +#[derive(clap::Subcommand)] +enum Commands { + /// Run first-time setup and generate a config file + Setup, + /// Show Warpgate's SSH client keys + ClientKeys, + /// Run Warpgate + Run, + /// Create a password hash for use in the config file + Hash, + /// Validate config file + Check, + /// Test the connection to a target host + TestTarget { target_name: String }, +} + +async fn _main() -> Result<()> { + let cli = Cli::parse(); + + match &cli.command { + Commands::Run => crate::commands::run::command(&cli).await, + Commands::Hash => crate::commands::hash::command().await, + Commands::Check => crate::commands::check::command(&cli).await, + Commands::TestTarget { target_name } => { + crate::commands::test_target::command(&cli, target_name).await + } + Commands::Setup => crate::commands::setup::command(&cli).await, + Commands::ClientKeys => crate::commands::client_keys::command(&cli).await, + } +} + +#[tokio::main] +async fn main() { + #[cfg(feature = "dhat-heap")] + let _profiler = dhat::Profiler::new_heap(); + + init_logging(); + + if let Err(error) = _main().await { + error!(?error, "Fatal error"); + std::process::exit(1); + } +}