commit 4ccf2b0437413732db352a4a0e00c02c97eb74d9 Author: Eugene Pankov Date: Sun Apr 10 22:58:58 2022 +0200 import 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 0000000..e673c91 Binary files /dev/null and b/warpgate-admin/app/src/assets/svelte.png differ 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); + } +}