├── .all-contributorsrc ├── .bumpversion.cfg ├── .cargo └── config.toml ├── .dockerignore ├── .flake8 ├── .github ├── FUNDING.yml ├── dependabot.yml ├── readme │ └── brand-dark.svg └── workflows │ ├── build.yml │ ├── dependency-review.yml │ ├── docker.yml │ ├── release.yml │ ├── reprotest.yml │ └── test.yml ├── .gitignore ├── .well-known └── funding-manifest-urls ├── Cargo.lock ├── Cargo.toml ├── Cranky.toml ├── Cross.toml ├── LICENSE ├── README.md ├── SECURITY.md ├── clippy.toml ├── deny.toml ├── docker ├── Dockerfile ├── Dockerfile.builder └── docker-compose.yml ├── justfile ├── rust-toolchain ├── rustfmt.toml ├── sonar-project.properties ├── tests ├── .gitignore ├── Makefile ├── __init__.py ├── api_client.py ├── api_sdk │ ├── .gitkeep │ └── __init__.py ├── certs │ ├── tls.certificate.pem │ └── tls.key.pem ├── conftest.py ├── images │ ├── mysql-server │ │ ├── Dockerfile │ │ └── init.sql │ ├── postgres-server │ │ ├── Dockerfile │ │ └── init.sql │ └── ssh-server │ │ └── Dockerfile ├── oidc-mock │ ├── clients-config.json │ └── docker-compose.yml ├── poetry.lock ├── pyproject.toml ├── run.sh ├── ssh-keys │ ├── id_ed25519 │ ├── id_ed25519.pub │ ├── id_rsa │ ├── id_rsa.pub │ └── wg │ │ ├── client-ed25519 │ │ ├── client-ed25519.pub │ │ ├── client-rsa │ │ ├── client-rsa.pub │ │ ├── host-ed25519 │ │ ├── host-ed25519.pub │ │ └── host-rsa ├── test_api_auth.py ├── test_http_basic.py ├── test_http_common.py ├── test_http_conntest.py ├── test_http_cookies.py ├── test_http_redirects.py ├── test_http_user_auth_logout.py ├── test_http_user_auth_otp.py ├── test_http_user_auth_password.py ├── test_http_user_auth_ticket.py ├── test_http_websocket.py ├── test_mysql_user_auth_password.py ├── test_postgres_user_auth_in_browser.py ├── test_postgres_user_auth_password.py ├── test_ssh_conntest.py ├── test_ssh_proto.py ├── test_ssh_target_selection.py ├── test_ssh_user_auth_in_browser.py ├── test_ssh_user_auth_otp.py ├── test_ssh_user_auth_password.py ├── test_ssh_user_auth_pubkey.py ├── test_ssh_user_auth_ticket.py └── util.py ├── warpgate-admin ├── Cargo.toml └── src │ ├── api │ ├── known_hosts_detail.rs │ ├── known_hosts_list.rs │ ├── logs.rs │ ├── mod.rs │ ├── otp_credentials.rs │ ├── pagination.rs │ ├── parameters.rs │ ├── password_credentials.rs │ ├── public_key_credentials.rs │ ├── recordings_detail.rs │ ├── roles.rs │ ├── sessions_detail.rs │ ├── sessions_list.rs │ ├── ssh_connection_test.rs │ ├── ssh_keys.rs │ ├── sso_credentials.rs │ ├── targets.rs │ ├── tickets_detail.rs │ ├── tickets_list.rs │ └── users.rs │ ├── lib.rs │ └── main.rs ├── warpgate-common ├── Cargo.toml └── src │ ├── auth │ ├── cred.rs │ ├── mod.rs │ ├── policy.rs │ ├── selector.rs │ └── state.rs │ ├── config │ ├── defaults.rs │ ├── mod.rs │ └── target.rs │ ├── consts.rs │ ├── error.rs │ ├── eventhub.rs │ ├── helpers │ ├── fs.rs │ ├── hash.rs │ ├── mod.rs │ ├── otp.rs │ ├── rng.rs │ ├── serde_base64.rs │ └── serde_base64_secret.rs │ ├── lib.rs │ ├── tls │ ├── cert.rs │ ├── error.rs │ ├── maybe_tls_stream.rs │ ├── mod.rs │ ├── rustls_helpers.rs │ └── rustls_root_certs.rs │ ├── try_macro.rs │ ├── types │ ├── aliases.rs │ ├── listen_endpoint.rs │ ├── mod.rs │ └── secret.rs │ └── version.rs ├── warpgate-core ├── Cargo.toml └── src │ ├── auth_state_store.rs │ ├── config_providers │ ├── db.rs │ └── mod.rs │ ├── consts.rs │ ├── data.rs │ ├── db │ └── mod.rs │ ├── lib.rs │ ├── logging │ ├── database.rs │ ├── layer.rs │ ├── mod.rs │ ├── socket.rs │ └── values.rs │ ├── protocols │ ├── handle.rs │ └── mod.rs │ ├── recordings │ ├── mod.rs │ ├── terminal.rs │ ├── traffic.rs │ └── writer.rs │ ├── services.rs │ └── state.rs ├── warpgate-database-protocols ├── Cargo.toml ├── README.md └── src │ ├── error.rs │ ├── io │ ├── buf.rs │ ├── buf_mut.rs │ ├── buf_stream.rs │ ├── decode.rs │ ├── encode.rs │ ├── mod.rs │ └── write_and_flush.rs │ ├── lib.rs │ └── mysql │ ├── collation.rs │ ├── io │ ├── buf.rs │ ├── buf_mut.rs │ └── mod.rs │ ├── mod.rs │ └── protocol │ ├── auth.rs │ ├── capabilities.rs │ ├── connect │ ├── auth_switch.rs │ ├── handshake.rs │ ├── handshake_response.rs │ ├── mod.rs │ └── ssl_request.rs │ ├── mod.rs │ ├── packet.rs │ ├── response │ ├── eof.rs │ ├── err.rs │ ├── mod.rs │ ├── ok.rs │ └── status.rs │ ├── row.rs │ └── text │ ├── column.rs │ ├── mod.rs │ ├── ping.rs │ ├── query.rs │ └── quit.rs ├── warpgate-db-entities ├── Cargo.toml └── src │ ├── ApiToken.rs │ ├── KnownHost.rs │ ├── LogEntry.rs │ ├── OtpCredential.rs │ ├── Parameters.rs │ ├── PasswordCredential.rs │ ├── PublicKeyCredential.rs │ ├── Recording.rs │ ├── Role.rs │ ├── Session.rs │ ├── SsoCredential.rs │ ├── Target.rs │ ├── TargetRoleAssignment.rs │ ├── Ticket.rs │ ├── User.rs │ ├── UserRoleAssignment.rs │ └── lib.rs ├── warpgate-db-migrations ├── Cargo.toml ├── README.md └── src │ ├── lib.rs │ ├── m00001_create_ticket.rs │ ├── m00002_create_session.rs │ ├── m00003_create_recording.rs │ ├── m00004_create_known_host.rs │ ├── m00005_create_log_entry.rs │ ├── m00006_add_session_protocol.rs │ ├── m00007_targets_and_roles.rs │ ├── m00008_users.rs │ ├── m00009_credential_models.rs │ ├── m00010_parameters.rs │ ├── m00011_rsa_key_algos.rs │ ├── m00012_add_openssh_public_key_label.rs │ ├── m00013_add_openssh_public_key_dates.rs │ ├── m00014_api_tokens.rs │ ├── m00015_fix_public_key_dates.rs │ ├── m00016_fix_public_key_length.rs │ ├── m00017_descriptions.rs │ ├── m00018_ticket_description.rs │ └── main.rs ├── warpgate-protocol-http ├── Cargo.toml └── src │ ├── api │ ├── api_tokens.rs │ ├── auth.rs │ ├── common.rs │ ├── credentials.rs │ ├── info.rs │ ├── mod.rs │ ├── sso_provider_detail.rs │ ├── sso_provider_list.rs │ └── targets_list.rs │ ├── catchall.rs │ ├── common.rs │ ├── error.rs │ ├── lib.rs │ ├── logging.rs │ ├── main.rs │ ├── middleware │ ├── cookie_host.rs │ ├── mod.rs │ └── ticket.rs │ ├── proxy.rs │ ├── session.rs │ └── session_handle.rs ├── warpgate-protocol-mysql ├── Cargo.toml └── src │ ├── client.rs │ ├── common.rs │ ├── error.rs │ ├── lib.rs │ ├── session.rs │ ├── session_handle.rs │ └── stream.rs ├── warpgate-protocol-postgres ├── Cargo.toml └── src │ ├── client.rs │ ├── common.rs │ ├── error.rs │ ├── lib.rs │ ├── session.rs │ ├── session_handle.rs │ └── stream.rs ├── warpgate-protocol-ssh ├── Cargo.toml └── src │ ├── client │ ├── channel_direct_tcpip.rs │ ├── channel_session.rs │ ├── error.rs │ ├── handler.rs │ └── mod.rs │ ├── common.rs │ ├── compat.rs │ ├── keys.rs │ ├── known_hosts.rs │ ├── lib.rs │ └── server │ ├── channel_writer.rs │ ├── mod.rs │ ├── russh_handler.rs │ ├── service_output.rs │ ├── session.rs │ └── session_handle.rs ├── warpgate-sso ├── Cargo.toml └── src │ ├── config.rs │ ├── error.rs │ ├── lib.rs │ ├── request.rs │ ├── response.rs │ └── sso.rs ├── warpgate-web ├── .editorconfig ├── .gitignore ├── Cargo.toml ├── eslint.config.mjs ├── openapitools.json ├── package-lock.json ├── package.json ├── public │ └── assets │ │ ├── brand.svg │ │ └── favicon.svg ├── src │ ├── admin │ │ ├── App.svelte │ │ ├── AuthPolicyEditor.svelte │ │ ├── CreateOtpModal.svelte │ │ ├── CreatePasswordModal.svelte │ │ ├── CredentialEditor.svelte │ │ ├── Home.svelte │ │ ├── Log.svelte │ │ ├── LogViewer.svelte │ │ ├── PublicKeyCredentialModal.svelte │ │ ├── Recording.svelte │ │ ├── RelativeDate.svelte │ │ ├── Session.svelte │ │ ├── SsoCredentialModal.svelte │ │ ├── TlsConfiguration.svelte │ │ ├── config │ │ │ ├── Config.svelte │ │ │ ├── CreateRole.svelte │ │ │ ├── CreateTarget.svelte │ │ │ ├── CreateTicket.svelte │ │ │ ├── CreateUser.svelte │ │ │ ├── Parameters.svelte │ │ │ ├── Role.svelte │ │ │ ├── Roles.svelte │ │ │ ├── SSHKeys.svelte │ │ │ ├── Tickets.svelte │ │ │ ├── User.svelte │ │ │ ├── Users.svelte │ │ │ └── targets │ │ │ │ ├── Target.svelte │ │ │ │ ├── Targets.svelte │ │ │ │ └── ssh │ │ │ │ ├── KeyChecker.svelte │ │ │ │ ├── KeyCheckerResult.svelte │ │ │ │ └── Options.svelte │ │ ├── index.html │ │ ├── index.ts │ │ ├── lib │ │ │ ├── api.ts │ │ │ ├── openapi-schema.json │ │ │ └── time.ts │ │ └── player │ │ │ └── TerminalRecordingPlayer.svelte │ ├── common │ │ ├── AsyncButton.svelte │ │ ├── AuthBar.svelte │ │ ├── Brand.svelte │ │ ├── ConnectionInstructions.svelte │ │ ├── CopyButton.svelte │ │ ├── CredentialUsedStateBadge.svelte │ │ ├── DelayedSpinner.svelte │ │ ├── EmptyState.svelte │ │ ├── GettingStarted.svelte │ │ ├── ItemList.svelte │ │ ├── Loadable.svelte │ │ ├── NavListItem.svelte │ │ ├── Pagination.svelte │ │ ├── RadioButton.svelte │ │ ├── ThemeSwitcher.svelte │ │ ├── autosave.ts │ │ ├── errors.ts │ │ ├── protocols.ts │ │ └── sveltestrap-s5-ports │ │ │ ├── Alert.svelte │ │ │ ├── Badge.svelte │ │ │ ├── ModalHeader.svelte │ │ │ ├── Tooltip.svelte │ │ │ └── _sveltestrapUtils.ts │ ├── embed │ │ ├── EmbeddedUI.svelte │ │ └── index.ts │ ├── gateway │ │ ├── ApiTokenManager.svelte │ │ ├── App.svelte │ │ ├── CreateApiTokenModal.svelte │ │ ├── CredentialManager.svelte │ │ ├── Login.svelte │ │ ├── OutOfBandAuth.svelte │ │ ├── Profile.svelte │ │ ├── ProfileApiTokens.svelte │ │ ├── ProfileCredentials.svelte │ │ ├── TargetList.svelte │ │ ├── index.html │ │ ├── index.ts │ │ ├── lib │ │ │ ├── api.ts │ │ │ ├── openapi-schema.json │ │ │ ├── shellEscape.ts │ │ │ └── store.ts │ │ └── login.ts │ ├── lib.rs │ ├── theme │ │ ├── _theme.scss │ │ ├── fonts.css │ │ ├── index.ts │ │ ├── theme.dark.scss │ │ ├── theme.light.scss │ │ ├── vars.common.scss │ │ ├── vars.dark.scss │ │ └── vars.light.scss │ └── vite-env.d.ts ├── svelte.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts └── warpgate ├── .gitignore ├── Cargo.toml └── src ├── commands ├── check.rs ├── client_keys.rs ├── common.rs ├── mod.rs ├── recover_access.rs ├── run.rs ├── setup.rs └── test_target.rs ├── config.rs ├── logging.rs ├── main.rs └── protocols.rs /.bumpversion.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.14.0-beta.2 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:warpgate/Cargo.toml] 7 | search = version = "{current_version}" 8 | replace = version = "{new_version}" 9 | 10 | [bumpversion:file:warpgate-admin/Cargo.toml] 11 | search = version = "{current_version}" 12 | replace = version = "{new_version}" 13 | 14 | [bumpversion:file:warpgate-common/Cargo.toml] 15 | search = version = "{current_version}" 16 | replace = version = "{new_version}" 17 | 18 | [bumpversion:file:warpgate-core/Cargo.toml] 19 | search = version = "{current_version}" 20 | replace = version = "{new_version}" 21 | 22 | [bumpversion:file:warpgate-database-protocols/Cargo.toml] 23 | search = version = "{current_version}" 24 | replace = version = "{new_version}" 25 | 26 | [bumpversion:file:warpgate-db-entities/Cargo.toml] 27 | search = version = "{current_version}" 28 | replace = version = "{new_version}" 29 | 30 | [bumpversion:file:warpgate-db-migrations/Cargo.toml] 31 | search = version = "{current_version}" 32 | replace = version = "{new_version}" 33 | 34 | [bumpversion:file:warpgate-protocol-http/Cargo.toml] 35 | search = version = "{current_version}" 36 | replace = version = "{new_version}" 37 | 38 | [bumpversion:file:warpgate-protocol-mysql/Cargo.toml] 39 | search = version = "{current_version}" 40 | replace = version = "{new_version}" 41 | 42 | [bumpversion:file:warpgate-protocol-postgres/Cargo.toml] 43 | search = version = "{current_version}" 44 | replace = version = "{new_version}" 45 | 46 | [bumpversion:file:warpgate-protocol-ssh/Cargo.toml] 47 | search = version = "{current_version}" 48 | replace = version = "{new_version}" 49 | 50 | [bumpversion:file:warpgate-sso/Cargo.toml] 51 | search = version = "{current_version}" 52 | replace = version = "{new_version}" 53 | 54 | [bumpversion:file:warpgate-web/Cargo.toml] 55 | search = version = "{current_version}" 56 | replace = version = "{new_version}" 57 | -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | # https://github.com/rust-lang/cargo/issues/5376#issuecomment-2163350032 2 | [target.'cfg(all())'] 3 | rustflags = [ 4 | "--cfg", "tokio_unstable", 5 | "-Zremap-cwd-prefix=/reproducible-cwd", 6 | "--remap-path-prefix=$HOME=/reproducible-home", 7 | "--remap-path-prefix=$PWD=/reproducible-pwd", 8 | ] 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | target 4 | */target 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | temp 13 | host_key* 14 | .vscode 15 | 16 | # --- 17 | 18 | data 19 | config.*.yaml 20 | config.yaml 21 | 22 | .git 23 | warpgate-web/dist 24 | warpgate-web/node_modules 25 | warpgate-web/src/admin/lib/api-client/ 26 | warpgate-web/src/gateway/lib/api-client/ 27 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore=E501,D103,C901,D203,W504,S607,S603,S404,S606,S322,S410,S320,B010 3 | exclude = .git,__pycache__,help,static,misc,locale,templates,tests,deployment,migrations,elements/ai/scripts 4 | max-complexity = 40 5 | builtins = _ 6 | per-file-ignores = scripts/*:T001,E402 7 | select = C,E,F,W,B,B902 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: eugeny 2 | open_collective: tabby 3 | ko_fi: eugeny 4 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # Please see the documentation for all configuration options: 2 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 3 | 4 | version: 2 5 | updates: 6 | - package-ecosystem: "cargo" 7 | directory: "/" 8 | labels: ["type/deps"] 9 | #open-pull-requests-limit: 25 10 | schedule: 11 | interval: "daily" 12 | groups: 13 | version-bumps: 14 | applies-to: version-updates 15 | update-types: 16 | - minor 17 | - patch 18 | - package-ecosystem: "npm" 19 | directory: "/warpgate-web" 20 | labels: ["type/deps"] 21 | #open-pull-requests-limit: 25 22 | groups: 23 | version-bumps: 24 | applies-to: version-updates 25 | update-types: 26 | - minor 27 | - patch 28 | schedule: 29 | interval: "daily" 30 | -------------------------------------------------------------------------------- /.github/workflows/dependency-review.yml: -------------------------------------------------------------------------------- 1 | # Dependency Review Action 2 | # 3 | # 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. 4 | # 5 | # Source repository: https://github.com/actions/dependency-review-action 6 | # Public documentation: https://docs.github.com/en/code-security/supply-chain-security/understanding-your-software-supply-chain/about-dependency-review#dependency-review-enforcement 7 | name: 'Dependency Review' 8 | on: [pull_request] 9 | 10 | permissions: 11 | contents: read 12 | 13 | jobs: 14 | dependency-review: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: 'Checkout Repository' 18 | uses: actions/checkout@v3 19 | - name: 'Dependency Review' 20 | uses: actions/dependency-review-action@v1 21 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: "Release" 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*" 7 | 8 | jobs: 9 | tagged-release: 10 | name: "Tagged Release" 11 | runs-on: "ubuntu-latest" 12 | 13 | steps: 14 | - uses: "marvinpinto/action-automatic-releases@latest" 15 | with: 16 | repo_token: "${{ secrets.GITHUB_TOKEN }}" 17 | prerelease: false 18 | draft: true 19 | -------------------------------------------------------------------------------- /.github/workflows/reprotest.yml: -------------------------------------------------------------------------------- 1 | name: Reproducibility test 2 | permissions: 3 | contents: read 4 | 5 | on: workflow_dispatch 6 | 7 | jobs: 8 | reprotest: 9 | name: Reproducibility test 10 | runs-on: ubuntu-24.04 11 | 12 | steps: 13 | - name: Setup 14 | run: | 15 | sudo apt update 16 | sudo apt install --no-install-recommends -y libssl-dev pkg-config disorderfs faketime locales-all reprotest diffoscope 17 | test -c /dev/fuse || mknod -m 666 /dev/fuse c 10 229 18 | test -f /etc/mtab || ln -s ../proc/self/mounts /etc/mtab 19 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sudo sh -s -- -y 20 | echo "/root/.cargo/bin" >> $GITHUB_PATH 21 | 22 | - uses: actions/checkout@v2 23 | with: 24 | submodules: recursive 25 | 26 | - name: Install tools 27 | run: | 28 | sudo env "PATH=$PATH" cargo install just 29 | 30 | - name: Reprotest 31 | run: | 32 | sudo ulimit -n 999999 33 | sudo env "PATH=$PATH" reprotest -vv --min-cpus=99999 --vary=environment,build_path,kernel,aslr,num_cpus,-time,-user_group,fileordering,domain_host,home,locales,exec_path,timezone,umask --build-command 'just npm ci; just npm run build; SOURCE_DATE_EPOCH=0 cargo build --all-features --release' . target/release/warpgate 34 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | Tests: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | submodules: recursive 12 | 13 | - uses: Swatinem/rust-cache@v2 14 | with: 15 | key: "test" 16 | 17 | - name: Install build deps 18 | run: | 19 | sudo apt-get install openssh-client expect 20 | cargo install just 21 | cargo install cargo-llvm-cov 22 | cargo clean 23 | rustup component add llvm-tools-preview 24 | 25 | - name: Build UI 26 | run: | 27 | just npm ci 28 | just openapi 29 | just npm run openapi:tests-sdk 30 | just npm run build 31 | 32 | - name: Build images 33 | working-directory: tests 34 | run: | 35 | make all 36 | 37 | - name: Install deps 38 | working-directory: tests 39 | run: | 40 | sudo apt update 41 | sudo apt install -y gnome-keyring 42 | pip3 install keyring==24 poetry==1.8.3 43 | poetry install 44 | 45 | - name: Run 46 | working-directory: tests 47 | run: | 48 | TIMEOUT=120 poetry run ./run.sh 49 | cargo llvm-cov report --lcov > coverage.lcov 50 | 51 | - name: Upload coverage 52 | uses: actions/upload-artifact@master 53 | with: 54 | name: coverage.lcov 55 | path: tests/coverage.lcov 56 | 57 | - name: Upload coverage (HTML) 58 | uses: actions/upload-artifact@master 59 | with: 60 | name: coverage-html 61 | path: target/llvm-cov/html 62 | 63 | - name: SonarCloud Scan 64 | uses: SonarSource/sonarqube-scan-action@v4.2.1 65 | if: github.repository_owner == 'warp-tech' && github.actor != 'dependabot[bot]' 66 | env: 67 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any 68 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | # MSVC Windows builds of rustc generate these, which store debugging information 10 | *.pdb 11 | 12 | temp 13 | host_key* 14 | .vscode 15 | 16 | # --- 17 | 18 | data 19 | data-* 20 | config.*.yaml 21 | config.yaml 22 | __pycache__ 23 | .pytest_cache 24 | dhat-heap.json 25 | 26 | # IntelliJ based IDEs 27 | .idea/ 28 | /.data/ 29 | 30 | 31 | cdx.xml 32 | *.cdx.xml 33 | -------------------------------------------------------------------------------- /.well-known/funding-manifest-urls: -------------------------------------------------------------------------------- 1 | https://null.page/funding.json 2 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | # cargo-features = ["profile-rustflags"] 2 | 3 | [workspace] 4 | members = [ 5 | "warpgate", 6 | "warpgate-admin", 7 | "warpgate-common", 8 | "warpgate-core", 9 | "warpgate-db-migrations", 10 | "warpgate-db-entities", 11 | "warpgate-database-protocols", 12 | "warpgate-protocol-http", 13 | "warpgate-protocol-mysql", 14 | "warpgate-protocol-postgres", 15 | "warpgate-protocol-ssh", 16 | "warpgate-sso", 17 | "warpgate-web", 18 | ] 19 | default-members = ["warpgate"] 20 | resolver = "2" 21 | 22 | [workspace.dependencies] 23 | bytes = "1.4" 24 | data-encoding = "2.3" 25 | serde = { version = "1.0", features = ["derive"] } 26 | serde_json = "1.0" 27 | russh = { version = "0.50.2", features = ["des"] } 28 | futures = "0.3" 29 | tokio-stream = { version = "0.1.17", features = ["net"] } 30 | tokio-rustls = "0.26" 31 | enum_dispatch = "0.3.13" 32 | rustls = "0.23" 33 | sqlx = { version = "0.8", features = ["tls-rustls-aws-lc-rs"] } 34 | sea-orm = { version = "1.0", default-features = false, features = ["runtime-tokio", "macros"] } 35 | sea-orm-migration = { version = "1.0", default-features = false, features = [ 36 | "cli", 37 | ] } 38 | poem = { version = "3.1", features = [ 39 | "cookie", 40 | "session", 41 | "anyhow", 42 | "websocket", 43 | "rustls", 44 | "embed", 45 | ] } 46 | password-hash = { version = "0.4", features = ["std"] } 47 | delegate = "0.13" 48 | tracing = "0.1" 49 | 50 | [profile.release] 51 | lto = true 52 | panic = "abort" 53 | strip = "debuginfo" 54 | 55 | [profile.coverage] 56 | inherits = "dev" 57 | # rustflags = ["-Cinstrument-coverage"] 58 | -------------------------------------------------------------------------------- /Cranky.toml: -------------------------------------------------------------------------------- 1 | deny = [ 2 | "unsafe_code", 3 | "clippy::unwrap_used", 4 | "clippy::expect_used", 5 | "clippy::panic", 6 | "clippy::indexing_slicing", 7 | "clippy::dbg_macro", 8 | ] 9 | -------------------------------------------------------------------------------- /Cross.toml: -------------------------------------------------------------------------------- 1 | [target.x86_64-unknown-linux-gnu] 2 | pre-build = ["apt-get update && apt-get install --assume-yes libz-dev"] 3 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | Please report vunerabilities using GitHub's Private Vulnerability Reporting tool. 6 | 7 | You can expect a response within a few days. 8 | 9 | --- 10 | 11 | Warpgate considers the following trusted inputs: 12 | 13 | * Contents of the connected database 14 | * Contents of the config file, as long as Warpgate does not fail to lock down its permissions. 15 | * HTTP requests made by a session previously authenticated by a user who has the `warpgate:admin` role. 16 | * Network infrastructure and actuality and stability of target IPs/hostnames. 17 | 18 | In particular, this does not include the traffic from known Warpgate targets. 19 | 20 | --- 21 | 22 | CNA: [GitHub](https://www.cve.org/PartnerInformation/ListofPartners/partner/GitHub_M) 23 | -------------------------------------------------------------------------------- /clippy.toml: -------------------------------------------------------------------------------- 1 | avoid-breaking-exported-api = false 2 | allow-unwrap-in-tests = true 3 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1.3-labs 2 | FROM rust:1.79.0-bullseye AS build 3 | 4 | ENV DEBIAN_FRONTEND noninteractive 5 | 6 | RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ 7 | && apt-get update \ 8 | && apt-get install -y ca-certificates-java nodejs openjdk-17-jdk \ 9 | && rm -rf /var/lib/apt/lists/* \ 10 | && cargo install just 11 | 12 | COPY . /opt/warpgate 13 | 14 | ENV SOURCE_DATE_EPOCH 0 # for rust-embed determinism 15 | RUN cd /opt/warpgate \ 16 | && just npm ci \ 17 | && just openapi \ 18 | && just npm run build \ 19 | && cargo build --features mysql,postgres --release 20 | 21 | FROM debian:bullseye-20221024 22 | LABEL maintainer=heywoodlh 23 | 24 | ENV DEBIAN_FRONTEND noninteractive 25 | RUN <"] 6 | 7 | [tool.poetry.dependencies] 8 | python = "^3.10" 9 | pytest = "^8" 10 | psutil = "^5.9.1" 11 | pyotp = "^2.6.0" 12 | paramiko = "^2.11.0" 13 | Flask = "^2.2.1" 14 | requests = "^2.28.1" 15 | flask-sock = "^0.5.2" 16 | websocket-client = "^1.3.3" 17 | PyYAML = "^6.0.2" 18 | openapi-client = { path = "./api_sdk", develop = true } 19 | aiohttp = "^3.11.18" 20 | 21 | [tool.poetry.dev-dependencies] 22 | flake8 = "^5.0.2" 23 | black = "^24" 24 | 25 | [tool.poetry.group.dev.dependencies] 26 | pytest-asyncio = "^0.26.0" 27 | pytest-timeout = "^2.4.0" 28 | 29 | [tool.pytest.ini_options] 30 | minversion = "6.0" 31 | filterwarnings = ["ignore::urllib3.exceptions.InsecureRequestWarning"] 32 | 33 | [build-system] 34 | requires = ["poetry-core>=1.0.0"] 35 | build-backend = "poetry.core.masonry.api" 36 | -------------------------------------------------------------------------------- /tests/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | cd .. 4 | rm target/llvm-cov-target/* || true 5 | cargo llvm-cov clean --workspace 6 | cargo llvm-cov --no-cfg-coverage-nightly --no-report --workspace --all-features -- --skip agent 7 | cd tests 8 | RUST_BACKTRACE=1 ENABLE_COVERAGE=1 poetry run pytest --timeout 60 $@ 9 | cargo llvm-cov report --html 10 | -------------------------------------------------------------------------------- /tests/ssh-keys/id_ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACAz/wkEtOQGGLxHmd1+hD0hOJYri7j+8Oqex4CMnTr26gAAAKC8ieiIvIno 4 | iAAAAAtzc2gtZWQyNTUxOQAAACAz/wkEtOQGGLxHmd1+hD0hOJYri7j+8Oqex4CMnTr26g 5 | AAAEBe+1fXsb/WtCsxt6nR5fVqIX9WHQqbpiVxxNTy41IsFDP/CQS05AYYvEeZ3X6EPSE4 6 | liuLuP7w6p7HgIydOvbqAAAAHGV1Z2VuZUBFdWdlbmVzLU1CUC5mcml0ei5ib3gB 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /tests/ssh-keys/id_ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDP/CQS05AYYvEeZ3X6EPSE4liuLuP7w6p7HgIydOvbq 2 | -------------------------------------------------------------------------------- /tests/ssh-keys/id_rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCd30zPwWdhmb+lcHUn7djHwgOxvy+6CAaYT/Elmb8fS+4IPi8D9oOzeB2pcuNTX+7IPQn8JA8fga82oCQnwCK8zZWGcWwbVAkuSWRcQd4EWVPzWbE7a3QMHhLpBBeBtgLte3JjqBWpFi4xP5ahqfMlxgy/eBIYWv9ifxadViXffOwPxi0uXcq7vX7mTKQUL/z8uXE5RjvdfNM9W4KERO/PsqEquUGIeE5DCPLHYHUl0ZCnmFKsFNKY7f4iYgywKWxDDd8/S9zuy3svTp44E1OuE86JCqPKfkygJ+ghEC7q4TugfKuwt9iJmslbJLP5N1lk150p9DFYNcFwrn7SnxxjJL9T7IYdGtKNIe2X6qpLxBgJN2W15H8QNK487vITre1Gk5GDovmSq4C6mt4fqu2A7QenP/pleP1uwBH4az4nozHOslZ4OIDFQCS+D1qdiCPeyQMUGx/KVV7Z57LV9C3HT+KIo2MXKumoc8BQjIldrlBOmWlk+Jwoylr5ffyrqB8= 2 | -------------------------------------------------------------------------------- /tests/ssh-keys/wg/client-ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACDujCSXcfts0V3KEZ/vc02DZ0cypyiOHQyNQgT3z35XbAAAAKBv+t0+b/rd 4 | PgAAAAtzc2gtZWQyNTUxOQAAACDujCSXcfts0V3KEZ/vc02DZ0cypyiOHQyNQgT3z35XbA 5 | AAAEDDVbCUHpecy/RHT/GFRXSnN+A0uEiI3xYwRuTIpgTXEO6MJJdx+2zRXcoRn+9zTYNn 6 | RzKnKI4dDI1CBPfPfldsAAAAGmV1Z2VuZUBFdWdlbmVzLU1CUC0yLmxvY2FsAQID 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /tests/ssh-keys/wg/client-ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIO6MJJdx+2zRXcoRn+9zTYNnRzKnKI4dDI1CBPfPflds eugene@Eugenes-MBP-2.local 2 | -------------------------------------------------------------------------------- /tests/ssh-keys/wg/client-rsa.pub: -------------------------------------------------------------------------------- 1 | ssh-rsa AAAADHJzYS1zaGEyLTI1NgAAAAMBAAEAAAIBAK+6SGPicZ/aXmd46OWiWe3H7jbJo7PzCYrNzyFEyDw3sasjie0ZwrueSB8C431rg34AhqW4CM8V4kC0ZIai+0uujgBScWyskxU3xPl4SFKATjySj4uKaLcliwLRV/1gedWHy3uiKh6uJIu7IDJNafKSZAhlxiBbWvduDztj2oIhx5clDByMz3tzM2ByTFHiTxr2Nr/yS/eyk1w0WXs1HuhnhzI59uern4UkI+iE2i+JqEy2rJXAZW4heF9NJ2gZF7qZFtsfIra4AfDYxQtRfgTXy/vZwYMjG5Y2puZ2MmjOVgpp/7HKRVnr6O/5auwvOIqaz1JZzDnjM26FHZhmf3b/Jb5ucGoyO2xkXGC8MEcQzQohlS69tXzhX+GyTAUCjfHI2nL3HouGUnXv8yQoqZ8Z5DbtGOwjxgkWaZqSBIfOmDr16qV1/iSCwAav8z1Thk0Tiet9CcpRjwA37rymnygpgVO2scnybQFjzVIYzHKemM0Qk8cG/uBgMNQljyoCHKkg/inEdGiEm7pKITU4SiXhm1DMTNIoozHGWRt6tq0S/NwKrqXt3UpLbVyZ4B22XuNIxVhOKPvsHawOeyaTk8Pvr3ABYlTlPwlovhsmJ3jn5Qp5nPxVDHd7maJL/JoaPW5oPqvKFcJnBCHR/2Eu+ik4Y0wSbTrerPfeW8HKz5MP 2 | -------------------------------------------------------------------------------- /tests/ssh-keys/wg/host-ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACDBXU9RmJEViQhZZYvaIyEnEMb1X1VCchYGTbyHqbyK6AAAAKBBjcVuQY3F 4 | bgAAAAtzc2gtZWQyNTUxOQAAACDBXU9RmJEViQhZZYvaIyEnEMb1X1VCchYGTbyHqbyK6A 5 | AAAEBeZZ1uCofYbG7ypyBBrHGlcggJRbFFFzqGrIxST/B9ksFdT1GYkRWJCFlli9ojIScQ 6 | xvVfVUJyFgZNvIepvIroAAAAGmV1Z2VuZUBFdWdlbmVzLU1CUC0yLmxvY2FsAQID 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /tests/ssh-keys/wg/host-ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIMFdT1GYkRWJCFlli9ojIScQxvVfVUJyFgZNvIepvIro eugene@Eugenes-MBP-2.local 2 | -------------------------------------------------------------------------------- /tests/test_api_auth.py: -------------------------------------------------------------------------------- 1 | import contextlib 2 | from .api_client import sdk 3 | from .conftest import WarpgateProcess 4 | from .test_http_common import * # noqa 5 | 6 | 7 | @contextlib.contextmanager 8 | def assert_401(): 9 | with pytest.raises(sdk.ApiException) as e: 10 | yield 11 | assert e.value.status == 401 12 | 13 | 14 | class TestAPIAuth: 15 | def test_unavailable_without_auth( 16 | self, 17 | shared_wg: WarpgateProcess, 18 | ): 19 | url = f"https://localhost:{shared_wg.http_port}" 20 | 21 | config = sdk.Configuration( 22 | host=f"{url}/@warpgate/admin/api", 23 | ) 24 | config.verify_ssl = False 25 | 26 | with sdk.ApiClient(config) as api_client: 27 | api = sdk.DefaultApi(api_client) 28 | with assert_401(): 29 | api.get_parameters() 30 | with assert_401(): 31 | api.get_role("1") 32 | with assert_401(): 33 | api.get_roles() 34 | with assert_401(): 35 | api.get_user("1") 36 | with assert_401(): 37 | api.get_users() 38 | with assert_401(): 39 | api.get_target("1") 40 | with assert_401(): 41 | api.get_targets() 42 | with assert_401(): 43 | api.get_session("1") 44 | with assert_401(): 45 | api.get_sessions() 46 | -------------------------------------------------------------------------------- /tests/test_http_common.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | import threading 3 | 4 | from .util import alloc_port 5 | 6 | 7 | @pytest.fixture(scope="session") 8 | def echo_server_port(): 9 | from flask import Flask, request, jsonify, redirect 10 | from flask_sock import Sock 11 | 12 | app = Flask(__name__) 13 | sock = Sock(app) 14 | 15 | @app.route("/set-cookie") 16 | def set_cookie(): 17 | response = jsonify({}) 18 | response.set_cookie("cookie", "value") 19 | return response 20 | 21 | @app.route("/redirect/") 22 | def r(url): 23 | return redirect(url) 24 | 25 | @app.route("/", defaults={"path": ""}) 26 | @app.route("/") 27 | def echo(path): 28 | return jsonify( 29 | { 30 | "method": request.method, 31 | "args": request.args, 32 | "path": request.path, 33 | "headers": request.headers.to_wsgi_list(), 34 | } 35 | ) 36 | 37 | @sock.route("/socket") 38 | def ws_echo(ws): 39 | while True: 40 | data = ws.receive() 41 | ws.send(data) 42 | 43 | port = alloc_port() 44 | 45 | def runner(): 46 | app.run(port=port, load_dotenv=False) 47 | 48 | thread = threading.Thread(target=runner, daemon=True) 49 | thread.start() 50 | 51 | yield port 52 | -------------------------------------------------------------------------------- /tests/test_http_conntest.py: -------------------------------------------------------------------------------- 1 | from uuid import uuid4 2 | 3 | from .api_client import admin_client, sdk 4 | from .conftest import ProcessManager, WarpgateProcess 5 | 6 | 7 | class Test: 8 | def test_success( 9 | self, 10 | processes: ProcessManager, 11 | echo_server_port, 12 | timeout, 13 | shared_wg: WarpgateProcess, 14 | ): 15 | url = f"https://localhost:{shared_wg.http_port}" 16 | with admin_client(url) as api: 17 | echo_target = api.create_target(sdk.TargetDataRequest( 18 | name=f"echo-{uuid4()}", 19 | options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( 20 | kind="Http", 21 | url=f"http://localhost:{echo_server_port}", 22 | tls=sdk.Tls( 23 | mode=sdk.TlsMode.DISABLED, 24 | verify=False, 25 | ), 26 | )), 27 | )) 28 | 29 | proc = processes.start_wg( 30 | share_with=shared_wg, 31 | args=["test-target", echo_target.name], 32 | ).process 33 | proc.wait(timeout=timeout) 34 | assert proc.returncode == 0 35 | 36 | def test_fail_no_connection( 37 | self, processes: ProcessManager, timeout, shared_wg: WarpgateProcess 38 | ): 39 | url = f"https://localhost:{shared_wg.http_port}" 40 | with admin_client(url) as api: 41 | echo_target = api.create_target(sdk.TargetDataRequest( 42 | name=f"echo-{uuid4()}", 43 | options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( 44 | kind="Http", 45 | url="http://localhostbaddomain", 46 | tls=sdk.Tls( 47 | mode=sdk.TlsMode.DISABLED, 48 | verify=False, 49 | ), 50 | )), 51 | )) 52 | 53 | proc = processes.start_wg( 54 | share_with=shared_wg, 55 | args=["test-target", echo_target.name], 56 | ).process 57 | proc.wait(timeout=timeout) 58 | assert proc.returncode != 0 59 | -------------------------------------------------------------------------------- /tests/test_http_cookies.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from uuid import uuid4 3 | 4 | from .api_client import admin_client, sdk 5 | from .conftest import WarpgateProcess 6 | from .test_http_common import * # noqa 7 | 8 | 9 | class TestHTTPCookies: 10 | def test( 11 | self, 12 | echo_server_port, 13 | shared_wg: WarpgateProcess, 14 | ): 15 | url = f"https://localhost:{shared_wg.http_port}" 16 | 17 | with admin_client(url) as api: 18 | role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) 19 | user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) 20 | api.create_password_credential( 21 | user.id, sdk.NewPasswordCredential(password="123") 22 | ) 23 | api.add_user_role(user.id, role.id) 24 | echo_target = api.create_target(sdk.TargetDataRequest( 25 | name=f"echo-{uuid4()}", 26 | options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( 27 | kind="Http", 28 | url=f"http://localhost:{echo_server_port}", 29 | tls=sdk.Tls( 30 | mode=sdk.TlsMode.DISABLED, 31 | verify=False, 32 | ), 33 | )), 34 | )) 35 | api.add_target_role(echo_target.id, role.id) 36 | 37 | session = requests.Session() 38 | session.verify = False 39 | headers = {"Host": f"localhost:{shared_wg.http_port}"} 40 | 41 | session.post( 42 | f"{url}/@warpgate/api/auth/login", 43 | json={ 44 | "username": user.username, 45 | "password": "123", 46 | }, 47 | headers=headers, 48 | ) 49 | 50 | session.get( 51 | f"{url}/set-cookie?warpgate-target={echo_target.name}", headers=headers 52 | ) 53 | 54 | cookies = session.cookies.get_dict() 55 | assert cookies["cookie"] == "value" 56 | -------------------------------------------------------------------------------- /tests/test_http_redirects.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from uuid import uuid4 3 | 4 | from .api_client import admin_client, sdk 5 | from .conftest import WarpgateProcess 6 | from .test_http_common import * # noqa 7 | 8 | 9 | class TestHTTPRedirects: 10 | def test( 11 | self, 12 | shared_wg: WarpgateProcess, 13 | echo_server_port, 14 | ): 15 | url = f"https://localhost:{shared_wg.http_port}" 16 | with admin_client(url) as api: 17 | role = api.create_role(sdk.RoleDataRequest(name=f"role-{uuid4()}")) 18 | user = api.create_user(sdk.CreateUserRequest(username=f"user-{uuid4()}")) 19 | api.create_password_credential( 20 | user.id, sdk.NewPasswordCredential(password="123") 21 | ) 22 | api.add_user_role(user.id, role.id) 23 | echo_target = api.create_target(sdk.TargetDataRequest( 24 | name=f"echo-{uuid4()}", 25 | options=sdk.TargetOptions(sdk.TargetOptionsTargetHTTPOptions( 26 | kind="Http", 27 | url=f"http://localhost:{echo_server_port}", 28 | tls=sdk.Tls( 29 | mode=sdk.TlsMode.DISABLED, 30 | verify=False, 31 | ), 32 | )), 33 | )) 34 | api.add_target_role(echo_target.id, role.id) 35 | 36 | session = requests.Session() 37 | session.verify = False 38 | headers = {"Host": f"localhost:{shared_wg.http_port}"} 39 | 40 | session.post( 41 | f"{url}/@warpgate/api/auth/login", 42 | json={ 43 | "username": user.username, 44 | "password": "123", 45 | }, 46 | headers=headers, 47 | ) 48 | 49 | response = session.get( 50 | f"{url}/redirect/http://localhost:{echo_server_port}/test?warpgate-target={echo_target.name}", 51 | headers=headers, 52 | allow_redirects=False, 53 | ) 54 | 55 | assert response.headers["location"] == "/test" 56 | -------------------------------------------------------------------------------- /warpgate-admin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-admin" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | anyhow = { version = "1.0", features = ["std"] } 9 | async-trait = "0.1" 10 | bytes.workspace = true 11 | chrono = { version = "0.4", default-features = false } 12 | futures.workspace = true 13 | hex = "0.4" 14 | mime_guess = { version = "2.0", default-features = false } 15 | poem.workspace = true 16 | poem-openapi = { version = "5.1", features = [ 17 | "swagger-ui", 18 | "chrono", 19 | "uuid", 20 | "static-files", 21 | ] } 22 | russh.workspace = true 23 | rust-embed = "8.3" 24 | sea-orm.workspace = true 25 | serde.workspace = true 26 | serde_json.workspace = true 27 | thiserror = "1.0" 28 | tokio = { version = "1.20", features = ["tracing"] } 29 | tracing.workspace = true 30 | uuid = { version = "1.3", features = ["v4", "serde"] } 31 | warpgate-common = { version = "*", path = "../warpgate-common" } 32 | warpgate-core = { version = "*", path = "../warpgate-core" } 33 | warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" } 34 | warpgate-protocol-ssh = { version = "*", path = "../warpgate-protocol-ssh" } 35 | regex = "1.6" 36 | -------------------------------------------------------------------------------- /warpgate-admin/src/api/known_hosts_detail.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use poem::web::Data; 4 | use poem_openapi::param::Path; 5 | use poem_openapi::{ApiResponse, OpenApi}; 6 | use sea_orm::{DatabaseConnection, EntityTrait, ModelTrait}; 7 | use tokio::sync::Mutex; 8 | use uuid::Uuid; 9 | use warpgate_common::WarpgateError; 10 | 11 | use super::AnySecurityScheme; 12 | pub struct Api; 13 | 14 | #[derive(ApiResponse)] 15 | enum DeleteSSHKnownHostResponse { 16 | #[oai(status = 204)] 17 | Deleted, 18 | 19 | #[oai(status = 404)] 20 | NotFound, 21 | } 22 | 23 | #[OpenApi] 24 | impl Api { 25 | #[oai( 26 | path = "/ssh/known-hosts/:id", 27 | method = "delete", 28 | operation_id = "delete_ssh_known_host" 29 | )] 30 | async fn api_ssh_delete_known_host( 31 | &self, 32 | db: Data<&Arc>>, 33 | id: Path, 34 | _auth: AnySecurityScheme, 35 | ) -> Result { 36 | use warpgate_db_entities::KnownHost; 37 | let db = db.lock().await; 38 | 39 | let known_host = KnownHost::Entity::find_by_id(id.0).one(&*db).await?; 40 | 41 | match known_host { 42 | Some(known_host) => { 43 | known_host.delete(&*db).await?; 44 | Ok(DeleteSSHKnownHostResponse::Deleted) 45 | } 46 | None => Ok(DeleteSSHKnownHostResponse::NotFound), 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /warpgate-admin/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::auth::ApiKey; 2 | use poem_openapi::{OpenApi, SecurityScheme}; 3 | 4 | mod known_hosts_detail; 5 | mod known_hosts_list; 6 | mod logs; 7 | mod otp_credentials; 8 | mod pagination; 9 | mod parameters; 10 | mod password_credentials; 11 | mod public_key_credentials; 12 | pub mod recordings_detail; 13 | mod roles; 14 | mod sessions_detail; 15 | pub mod sessions_list; 16 | mod ssh_connection_test; 17 | mod ssh_keys; 18 | mod sso_credentials; 19 | mod targets; 20 | mod tickets_detail; 21 | mod tickets_list; 22 | pub mod users; 23 | 24 | #[derive(SecurityScheme)] 25 | #[oai(ty = "api_key", key_name = "X-Warpgate-Token", key_in = "header")] 26 | #[allow(dead_code)] 27 | pub struct TokenSecurityScheme(ApiKey); 28 | 29 | #[derive(SecurityScheme)] 30 | #[oai(ty = "api_key", key_name = "warpgate-http-session", key_in = "cookie")] 31 | #[allow(dead_code)] 32 | pub struct CookieSecurityScheme(ApiKey); 33 | 34 | #[derive(SecurityScheme)] 35 | #[allow(dead_code)] 36 | pub enum AnySecurityScheme { 37 | Token(TokenSecurityScheme), 38 | Cookie(CookieSecurityScheme), 39 | } 40 | 41 | pub fn get() -> impl OpenApi { 42 | ( 43 | (sessions_list::Api, sessions_detail::Api), 44 | recordings_detail::Api, 45 | (roles::ListApi, roles::DetailApi), 46 | (tickets_list::Api, tickets_detail::Api), 47 | (known_hosts_list::Api, known_hosts_detail::Api), 48 | ssh_keys::Api, 49 | logs::Api, 50 | (targets::ListApi, targets::DetailApi, targets::RolesApi), 51 | (users::ListApi, users::DetailApi, users::RolesApi), 52 | ( 53 | password_credentials::ListApi, 54 | password_credentials::DetailApi, 55 | ), 56 | (sso_credentials::ListApi, sso_credentials::DetailApi), 57 | ( 58 | public_key_credentials::ListApi, 59 | public_key_credentials::DetailApi, 60 | ), 61 | (otp_credentials::ListApi, otp_credentials::DetailApi), 62 | parameters::Api, 63 | ssh_connection_test::Api, 64 | ) 65 | } 66 | -------------------------------------------------------------------------------- /warpgate-admin/src/api/pagination.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::types::{ParseFromJSON, ToJSON}; 2 | use poem_openapi::Object; 3 | use sea_orm::{ConnectionTrait, EntityTrait, FromQueryResult, PaginatorTrait, QuerySelect, Select}; 4 | use warpgate_common::WarpgateError; 5 | 6 | #[derive(Object)] 7 | pub struct PaginatedResponse { 8 | items: Vec, 9 | offset: u64, 10 | total: u64, 11 | } 12 | 13 | pub struct PaginationParams { 14 | pub offset: Option, 15 | pub limit: Option, 16 | } 17 | 18 | impl PaginatedResponse { 19 | pub async fn new( 20 | query: Select, 21 | params: PaginationParams, 22 | db: &'_ C, 23 | postprocess: P, 24 | ) -> Result, WarpgateError> 25 | where 26 | E: EntityTrait, 27 | C: ConnectionTrait, 28 | M: FromQueryResult + Sized + Send + Sync + 'static, 29 | P: FnMut(E::Model) -> T, 30 | { 31 | let offset = params.offset.unwrap_or(0); 32 | let limit = params.limit.unwrap_or(100); 33 | 34 | let paginator = query.clone().paginate(db, limit); 35 | 36 | let total = paginator.num_items().await?; 37 | 38 | let query = query.offset(offset).limit(limit); 39 | 40 | let items = query.all(db).await?; 41 | 42 | let items = items.into_iter().map(postprocess).collect::>(); 43 | Ok(PaginatedResponse { 44 | items, 45 | offset, 46 | total, 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /warpgate-admin/src/api/ssh_keys.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use poem::web::Data; 4 | use poem_openapi::payload::Json; 5 | use poem_openapi::{ApiResponse, Object, OpenApi}; 6 | use russh::keys::PublicKeyBase64; 7 | use serde::Serialize; 8 | use tokio::sync::Mutex; 9 | use warpgate_common::{WarpgateConfig, WarpgateError}; 10 | 11 | use super::AnySecurityScheme; 12 | 13 | pub struct Api; 14 | 15 | #[derive(Serialize, Object)] 16 | struct SSHKey { 17 | pub kind: String, 18 | pub public_key_base64: String, 19 | } 20 | 21 | #[derive(ApiResponse)] 22 | enum GetSSHOwnKeysResponse { 23 | #[oai(status = 200)] 24 | Ok(Json>), 25 | } 26 | 27 | #[OpenApi] 28 | impl Api { 29 | #[oai( 30 | path = "/ssh/own-keys", 31 | method = "get", 32 | operation_id = "get_ssh_own_keys" 33 | )] 34 | async fn api_ssh_get_own_keys( 35 | &self, 36 | config: Data<&Arc>>, 37 | _auth: AnySecurityScheme, 38 | ) -> Result { 39 | let config = config.lock().await; 40 | let keys = warpgate_protocol_ssh::load_client_keys(&config)?; 41 | 42 | let keys = keys 43 | .into_iter() 44 | .map(|k| SSHKey { 45 | kind: k.algorithm().to_string(), 46 | public_key_base64: k.public_key_base64(), 47 | }) 48 | .collect(); 49 | Ok(GetSSHOwnKeysResponse::Ok(Json(keys))) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /warpgate-admin/src/api/tickets_detail.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use poem::web::Data; 4 | use poem_openapi::param::Path; 5 | use poem_openapi::{ApiResponse, OpenApi}; 6 | use sea_orm::{DatabaseConnection, EntityTrait, ModelTrait}; 7 | use tokio::sync::Mutex; 8 | use uuid::Uuid; 9 | use warpgate_common::WarpgateError; 10 | 11 | use super::AnySecurityScheme; 12 | 13 | pub struct Api; 14 | 15 | #[derive(ApiResponse)] 16 | enum DeleteTicketResponse { 17 | #[oai(status = 204)] 18 | Deleted, 19 | 20 | #[oai(status = 404)] 21 | NotFound, 22 | } 23 | 24 | #[OpenApi] 25 | impl Api { 26 | #[oai( 27 | path = "/tickets/:id", 28 | method = "delete", 29 | operation_id = "delete_ticket" 30 | )] 31 | async fn api_delete_ticket( 32 | &self, 33 | db: Data<&Arc>>, 34 | id: Path, 35 | _auth: AnySecurityScheme, 36 | ) -> Result { 37 | use warpgate_db_entities::Ticket; 38 | let db = db.lock().await; 39 | 40 | let ticket = Ticket::Entity::find_by_id(id.0).one(&*db).await?; 41 | 42 | match ticket { 43 | Some(ticket) => { 44 | ticket.delete(&*db).await?; 45 | Ok(DeleteTicketResponse::Deleted) 46 | } 47 | None => Ok(DeleteTicketResponse::NotFound), 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /warpgate-admin/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod api; 2 | use poem::{EndpointExt, IntoEndpoint, Route}; 3 | use poem_openapi::OpenApiService; 4 | use warpgate_common::version::warpgate_version; 5 | use warpgate_core::Services; 6 | 7 | pub fn admin_api_app(services: &Services) -> impl IntoEndpoint { 8 | let api_service = 9 | OpenApiService::new(crate::api::get(), "Warpgate admin API", warpgate_version()) 10 | .server("/@warpgate/admin/api"); 11 | 12 | let ui = api_service.swagger_ui(); 13 | let spec = api_service.spec_endpoint(); 14 | let db = services.db.clone(); 15 | let config = services.config.clone(); 16 | let config_provider = services.config_provider.clone(); 17 | let recordings = services.recordings.clone(); 18 | let state = services.state.clone(); 19 | 20 | Route::new() 21 | .nest("", api_service) 22 | .nest("/swagger", ui) 23 | .nest("/openapi.json", spec) 24 | .at( 25 | "/recordings/:id/cast", 26 | crate::api::recordings_detail::api_get_recording_cast, 27 | ) 28 | .at( 29 | "/recordings/:id/stream", 30 | crate::api::recordings_detail::api_get_recording_stream, 31 | ) 32 | .at( 33 | "/recordings/:id/tcpdump", 34 | crate::api::recordings_detail::api_get_recording_tcpdump, 35 | ) 36 | .at( 37 | "/sessions/changes", 38 | crate::api::sessions_list::api_get_sessions_changes_stream, 39 | ) 40 | .data(db) 41 | .data(config_provider) 42 | .data(state) 43 | .data(recordings) 44 | .data(config) 45 | } 46 | -------------------------------------------------------------------------------- /warpgate-admin/src/main.rs: -------------------------------------------------------------------------------- 1 | mod api; 2 | use poem_openapi::OpenApiService; 3 | use regex::Regex; 4 | use warpgate_common::version::warpgate_version; 5 | 6 | #[allow(clippy::unwrap_used)] 7 | pub fn main() { 8 | let api_service = OpenApiService::new(api::get(), "Warpgate Web Admin", warpgate_version()) 9 | .server("/@warpgate/admin/api"); 10 | 11 | let spec = api_service.spec(); 12 | let re = Regex::new(r"PaginatedResponse<(?P\w+)>").unwrap(); 13 | let spec = re.replace_all(&spec, "Paginated$name"); 14 | 15 | println!("{spec}"); 16 | } 17 | -------------------------------------------------------------------------------- /warpgate-common/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-common" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | argon2 = "0.5" 10 | async-trait = "0.1" 11 | bytes.workspace = true 12 | chrono = { version = "0.4", default-features = false, features = ["serde"] } 13 | data-encoding.workspace = true 14 | delegate.workspace = true 15 | humantime-serde = "1.1" 16 | futures.workspace = true 17 | once_cell = "1.17" 18 | password-hash.workspace = true 19 | poem = { version = "3.1", features = ["rustls"] } 20 | poem-openapi = { version = "5.1", features = [ 21 | "swagger-ui", 22 | "chrono", 23 | "uuid", 24 | "static-files", 25 | ] } 26 | rand = "0.8" 27 | rand_chacha = "0.3" 28 | rand_core = { version = "0.6", features = ["std"] } 29 | russh.workspace = true 30 | rustls-native-certs = "0.8" 31 | sea-orm.workspace = true 32 | serde.workspace = true 33 | serde_json.workspace = true 34 | thiserror = "1.0" 35 | tokio = { version = "1.20", features = ["tracing"] } 36 | tokio-rustls.workspace = true 37 | totp-rs = { version = "5.0", features = ["otpauth"] } 38 | tracing.workspace = true 39 | tracing-core = "0.1" 40 | url = "2.2" 41 | uuid = { version = "1.3", features = ["v4", "serde"] } 42 | warpgate-sso = { version = "*", path = "../warpgate-sso" } 43 | rustls.workspace = true 44 | rustls-pemfile = "1.0" 45 | webpki = "0.22" 46 | aho-corasick = "1.1.3" 47 | tokio-stream.workspace = true 48 | git-version = "0.3.9" 49 | -------------------------------------------------------------------------------- /warpgate-common/src/auth/cred.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use poem_openapi::Enum; 3 | use russh::keys::Algorithm; 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::Secret; 7 | 8 | #[derive(Debug, Deserialize, Serialize, Clone, Copy, PartialEq, Eq, Hash, Enum)] 9 | pub enum CredentialKind { 10 | #[serde(rename = "password")] 11 | Password, 12 | #[serde(rename = "publickey")] 13 | PublicKey, 14 | #[serde(rename = "otp")] 15 | Totp, 16 | #[serde(rename = "sso")] 17 | Sso, 18 | #[serde(rename = "web")] 19 | WebUserApproval, 20 | } 21 | 22 | #[derive(Debug, Clone, PartialEq, Eq)] 23 | pub enum AuthCredential { 24 | Otp(Secret), 25 | Password(Secret), 26 | PublicKey { 27 | kind: Algorithm, 28 | public_key_bytes: Bytes, 29 | }, 30 | Sso { 31 | provider: String, 32 | email: String, 33 | }, 34 | WebUserApproval, 35 | } 36 | 37 | impl AuthCredential { 38 | pub fn kind(&self) -> CredentialKind { 39 | match self { 40 | Self::Password { .. } => CredentialKind::Password, 41 | Self::PublicKey { .. } => CredentialKind::PublicKey, 42 | Self::Otp { .. } => CredentialKind::Totp, 43 | Self::Sso { .. } => CredentialKind::Sso, 44 | Self::WebUserApproval => CredentialKind::WebUserApproval, 45 | } 46 | } 47 | 48 | pub fn safe_description(&self) -> String { 49 | match self { 50 | Self::Password { .. } => "password".to_string(), 51 | Self::PublicKey { .. } => "public key".to_string(), 52 | Self::Otp { .. } => "one-time password".to_string(), 53 | Self::Sso { provider, .. } => format!("SSO ({provider})"), 54 | Self::WebUserApproval => "in-browser auth".to_string(), 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /warpgate-common/src/auth/mod.rs: -------------------------------------------------------------------------------- 1 | mod cred; 2 | mod policy; 3 | mod selector; 4 | mod state; 5 | pub use cred::*; 6 | pub use policy::*; 7 | pub use selector::*; 8 | pub use state::*; 9 | -------------------------------------------------------------------------------- /warpgate-common/src/auth/selector.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use crate::consts::TICKET_SELECTOR_PREFIX; 4 | use crate::Secret; 5 | 6 | pub enum AuthSelector { 7 | User { 8 | username: String, 9 | target_name: String, 10 | }, 11 | Ticket { 12 | secret: Secret, 13 | }, 14 | } 15 | 16 | impl> From for AuthSelector { 17 | fn from(selector: T) -> Self { 18 | if let Some(secret) = selector.as_ref().strip_prefix(TICKET_SELECTOR_PREFIX) { 19 | let secret = Secret::new(secret.into()); 20 | return AuthSelector::Ticket { secret }; 21 | } 22 | 23 | let separator = if selector.as_ref().contains('#') { 24 | '#' 25 | } else { 26 | ':' 27 | }; 28 | 29 | let mut parts = selector.as_ref().splitn(2, separator); 30 | let username = parts.next().unwrap_or("").to_string(); 31 | let target_name = parts.next().unwrap_or("").to_string(); 32 | AuthSelector::User { 33 | username, 34 | target_name, 35 | } 36 | } 37 | } 38 | 39 | impl Debug for AuthSelector { 40 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 41 | match self { 42 | AuthSelector::User { 43 | username, 44 | target_name, 45 | } => write!(f, "<{username} for {target_name}>"), 46 | AuthSelector::Ticket { .. } => write!(f, ""), 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /warpgate-common/src/consts.rs: -------------------------------------------------------------------------------- 1 | pub const TICKET_SELECTOR_PREFIX: &str = "ticket-"; 2 | -------------------------------------------------------------------------------- /warpgate-common/src/helpers/fs.rs: -------------------------------------------------------------------------------- 1 | use std::os::unix::prelude::PermissionsExt; 2 | use std::path::Path; 3 | 4 | fn maybe_apply_permissions>( 5 | path: P, 6 | permissions: std::fs::Permissions, 7 | ) -> std::io::Result<()> { 8 | let current = std::fs::metadata(&path)?.permissions(); 9 | if (current.mode() & 0o777) != permissions.mode() { 10 | std::fs::set_permissions(path, permissions)?; 11 | } 12 | Ok(()) 13 | } 14 | 15 | pub fn secure_directory>(path: P) -> std::io::Result<()> { 16 | maybe_apply_permissions(path.as_ref(), std::fs::Permissions::from_mode(0o700)) 17 | } 18 | 19 | pub fn secure_file>(path: P) -> std::io::Result<()> { 20 | maybe_apply_permissions(path.as_ref(), std::fs::Permissions::from_mode(0o600)) 21 | } 22 | -------------------------------------------------------------------------------- /warpgate-common/src/helpers/hash.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use argon2::password_hash::rand_core::OsRng; 3 | use argon2::password_hash::{Error, PasswordHash, PasswordHasher, PasswordVerifier, SaltString}; 4 | use argon2::Argon2; 5 | use data_encoding::HEXLOWER; 6 | use rand::Rng; 7 | 8 | use crate::Secret; 9 | 10 | pub fn hash_password(password: &str) -> String { 11 | let salt = SaltString::generate(&mut OsRng); 12 | let argon2 = Argon2::default(); 13 | // Only panics for invalid hash parameters 14 | #[allow(clippy::unwrap_used)] 15 | argon2 16 | .hash_password(password.as_bytes(), &salt) 17 | .unwrap() 18 | .to_string() 19 | } 20 | 21 | pub fn parse_hash(hash: &str) -> Result, Error> { 22 | PasswordHash::new(hash) 23 | } 24 | 25 | pub fn verify_password_hash(password: &str, hash: &str) -> Result { 26 | let parsed_hash = parse_hash(hash).map_err(|e| anyhow::anyhow!(e))?; 27 | match Argon2::default().verify_password(password.as_bytes(), &parsed_hash) { 28 | Ok(()) => Ok(true), 29 | Err(Error::Password) => Ok(false), 30 | Err(e) => Err(anyhow::anyhow!(e)), 31 | } 32 | } 33 | 34 | pub fn generate_ticket_secret() -> Secret { 35 | let mut bytes = [0; 32]; 36 | rand::thread_rng().fill(&mut bytes[..]); 37 | Secret::new(HEXLOWER.encode(&bytes)) 38 | } 39 | -------------------------------------------------------------------------------- /warpgate-common/src/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod fs; 2 | pub mod hash; 3 | pub mod otp; 4 | pub mod rng; 5 | pub mod serde_base64; 6 | pub mod serde_base64_secret; 7 | -------------------------------------------------------------------------------- /warpgate-common/src/helpers/otp.rs: -------------------------------------------------------------------------------- 1 | use std::time::SystemTime; 2 | 3 | use rand::Rng; 4 | use totp_rs::{Algorithm, TOTP}; 5 | 6 | use super::rng::get_crypto_rng; 7 | use crate::types::Secret; 8 | 9 | pub type OtpExposedSecretKey = Vec; 10 | pub type OtpSecretKey = Secret; 11 | 12 | pub fn generate_key() -> OtpSecretKey { 13 | Secret::new(get_crypto_rng().gen::<[u8; 32]>().into()) 14 | } 15 | 16 | pub fn generate_setup_url(key: &OtpSecretKey, label: &str) -> Secret { 17 | let totp = get_totp(key, Some(label)); 18 | Secret::new(totp.get_url()) 19 | } 20 | 21 | fn get_totp(key: &OtpSecretKey, label: Option<&str>) -> TOTP { 22 | TOTP { 23 | algorithm: Algorithm::SHA1, 24 | digits: 6, 25 | skew: 1, 26 | step: 30, 27 | secret: key.expose_secret().clone(), 28 | issuer: Some("Warpgate".to_string()), 29 | account_name: label.unwrap_or("").to_string(), 30 | } 31 | } 32 | 33 | pub fn verify_totp(code: &str, key: &OtpSecretKey) -> bool { 34 | #[allow(clippy::unwrap_used)] 35 | let time = SystemTime::now() 36 | .duration_since(SystemTime::UNIX_EPOCH) 37 | .unwrap() 38 | .as_secs(); 39 | get_totp(key, None).check(code, time) 40 | } 41 | -------------------------------------------------------------------------------- /warpgate-common/src/helpers/rng.rs: -------------------------------------------------------------------------------- 1 | use rand::SeedableRng; 2 | use rand_chacha::ChaCha20Rng; 3 | 4 | pub fn get_crypto_rng() -> ChaCha20Rng { 5 | ChaCha20Rng::from_entropy() 6 | } 7 | -------------------------------------------------------------------------------- /warpgate-common/src/helpers/serde_base64.rs: -------------------------------------------------------------------------------- 1 | use data_encoding::BASE64; 2 | use serde::{Deserialize, Serializer}; 3 | 4 | pub fn serialize>( 5 | bytes: B, 6 | serializer: S, 7 | ) -> Result { 8 | serializer.serialize_str(&BASE64.encode(bytes.as_ref())) 9 | } 10 | 11 | pub fn deserialize<'de, D: serde::Deserializer<'de>, B: From>>( 12 | deserializer: D, 13 | ) -> Result { 14 | let s = String::deserialize(deserializer)?; 15 | Ok(BASE64 16 | .decode(s.as_bytes()) 17 | .map_err(serde::de::Error::custom)? 18 | .into()) 19 | } 20 | -------------------------------------------------------------------------------- /warpgate-common/src/helpers/serde_base64_secret.rs: -------------------------------------------------------------------------------- 1 | use serde::Serializer; 2 | 3 | use super::serde_base64; 4 | use crate::Secret; 5 | 6 | pub fn serialize( 7 | secret: &Secret>, 8 | serializer: S, 9 | ) -> Result { 10 | serde_base64::serialize(secret.expose_secret(), serializer) 11 | } 12 | 13 | pub fn deserialize<'de, D: serde::Deserializer<'de>>( 14 | deserializer: D, 15 | ) -> Result>, D::Error> { 16 | let inner = serde_base64::deserialize(deserializer)?; 17 | Ok(Secret::new(inner)) 18 | } 19 | -------------------------------------------------------------------------------- /warpgate-common/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | mod config; 3 | pub mod consts; 4 | mod error; 5 | pub mod eventhub; 6 | pub mod helpers; 7 | mod tls; 8 | mod try_macro; 9 | mod types; 10 | pub mod version; 11 | 12 | pub use config::*; 13 | pub use error::WarpgateError; 14 | pub use tls::*; 15 | pub use types::*; 16 | -------------------------------------------------------------------------------- /warpgate-common/src/tls/error.rs: -------------------------------------------------------------------------------- 1 | use rustls::server::VerifierBuilderError; 2 | 3 | #[derive(thiserror::Error, Debug)] 4 | pub enum RustlsSetupError { 5 | #[error("rustls: {0}")] 6 | Rustls(#[from] rustls::Error), 7 | #[error("verifier setup: {0}")] 8 | VerifierBuilder(#[from] VerifierBuilderError), 9 | #[error("no certificates found in certificate file")] 10 | NoCertificates, 11 | #[error("no private keys found in key file")] 12 | NoKeys, 13 | #[error("I/O: {0}")] 14 | Io(#[from] std::io::Error), 15 | #[error("PKI: {0}")] 16 | Pki(webpki::Error), 17 | } 18 | -------------------------------------------------------------------------------- /warpgate-common/src/tls/mod.rs: -------------------------------------------------------------------------------- 1 | mod cert; 2 | mod error; 3 | mod maybe_tls_stream; 4 | mod rustls_helpers; 5 | mod rustls_root_certs; 6 | 7 | pub use cert::*; 8 | pub use error::*; 9 | pub use maybe_tls_stream::{MaybeTlsStream, MaybeTlsStreamError, UpgradableStream}; 10 | pub use rustls_helpers::{configure_tls_connector, ResolveServerCert}; 11 | pub use rustls_root_certs::ROOT_CERT_STORE; 12 | -------------------------------------------------------------------------------- /warpgate-common/src/tls/rustls_root_certs.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use rustls::RootCertStore; 3 | 4 | #[allow(clippy::expect_used)] 5 | pub static ROOT_CERT_STORE: Lazy = Lazy::new(|| { 6 | let mut roots = RootCertStore::empty(); 7 | for cert in 8 | rustls_native_certs::load_native_certs().expect("could not load root TLS certificates") 9 | { 10 | roots.add(cert).expect("could not add root TLS certificate"); 11 | } 12 | roots 13 | }); 14 | -------------------------------------------------------------------------------- /warpgate-common/src/try_macro.rs: -------------------------------------------------------------------------------- 1 | #[macro_export] 2 | macro_rules! try_block { 3 | ($try:block catch ($err:ident : $errtype:ty) $catch:block) => {{ 4 | #[allow(unreachable_code)] 5 | let result: anyhow::Result<_, $errtype> = (|| Ok::<_, $errtype>($try))(); 6 | match result { 7 | Ok(_) => (), 8 | Err($err) => { 9 | { 10 | $catch 11 | }; 12 | } 13 | }; 14 | }}; 15 | (async $try:block catch ($err:ident : $errtype:ty) $catch:block) => {{ 16 | let result: anyhow::Result<_, $errtype> = (async { Ok::<_, $errtype>($try) }).await; 17 | match result { 18 | Ok(_) => (), 19 | Err($err) => { 20 | { 21 | $catch 22 | }; 23 | } 24 | }; 25 | }}; 26 | } 27 | 28 | #[test] 29 | #[allow(clippy::assertions_on_constants)] 30 | #[allow(clippy::unwrap_used)] 31 | fn test_catch() { 32 | let mut caught = false; 33 | try_block!({ 34 | let _: u32 = "asdf".parse()?; 35 | assert!(false) 36 | } catch (e: anyhow::Error) { 37 | assert_eq!(e.to_string(), "asdf".parse::().unwrap_err().to_string()); 38 | caught = true; 39 | }); 40 | assert!(caught); 41 | } 42 | 43 | #[test] 44 | #[allow(clippy::assertions_on_constants)] 45 | fn test_success() { 46 | try_block!({ 47 | let _: u32 = "123".parse()?; 48 | } catch (_e: anyhow::Error) { 49 | assert!(false) 50 | }); 51 | } 52 | -------------------------------------------------------------------------------- /warpgate-common/src/types/aliases.rs: -------------------------------------------------------------------------------- 1 | use uuid::Uuid; 2 | 3 | pub type SessionId = Uuid; 4 | pub type ProtocolName = &'static str; 5 | -------------------------------------------------------------------------------- /warpgate-common/src/types/mod.rs: -------------------------------------------------------------------------------- 1 | mod aliases; 2 | mod listen_endpoint; 3 | mod secret; 4 | 5 | pub use aliases::*; 6 | pub use listen_endpoint::*; 7 | pub use secret::*; 8 | -------------------------------------------------------------------------------- /warpgate-common/src/version.rs: -------------------------------------------------------------------------------- 1 | use git_version::git_version; 2 | 3 | pub fn warpgate_version() -> &'static str { 4 | git_version!( 5 | args = ["--tags", "--always", "--dirty=-modified"], 6 | fallback = "unknown" 7 | ) 8 | } 9 | -------------------------------------------------------------------------------- /warpgate-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-core" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | warpgate-common = { version = "*", path = "../warpgate-common" } 9 | warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" } 10 | warpgate-db-migrations = { version = "*", path = "../warpgate-db-migrations" } 11 | 12 | anyhow = { version = "1.0", features = ["std"] } 13 | argon2 = "0.5" 14 | async-trait = "0.1" 15 | bytes.workspace = true 16 | chrono = { version = "0.4", default-features = false, features = ["serde"] } 17 | data-encoding.workspace = true 18 | enum_dispatch.workspace = true 19 | humantime-serde = "1.1" 20 | futures.workspace = true 21 | once_cell = "1.17" 22 | packet = "0.1" 23 | password-hash.workspace = true 24 | poem.workspace = true 25 | poem-openapi = { version = "5.1", features = [ 26 | "swagger-ui", 27 | "chrono", 28 | "uuid", 29 | "static-files", 30 | ] } 31 | rand = "0.8" 32 | rand_chacha = "0.3" 33 | rand_core = { version = "0.6", features = ["std"] } 34 | sea-orm.workspace = true 35 | serde.workspace = true 36 | serde_json.workspace = true 37 | thiserror = "1.0" 38 | tokio = { version = "1.20", features = ["tracing"] } 39 | totp-rs = { version = "5.0", features = ["otpauth"] } 40 | tracing.workspace = true 41 | tracing-core = "0.1" 42 | tracing-subscriber = "0.3" 43 | url = "2.2" 44 | uuid = { version = "1.3", features = ["v4", "serde"] } 45 | warpgate-sso = { version = "*", path = "../warpgate-sso" } 46 | rustls.workspace = true 47 | rustls-pemfile = "1.0" 48 | webpki = "0.22" 49 | 50 | [features] 51 | postgres = ["sea-orm/sqlx-postgres"] 52 | mysql = ["sea-orm/sqlx-mysql"] 53 | sqlite = ["sea-orm/sqlx-sqlite"] 54 | -------------------------------------------------------------------------------- /warpgate-core/src/consts.rs: -------------------------------------------------------------------------------- 1 | pub static BUILTIN_ADMIN_TARGET_NAME: &str = "warpgate:admin"; 2 | pub static BUILTIN_ADMIN_ROLE_NAME: &str = "warpgate:admin"; 3 | pub static BUILTIN_ADMIN_USERNAME: &str = "admin"; 4 | -------------------------------------------------------------------------------- /warpgate-core/src/data.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use poem_openapi::Object; 3 | use serde::{Deserialize, Serialize}; 4 | use uuid::Uuid; 5 | use warpgate_common::{SessionId, Target}; 6 | use warpgate_db_entities::Session; 7 | 8 | #[derive(Serialize, Deserialize, Object)] 9 | pub struct SessionSnapshot { 10 | pub id: SessionId, 11 | pub username: Option, 12 | pub target: Option, 13 | pub started: DateTime, 14 | pub ended: Option>, 15 | pub ticket_id: Option, 16 | pub protocol: String, 17 | } 18 | 19 | impl From for SessionSnapshot { 20 | fn from(model: Session::Model) -> Self { 21 | Self { 22 | id: model.id, 23 | username: model.username, 24 | target: model 25 | .target_snapshot 26 | .and_then(|s| serde_json::from_str(&s).ok()), 27 | started: model.started, 28 | ended: model.ended, 29 | ticket_id: model.ticket_id, 30 | protocol: model.protocol, 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /warpgate-core/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod consts; 2 | mod data; 3 | mod state; 4 | pub use data::*; 5 | pub use state::{SessionState, SessionStateInit, State}; 6 | mod config_providers; 7 | pub use config_providers::*; 8 | pub mod db; 9 | mod protocols; 10 | pub use protocols::*; 11 | pub mod recordings; 12 | mod services; 13 | pub use services::*; 14 | mod auth_state_store; 15 | pub use auth_state_store::*; 16 | pub mod logging; 17 | -------------------------------------------------------------------------------- /warpgate-core/src/logging/mod.rs: -------------------------------------------------------------------------------- 1 | mod layer; 2 | mod socket; 3 | mod values; 4 | 5 | pub use socket::make_socket_logger_layer; 6 | mod database; 7 | pub use database::{install_database_logger, make_database_logger_layer}; 8 | -------------------------------------------------------------------------------- /warpgate-core/src/logging/socket.rs: -------------------------------------------------------------------------------- 1 | use bytes::BytesMut; 2 | use chrono::Local; 3 | use tokio::net::UnixDatagram; 4 | use tracing::*; 5 | use tracing_subscriber::registry::LookupSpan; 6 | use tracing_subscriber::Layer; 7 | use warpgate_common::WarpgateConfig; 8 | 9 | use super::layer::ValuesLogLayer; 10 | 11 | static SKIP_KEY: &str = "is_socket_logging_error"; 12 | 13 | pub async fn make_socket_logger_layer(config: &WarpgateConfig) -> impl Layer 14 | where 15 | S: Subscriber + for<'a> LookupSpan<'a>, 16 | { 17 | let mut socket = None; 18 | let socket_address = config.store.log.send_to.clone(); 19 | if socket_address.is_some() { 20 | socket = UnixDatagram::unbound() 21 | .map_err(|error| { 22 | println!("Failed to create the log forwarding UDP socket: {error}"); 23 | }) 24 | .ok(); 25 | } 26 | 27 | let (tx, mut rx) = tokio::sync::mpsc::channel(1024); 28 | 29 | let got_socket = socket.is_some(); 30 | 31 | let layer = ValuesLogLayer::new(move |mut values| { 32 | if !got_socket || values.contains_key(&SKIP_KEY) { 33 | return; 34 | } 35 | values.insert("timestamp", Local::now().to_rfc3339()); 36 | let _ = tx.try_send(values); 37 | }); 38 | 39 | if !got_socket { 40 | return layer; 41 | } 42 | 43 | tokio::spawn(async move { 44 | while let Some(values) = rx.recv().await { 45 | let Some(ref socket) = socket else { return }; 46 | let Some(ref socket_address) = socket_address else { 47 | return; 48 | }; 49 | 50 | let Ok(serialized) = serde_json::to_vec(&values) else { 51 | eprintln!("Failed to serialize log entry {values:?}"); 52 | continue; 53 | }; 54 | 55 | let buffer = BytesMut::from(&serialized[..]); 56 | if let Err(error) = socket.send_to(buffer.as_ref(), socket_address).await { 57 | error!(%error, is_socket_logging_error=true, "Failed to forward log entry"); 58 | } 59 | } 60 | }); 61 | 62 | layer 63 | } 64 | -------------------------------------------------------------------------------- /warpgate-core/src/logging/values.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fmt::Debug; 3 | use std::ops::DerefMut; 4 | 5 | use serde::Serialize; 6 | use tracing::field::Visit; 7 | use tracing_core::Field; 8 | 9 | pub type SerializedRecordValuesInner = HashMap<&'static str, String>; 10 | 11 | #[derive(Serialize, Debug)] 12 | pub struct SerializedRecordValues(SerializedRecordValuesInner); 13 | 14 | impl SerializedRecordValues { 15 | pub fn new() -> Self { 16 | Self(HashMap::new()) 17 | } 18 | 19 | pub fn into_values(self) -> SerializedRecordValuesInner { 20 | self.0 21 | } 22 | } 23 | 24 | impl std::ops::Deref for SerializedRecordValues { 25 | type Target = SerializedRecordValuesInner; 26 | 27 | fn deref(&self) -> &Self::Target { 28 | &self.0 29 | } 30 | } 31 | 32 | impl DerefMut for SerializedRecordValues { 33 | fn deref_mut(&mut self) -> &mut Self::Target { 34 | &mut self.0 35 | } 36 | } 37 | 38 | pub struct RecordVisitor<'a> { 39 | values: &'a mut SerializedRecordValues, 40 | } 41 | 42 | impl<'a> RecordVisitor<'a> { 43 | pub fn new(values: &'a mut SerializedRecordValues) -> Self { 44 | Self { values } 45 | } 46 | } 47 | 48 | impl Visit for RecordVisitor<'_> { 49 | fn record_str(&mut self, field: &Field, value: &str) { 50 | self.values.insert(field.name(), value.to_string()); 51 | } 52 | 53 | fn record_debug(&mut self, field: &Field, value: &dyn Debug) { 54 | self.values.insert(field.name(), format!("{value:?}")); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /warpgate-core/src/protocols/mod.rs: -------------------------------------------------------------------------------- 1 | mod handle; 2 | 3 | use std::future::Future; 4 | 5 | use anyhow::Result; 6 | pub use handle::{SessionHandle, WarpgateServerHandle}; 7 | use warpgate_common::{ListenEndpoint, Target}; 8 | 9 | #[derive(Debug, thiserror::Error)] 10 | pub enum TargetTestError { 11 | #[error("unreachable")] 12 | Unreachable, 13 | #[error("authentication failed")] 14 | AuthenticationError, 15 | #[error("connection error: {0}")] 16 | ConnectionError(String), 17 | #[error("misconfigured: {0}")] 18 | Misconfigured(String), 19 | #[error("I/O: {0}")] 20 | Io(#[from] std::io::Error), 21 | } 22 | 23 | pub trait ProtocolServer { 24 | fn run(self, address: ListenEndpoint) -> impl Future> + Send; 25 | fn test_target( 26 | &self, 27 | target: Target, 28 | ) -> impl Future> + Send; 29 | } 30 | -------------------------------------------------------------------------------- /warpgate-database-protocols/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "warpgate-database-protocols" 3 | version = "0.14.0" 4 | description = "Core of SQLx, the rust SQL toolkit. Just the database protocol parts." 5 | license = "MIT OR Apache-2.0" 6 | edition = "2021" 7 | authors = [ 8 | "Ryan Leckey ", 9 | "Austin Bonander ", 10 | "Chloe Ross ", 11 | "Daniel Akhterov ", 12 | ] 13 | 14 | [dependencies] 15 | tokio = { version = "1.20", features = ["io-util"] } 16 | bitflags = { version = "1.3", default-features = false } 17 | bytes.workspace = true 18 | futures-core = { version = "0.3", default-features = false } 19 | futures-util = { version = "0.3", default-features = false, features = [ 20 | "alloc", 21 | "sink", 22 | ] } 23 | memchr = { version = "2.5", default-features = false } 24 | thiserror = "1.0" 25 | -------------------------------------------------------------------------------- /warpgate-database-protocols/README.md: -------------------------------------------------------------------------------- 1 | This is an extract from sqlx-core with Encode/Decode impls added for server-side packet flow 2 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/io/buf.rs: -------------------------------------------------------------------------------- 1 | use std::str::from_utf8; 2 | 3 | use bytes::{Buf, Bytes}; 4 | use memchr::memchr; 5 | 6 | use crate::err_protocol; 7 | use crate::error::Error; 8 | 9 | pub trait BufExt: Buf { 10 | // Read a nul-terminated byte sequence 11 | fn get_bytes_nul(&mut self) -> Result; 12 | 13 | // Read a byte sequence of the exact length 14 | fn get_bytes(&mut self, len: usize) -> Bytes; 15 | 16 | // Read a nul-terminated string 17 | fn get_str_nul(&mut self) -> Result; 18 | 19 | // Read a string of the exact length 20 | fn get_str(&mut self, len: usize) -> Result; 21 | } 22 | 23 | impl BufExt for Bytes { 24 | fn get_bytes_nul(&mut self) -> Result { 25 | let nul = 26 | memchr(b'\0', self).ok_or_else(|| err_protocol!("expected NUL in byte sequence"))?; 27 | 28 | let v = self.slice(0..nul); 29 | 30 | self.advance(nul + 1); 31 | 32 | Ok(v) 33 | } 34 | 35 | fn get_bytes(&mut self, len: usize) -> Bytes { 36 | let v = self.slice(..len); 37 | self.advance(len); 38 | 39 | v 40 | } 41 | 42 | fn get_str_nul(&mut self) -> Result { 43 | self.get_bytes_nul().and_then(|bytes| { 44 | from_utf8(&bytes) 45 | .map(ToOwned::to_owned) 46 | .map_err(|err| err_protocol!("{}", err)) 47 | }) 48 | } 49 | 50 | fn get_str(&mut self, len: usize) -> Result { 51 | let v = from_utf8(&self[..len]) 52 | .map_err(|err| err_protocol!("{}", err)) 53 | .map(ToOwned::to_owned)?; 54 | 55 | self.advance(len); 56 | 57 | Ok(v) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/io/buf_mut.rs: -------------------------------------------------------------------------------- 1 | use bytes::BufMut; 2 | 3 | pub trait BufMutExt: BufMut { 4 | fn put_str_nul(&mut self, s: &str); 5 | } 6 | 7 | impl BufMutExt for Vec { 8 | fn put_str_nul(&mut self, s: &str) { 9 | self.extend(s.as_bytes()); 10 | self.push(0); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/io/decode.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | 3 | use crate::error::Error; 4 | 5 | pub trait Decode<'de, Context = ()> 6 | where 7 | Self: Sized, 8 | { 9 | fn decode(buf: Bytes) -> Result 10 | where 11 | Self: Decode<'de, ()>, 12 | { 13 | Self::decode_with(buf, ()) 14 | } 15 | 16 | fn decode_with(buf: Bytes, context: Context) -> Result; 17 | } 18 | 19 | impl Decode<'_> for Bytes { 20 | fn decode_with(buf: Bytes, _: ()) -> Result { 21 | Ok(buf) 22 | } 23 | } 24 | 25 | impl Decode<'_> for () { 26 | fn decode_with(_: Bytes, _: ()) -> Result<(), Error> { 27 | Ok(()) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/io/encode.rs: -------------------------------------------------------------------------------- 1 | pub trait Encode<'en, Context = ()> { 2 | fn encode(&self, buf: &mut Vec) 3 | where 4 | Self: Encode<'en, ()>, 5 | { 6 | self.encode_with(buf, ()); 7 | } 8 | 9 | fn encode_with(&self, buf: &mut Vec, context: Context); 10 | } 11 | 12 | impl Encode<'_, C> for &'_ [u8] { 13 | fn encode_with(&self, buf: &mut Vec, _: C) { 14 | buf.extend_from_slice(self); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/io/mod.rs: -------------------------------------------------------------------------------- 1 | mod buf; 2 | mod buf_mut; 3 | mod buf_stream; 4 | mod decode; 5 | mod encode; 6 | mod write_and_flush; 7 | 8 | pub use buf::BufExt; 9 | pub use buf_mut::BufMutExt; 10 | pub use buf_stream::BufStream; 11 | pub use decode::Decode; 12 | pub use encode::Encode; 13 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/io/write_and_flush.rs: -------------------------------------------------------------------------------- 1 | use std::io::{BufRead, Cursor}; 2 | use std::pin::Pin; 3 | use std::task::{Context, Poll}; 4 | 5 | use futures_core::Future; 6 | use futures_util::ready; 7 | use tokio::io::AsyncWrite; 8 | 9 | use crate::error::Error; 10 | 11 | // Atomic operation that writes the full buffer to the stream, flushes the stream, and then 12 | // clears the buffer (even if either of the two previous operations failed). 13 | pub struct WriteAndFlush<'a, S> { 14 | pub(super) stream: &'a mut S, 15 | pub(super) buf: Cursor<&'a mut Vec>, 16 | } 17 | 18 | impl Future for WriteAndFlush<'_, S> { 19 | type Output = Result<(), Error>; 20 | 21 | fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { 22 | let Self { 23 | ref mut stream, 24 | ref mut buf, 25 | } = *self; 26 | 27 | loop { 28 | let read = buf.fill_buf()?; 29 | 30 | if !read.is_empty() { 31 | let written = ready!(Pin::new(&mut *stream).poll_write(cx, read)?); 32 | buf.consume(written); 33 | } else { 34 | break; 35 | } 36 | } 37 | 38 | Pin::new(stream).poll_flush(cx).map_err(Error::Io) 39 | } 40 | } 41 | 42 | impl Drop for WriteAndFlush<'_, S> { 43 | fn drop(&mut self) { 44 | // clear the buffer regardless of whether the flush succeeded or not 45 | self.buf.get_mut().clear(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(dead_code, clippy::indexing_slicing)] 2 | pub mod io; 3 | pub mod mysql; 4 | 5 | #[macro_use] 6 | pub mod error; 7 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/io/buf.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, Bytes}; 2 | 3 | use crate::error::Error; 4 | use crate::io::BufExt; 5 | 6 | pub trait MySqlBufExt: Buf { 7 | // Read a length-encoded integer. 8 | // NOTE: 0xfb or NULL is only returned for binary value encoding to indicate NULL. 9 | // NOTE: 0xff is only returned during a result set to indicate ERR. 10 | // 11 | fn get_uint_lenenc(&mut self) -> u64; 12 | 13 | // Read a length-encoded string. 14 | fn get_str_lenenc(&mut self) -> Result; 15 | 16 | // Read a length-encoded byte sequence. 17 | fn get_bytes_lenenc(&mut self) -> Bytes; 18 | } 19 | 20 | impl MySqlBufExt for Bytes { 21 | fn get_uint_lenenc(&mut self) -> u64 { 22 | match self.get_u8() { 23 | 0xfc => u64::from(self.get_u16_le()), 24 | 0xfd => self.get_uint_le(3), 25 | 0xfe => self.get_u64_le(), 26 | 27 | v => u64::from(v), 28 | } 29 | } 30 | 31 | fn get_str_lenenc(&mut self) -> Result { 32 | let size = self.get_uint_lenenc(); 33 | self.get_str(size as usize) 34 | } 35 | 36 | fn get_bytes_lenenc(&mut self) -> Bytes { 37 | let size = self.get_uint_lenenc(); 38 | self.split_to(size as usize) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/io/mod.rs: -------------------------------------------------------------------------------- 1 | mod buf; 2 | mod buf_mut; 3 | 4 | pub use buf::MySqlBufExt; 5 | pub use buf_mut::MySqlBufMutExt; 6 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/mod.rs: -------------------------------------------------------------------------------- 1 | //! **MySQL** database driver. 2 | 3 | pub mod collation; 4 | pub mod io; 5 | pub mod protocol; 6 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/auth.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | 3 | use crate::err_protocol; 4 | use crate::error::Error; 5 | 6 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] 7 | #[allow(clippy::enum_variant_names)] 8 | pub enum AuthPlugin { 9 | MySqlClearPassword, 10 | MySqlNativePassword, 11 | CachingSha2Password, 12 | Sha256Password, 13 | } 14 | 15 | impl AuthPlugin { 16 | pub(crate) fn name(self) -> &'static str { 17 | match self { 18 | AuthPlugin::MySqlClearPassword => "mysql_clear_password", 19 | AuthPlugin::MySqlNativePassword => "mysql_native_password", 20 | AuthPlugin::CachingSha2Password => "caching_sha2_password", 21 | AuthPlugin::Sha256Password => "sha256_password", 22 | } 23 | } 24 | } 25 | 26 | impl FromStr for AuthPlugin { 27 | type Err = Error; 28 | 29 | fn from_str(s: &str) -> Result { 30 | match s { 31 | "mysql_clear_password" => Ok(AuthPlugin::MySqlClearPassword), 32 | "mysql_native_password" => Ok(AuthPlugin::MySqlNativePassword), 33 | "caching_sha2_password" => Ok(AuthPlugin::CachingSha2Password), 34 | "sha256_password" => Ok(AuthPlugin::Sha256Password), 35 | 36 | _ => Err(err_protocol!("unknown authentication plugin: {}", s)), 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/connect/auth_switch.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, BufMut, Bytes}; 2 | 3 | use crate::err_protocol; 4 | use crate::error::Error; 5 | use crate::io::{BufExt, BufMutExt, Decode, Encode}; 6 | use crate::mysql::protocol::auth::AuthPlugin; 7 | use crate::mysql::protocol::Capabilities; 8 | 9 | // https://dev.mysql.com/doc/dev/mysql-server/8.0.12/page_protocol_connection_phase_packets_protocol_auth_switch_request.html 10 | 11 | #[derive(Debug)] 12 | pub struct AuthSwitchRequest { 13 | pub plugin: AuthPlugin, 14 | pub data: Bytes, 15 | } 16 | 17 | impl Decode<'_> for AuthSwitchRequest { 18 | fn decode_with(mut buf: Bytes, _: ()) -> Result { 19 | let header = buf.get_u8(); 20 | if header != 0xfe { 21 | return Err(err_protocol!( 22 | "expected 0xfe (AUTH_SWITCH) but found 0x{:x}", 23 | header 24 | )); 25 | } 26 | 27 | let plugin = buf.get_str_nul()?.parse()?; 28 | 29 | // See: https://github.com/mysql/mysql-server/blob/ea7d2e2d16ac03afdd9cb72a972a95981107bf51/sql/auth/sha2_password.cc#L942 30 | if buf.len() != 21 { 31 | return Err(err_protocol!( 32 | "expected 21 bytes but found {} bytes", 33 | buf.len() 34 | )); 35 | } 36 | let data = buf.get_bytes(20); 37 | buf.advance(1); // NUL-terminator 38 | 39 | Ok(Self { plugin, data }) 40 | } 41 | } 42 | 43 | impl Encode<'_, ()> for AuthSwitchRequest { 44 | fn encode_with(&self, buf: &mut Vec, _: ()) { 45 | buf.put_u8(0xfe); 46 | buf.put_str_nul(self.plugin.name()); 47 | buf.extend(&self.data); 48 | } 49 | } 50 | 51 | #[derive(Debug)] 52 | pub struct AuthSwitchResponse(pub Vec); 53 | 54 | impl Encode<'_, Capabilities> for AuthSwitchResponse { 55 | fn encode_with(&self, buf: &mut Vec, _: Capabilities) { 56 | buf.extend_from_slice(&self.0); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/connect/mod.rs: -------------------------------------------------------------------------------- 1 | //! Connection Phase 2 | //! 3 | //! 4 | 5 | mod auth_switch; 6 | mod handshake; 7 | mod handshake_response; 8 | mod ssl_request; 9 | 10 | pub use auth_switch::{AuthSwitchRequest, AuthSwitchResponse}; 11 | pub use handshake::Handshake; 12 | pub use handshake_response::HandshakeResponse; 13 | pub use ssl_request::SslRequest; 14 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/connect/ssl_request.rs: -------------------------------------------------------------------------------- 1 | use crate::io::Encode; 2 | use crate::mysql::protocol::Capabilities; 3 | 4 | // https://dev.mysql.com/doc/dev/mysql-server/8.0.12/page_protocol_connection_phase_packets_protocol_handshake_response.html 5 | // https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::SSLRequest 6 | 7 | #[derive(Debug)] 8 | pub struct SslRequest { 9 | pub max_packet_size: u32, 10 | pub collation: u8, 11 | } 12 | 13 | impl Encode<'_, Capabilities> for SslRequest { 14 | fn encode_with(&self, buf: &mut Vec, capabilities: Capabilities) { 15 | buf.extend((capabilities.bits() as u32).to_le_bytes()); 16 | buf.extend(self.max_packet_size.to_le_bytes()); 17 | buf.push(self.collation); 18 | 19 | // reserved: string<19> 20 | buf.extend([0_u8; 19]); 21 | 22 | if capabilities.contains(Capabilities::MYSQL) { 23 | // reserved: string<4> 24 | buf.extend([0_u8; 4]); 25 | } else { 26 | // extended client capabilities (MariaDB-specified): int<4> 27 | buf.extend(((capabilities.bits() >> 32) as u32).to_le_bytes()); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth; 2 | pub mod capabilities; 3 | pub mod connect; 4 | pub mod packet; 5 | pub mod response; 6 | pub mod row; 7 | pub mod text; 8 | 9 | pub use capabilities::Capabilities; 10 | pub use packet::Packet; 11 | pub use row::Row; 12 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/response/eof.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, Bytes}; 2 | 3 | use crate::err_protocol; 4 | use crate::error::Error; 5 | use crate::io::Decode; 6 | use crate::mysql::protocol::response::Status; 7 | use crate::mysql::protocol::Capabilities; 8 | 9 | /// Marks the end of a result set, returning status and warnings. 10 | /// 11 | /// # Note 12 | /// 13 | /// The EOF packet is deprecated as of MySQL 5.7.5. SQLx only uses this packet for MySQL 14 | /// prior MySQL versions. 15 | #[derive(Debug)] 16 | pub struct EofPacket { 17 | pub warnings: u16, 18 | pub status: Status, 19 | } 20 | 21 | impl Decode<'_, Capabilities> for EofPacket { 22 | fn decode_with(mut buf: Bytes, _: Capabilities) -> Result { 23 | let header = buf.get_u8(); 24 | if header != 0xfe { 25 | return Err(err_protocol!( 26 | "expected 0xfe (EOF_Packet) but found 0x{:x}", 27 | header 28 | )); 29 | } 30 | 31 | let warnings = buf.get_u16_le(); 32 | let status = Status::from_bits_truncate(buf.get_u16_le()); 33 | 34 | Ok(Self { status, warnings }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/response/mod.rs: -------------------------------------------------------------------------------- 1 | //! Generic Response Packets 2 | //! 3 | //! 4 | //! 5 | 6 | mod eof; 7 | mod err; 8 | mod ok; 9 | mod status; 10 | 11 | pub use eof::EofPacket; 12 | pub use err::ErrPacket; 13 | pub use ok::OkPacket; 14 | pub use status::Status; 15 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/response/ok.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, BufMut, Bytes}; 2 | 3 | use crate::err_protocol; 4 | use crate::error::Error; 5 | use crate::io::{Decode, Encode}; 6 | use crate::mysql::io::{MySqlBufExt, MySqlBufMutExt}; 7 | use crate::mysql::protocol::response::Status; 8 | 9 | /// Indicates successful completion of a previous command sent by the client. 10 | #[derive(Debug)] 11 | pub struct OkPacket { 12 | pub affected_rows: u64, 13 | pub last_insert_id: u64, 14 | pub status: Status, 15 | pub warnings: u16, 16 | } 17 | 18 | impl Decode<'_> for OkPacket { 19 | fn decode_with(mut buf: Bytes, _: ()) -> Result { 20 | let header = buf.get_u8(); 21 | if header != 0 && header != 0xfe { 22 | return Err(err_protocol!( 23 | "expected 0x00 or 0xfe (OK_Packet) but found 0x{:02x}", 24 | header 25 | )); 26 | } 27 | 28 | let affected_rows = buf.get_uint_lenenc(); 29 | let last_insert_id = buf.get_uint_lenenc(); 30 | let status = Status::from_bits_truncate(buf.get_u16_le()); 31 | let warnings = buf.get_u16_le(); 32 | 33 | Ok(Self { 34 | affected_rows, 35 | last_insert_id, 36 | status, 37 | warnings, 38 | }) 39 | } 40 | } 41 | 42 | impl Encode<'_, ()> for OkPacket { 43 | fn encode_with(&self, buf: &mut Vec, _: ()) { 44 | buf.put_u8(0); 45 | buf.put_uint_lenenc(self.affected_rows); 46 | buf.put_uint_lenenc(self.last_insert_id); 47 | buf.put_u16_le(self.status.bits()); 48 | buf.put_u16_le(self.warnings); 49 | } 50 | } 51 | 52 | #[test] 53 | #[allow(clippy::unwrap_used)] 54 | fn test_decode_ok_packet() { 55 | const DATA: &[u8] = b"\x00\x00\x00\x02@\x00\x00"; 56 | 57 | let p = OkPacket::decode(DATA.into()).unwrap(); 58 | 59 | assert_eq!(p.affected_rows, 0); 60 | assert_eq!(p.last_insert_id, 0); 61 | assert_eq!(p.warnings, 0); 62 | assert!(p.status.contains(Status::SERVER_STATUS_AUTOCOMMIT)); 63 | assert!(p.status.contains(Status::SERVER_SESSION_STATE_CHANGED)); 64 | } 65 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/row.rs: -------------------------------------------------------------------------------- 1 | use std::ops::Range; 2 | 3 | use bytes::Bytes; 4 | 5 | #[derive(Debug)] 6 | pub struct Row { 7 | pub(crate) storage: Bytes, 8 | pub(crate) values: Vec>>, 9 | } 10 | 11 | impl Row { 12 | pub(crate) fn get(&self, index: usize) -> Option<&[u8]> { 13 | self.values[index] 14 | .as_ref() 15 | .map(|col| &self.storage[col.start..col.end]) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/text/mod.rs: -------------------------------------------------------------------------------- 1 | mod column; 2 | mod ping; 3 | mod query; 4 | mod quit; 5 | 6 | pub use column::{ColumnDefinition, ColumnFlags, ColumnType}; 7 | pub use ping::Ping; 8 | pub use query::Query; 9 | pub use quit::Quit; 10 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/text/ping.rs: -------------------------------------------------------------------------------- 1 | use crate::io::Encode; 2 | use crate::mysql::protocol::Capabilities; 3 | 4 | // https://dev.mysql.com/doc/internals/en/com-ping.html 5 | 6 | #[derive(Debug)] 7 | pub struct Ping; 8 | 9 | impl Encode<'_, Capabilities> for Ping { 10 | fn encode_with(&self, buf: &mut Vec, _: Capabilities) { 11 | buf.push(0x0e); // COM_PING 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/text/query.rs: -------------------------------------------------------------------------------- 1 | use bytes::{Buf, Bytes}; 2 | 3 | use crate::error::Error; 4 | use crate::io::{BufExt, Decode, Encode}; 5 | use crate::mysql::protocol::Capabilities; 6 | 7 | // https://dev.mysql.com/doc/internals/en/com-query.html 8 | 9 | #[derive(Debug)] 10 | pub struct Query(pub String); 11 | 12 | impl Encode<'_, ()> for Query { 13 | fn encode_with(&self, buf: &mut Vec, _: ()) { 14 | buf.push(0x03); // COM_QUERY 15 | buf.extend(self.0.as_bytes()) 16 | } 17 | } 18 | 19 | impl Encode<'_, Capabilities> for Query { 20 | fn encode_with(&self, buf: &mut Vec, _: Capabilities) { 21 | buf.push(0x03); // COM_QUERY 22 | buf.extend(self.0.as_bytes()) 23 | } 24 | } 25 | 26 | impl Decode<'_> for Query { 27 | fn decode_with(mut buf: Bytes, _: ()) -> Result { 28 | buf.advance(1); 29 | let q = buf.get_str(buf.len())?; 30 | Ok(Query(q)) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /warpgate-database-protocols/src/mysql/protocol/text/quit.rs: -------------------------------------------------------------------------------- 1 | use crate::io::Encode; 2 | use crate::mysql::protocol::Capabilities; 3 | 4 | // https://dev.mysql.com/doc/internals/en/com-quit.html 5 | 6 | #[derive(Debug)] 7 | pub struct Quit; 8 | 9 | impl Encode<'_, Capabilities> for Quit { 10 | fn encode_with(&self, buf: &mut Vec, _: Capabilities) { 11 | buf.push(0x01); // COM_QUIT 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /warpgate-db-entities/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-db-entities" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | bytes = "1.4" 9 | chrono = { version = "0.4", default-features = false, features = ["serde"] } 10 | poem-openapi = { version = "5.1", features = ["chrono", "uuid"] } 11 | sqlx.workspace = true 12 | sea-orm = { workspace = true, features = [ 13 | "macros", 14 | "with-chrono", 15 | "with-uuid", 16 | "with-json", 17 | ], default-features = false } 18 | serde.workspace = true 19 | serde_json.workspace = true 20 | uuid = { version = "1.3", features = ["v4", "serde"] } 21 | warpgate-common = { version = "*", path = "../warpgate-common" } 22 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/ApiToken.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use sea_orm::entity::prelude::*; 3 | use sea_orm::sea_query::ForeignKeyAction; 4 | use serde::Serialize; 5 | use uuid::Uuid; 6 | 7 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)] 8 | #[sea_orm(table_name = "api_tokens")] 9 | pub struct Model { 10 | #[sea_orm(primary_key, auto_increment = false)] 11 | pub id: Uuid, 12 | pub user_id: Uuid, 13 | pub label: String, 14 | pub secret: String, 15 | pub created: DateTime, 16 | pub expiry: DateTime, 17 | } 18 | 19 | #[derive(Copy, Clone, Debug, EnumIter)] 20 | pub enum Relation { 21 | User, 22 | } 23 | 24 | impl RelationTrait for Relation { 25 | fn def(&self) -> RelationDef { 26 | match self { 27 | Self::User => Entity::belongs_to(super::User::Entity) 28 | .from(Column::UserId) 29 | .to(super::User::Column::Id) 30 | .on_delete(ForeignKeyAction::Cascade) 31 | .into(), 32 | } 33 | } 34 | } 35 | 36 | impl Related for Entity { 37 | fn to() -> RelationDef { 38 | Relation::User.def() 39 | } 40 | } 41 | 42 | impl ActiveModelBehavior for ActiveModel {} 43 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/KnownHost.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::Object; 2 | use sea_orm::entity::prelude::*; 3 | use serde::Serialize; 4 | use uuid::Uuid; 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Object, Serialize)] 7 | #[sea_orm(table_name = "known_hosts")] 8 | #[oai(rename = "SSHKnownHost")] 9 | pub struct Model { 10 | #[sea_orm(primary_key, auto_increment = false)] 11 | pub id: Uuid, 12 | pub host: String, 13 | pub port: i32, 14 | pub key_type: String, 15 | pub key_base64: String, 16 | } 17 | 18 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 19 | pub enum Relation {} 20 | 21 | impl ActiveModelBehavior for ActiveModel {} 22 | 23 | impl Model { 24 | pub fn key_openssh(&self) -> String { 25 | format!("{} {}", self.key_type, self.key_base64) 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/LogEntry.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use poem_openapi::Object; 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::query::JsonValue; 5 | use serde::Serialize; 6 | use uuid::Uuid; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] 9 | #[sea_orm(table_name = "log")] 10 | #[oai(rename = "LogEntry")] 11 | pub struct Model { 12 | #[sea_orm(primary_key, auto_increment = false)] 13 | pub id: Uuid, 14 | pub text: String, 15 | pub values: JsonValue, 16 | pub timestamp: DateTime, 17 | pub session_id: Uuid, 18 | pub username: Option, 19 | } 20 | 21 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 22 | pub enum Relation {} 23 | 24 | impl ActiveModelBehavior for ActiveModel {} 25 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/OtpCredential.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use sea_orm::sea_query::ForeignKeyAction; 3 | use sea_orm::Set; 4 | use serde::Serialize; 5 | use uuid::Uuid; 6 | use warpgate_common::{UserAuthCredential, UserTotpCredential}; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)] 9 | #[sea_orm(table_name = "credentials_otp")] 10 | pub struct Model { 11 | #[sea_orm(primary_key, auto_increment = false)] 12 | pub id: Uuid, 13 | pub user_id: Uuid, 14 | pub secret_key: Vec, 15 | } 16 | 17 | #[derive(Copy, Clone, Debug, EnumIter)] 18 | pub enum Relation { 19 | User, 20 | } 21 | 22 | impl RelationTrait for Relation { 23 | fn def(&self) -> RelationDef { 24 | match self { 25 | Self::User => Entity::belongs_to(super::User::Entity) 26 | .from(Column::UserId) 27 | .to(super::User::Column::Id) 28 | .on_delete(ForeignKeyAction::Cascade) 29 | .into(), 30 | } 31 | } 32 | } 33 | 34 | impl Related for Entity { 35 | fn to() -> RelationDef { 36 | Relation::User.def() 37 | } 38 | } 39 | 40 | impl ActiveModelBehavior for ActiveModel {} 41 | 42 | impl From for UserTotpCredential { 43 | fn from(credential: Model) -> Self { 44 | UserTotpCredential { 45 | key: credential.secret_key.into(), 46 | } 47 | } 48 | } 49 | 50 | impl From for UserAuthCredential { 51 | fn from(model: Model) -> Self { 52 | Self::Totp(model.into()) 53 | } 54 | } 55 | 56 | impl From for ActiveModel { 57 | fn from(credential: UserTotpCredential) -> Self { 58 | Self { 59 | secret_key: Set(credential.key.expose_secret().clone()), 60 | ..Default::default() 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/Parameters.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use sea_orm::Set; 3 | use uuid::Uuid; 4 | 5 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 6 | #[sea_orm(table_name = "parameters")] 7 | pub struct Model { 8 | #[sea_orm(primary_key, auto_increment = false)] 9 | pub id: Uuid, 10 | pub allow_own_credential_management: bool, 11 | } 12 | 13 | impl ActiveModelBehavior for ActiveModel {} 14 | 15 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 16 | pub enum Relation {} 17 | 18 | impl Entity { 19 | pub async fn get(db: &DatabaseConnection) -> Result { 20 | match Self::find().one(db).await? { 21 | Some(model) => Ok(model), 22 | None => { 23 | ActiveModel { 24 | id: Set(Uuid::new_v4()), 25 | allow_own_credential_management: Set(true), 26 | } 27 | .insert(db) 28 | .await 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/PublicKeyCredential.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use sea_orm::entity::prelude::*; 3 | use sea_orm::sea_query::ForeignKeyAction; 4 | use sea_orm::Set; 5 | use serde::Serialize; 6 | use uuid::Uuid; 7 | use warpgate_common::{UserAuthCredential, UserPublicKeyCredential}; 8 | 9 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)] 10 | #[sea_orm(table_name = "credentials_public_key")] 11 | pub struct Model { 12 | #[sea_orm(primary_key, auto_increment = false)] 13 | pub id: Uuid, 14 | pub user_id: Uuid, 15 | pub label: String, 16 | pub date_added: Option>, 17 | pub last_used: Option>, 18 | #[sea_orm(column_type = "Text")] 19 | pub openssh_public_key: String, 20 | } 21 | 22 | #[derive(Copy, Clone, Debug, EnumIter)] 23 | pub enum Relation { 24 | User, 25 | } 26 | 27 | impl RelationTrait for Relation { 28 | fn def(&self) -> RelationDef { 29 | match self { 30 | Self::User => Entity::belongs_to(super::User::Entity) 31 | .from(Column::UserId) 32 | .to(super::User::Column::Id) 33 | .on_delete(ForeignKeyAction::Cascade) 34 | .into(), 35 | } 36 | } 37 | } 38 | 39 | impl Related for Entity { 40 | fn to() -> RelationDef { 41 | Relation::User.def() 42 | } 43 | } 44 | 45 | impl ActiveModelBehavior for ActiveModel {} 46 | 47 | impl From for UserPublicKeyCredential { 48 | fn from(credential: Model) -> Self { 49 | UserPublicKeyCredential { 50 | key: credential.openssh_public_key.into(), 51 | } 52 | } 53 | } 54 | 55 | impl From for UserAuthCredential { 56 | fn from(model: Model) -> Self { 57 | Self::PublicKey(model.into()) 58 | } 59 | } 60 | 61 | impl From for ActiveModel { 62 | fn from(credential: UserPublicKeyCredential) -> Self { 63 | Self { 64 | openssh_public_key: Set(credential.key.expose_secret().clone()), 65 | ..Default::default() 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/Recording.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use poem_openapi::{Enum, Object}; 3 | use sea_orm::entity::prelude::*; 4 | use sea_orm::sea_query::ForeignKeyAction; 5 | use serde::Serialize; 6 | use uuid::Uuid; 7 | 8 | #[derive(Debug, Clone, PartialEq, Eq, EnumIter, Enum, DeriveActiveEnum, Serialize)] 9 | #[sea_orm(rs_type = "String", db_type = "String(StringLen::N(16))")] 10 | pub enum RecordingKind { 11 | #[sea_orm(string_value = "terminal")] 12 | Terminal, 13 | #[sea_orm(string_value = "traffic")] 14 | Traffic, 15 | } 16 | 17 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] 18 | #[sea_orm(table_name = "recordings")] 19 | #[oai(rename = "Recording")] 20 | pub struct Model { 21 | #[sea_orm(primary_key, auto_increment = false)] 22 | pub id: Uuid, 23 | pub name: String, 24 | pub started: DateTime, 25 | pub ended: Option>, 26 | pub session_id: Uuid, 27 | pub kind: RecordingKind, 28 | } 29 | 30 | #[derive(Copy, Clone, Debug, EnumIter)] 31 | pub enum Relation { 32 | Session, 33 | } 34 | 35 | impl RelationTrait for Relation { 36 | fn def(&self) -> RelationDef { 37 | match self { 38 | Self::Session => Entity::belongs_to(super::Session::Entity) 39 | .from(Column::SessionId) 40 | .to(super::Session::Column::Id) 41 | .on_delete(ForeignKeyAction::Cascade) 42 | .into(), 43 | } 44 | } 45 | } 46 | 47 | impl Related for Entity { 48 | fn to() -> RelationDef { 49 | Relation::Session.def() 50 | } 51 | } 52 | 53 | impl ActiveModelBehavior for ActiveModel {} 54 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/Role.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::Object; 2 | use sea_orm::entity::prelude::*; 3 | use serde::Serialize; 4 | use uuid::Uuid; 5 | use warpgate_common::Role; 6 | 7 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] 8 | #[sea_orm(table_name = "roles")] 9 | #[oai(rename = "Role")] 10 | pub struct Model { 11 | #[sea_orm(primary_key, auto_increment = false)] 12 | pub id: Uuid, 13 | pub name: String, 14 | #[sea_orm(column_type = "Text")] 15 | pub description: String, 16 | } 17 | 18 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 19 | pub enum Relation {} 20 | 21 | impl Related for Entity { 22 | fn to() -> RelationDef { 23 | super::TargetRoleAssignment::Relation::Target.def() 24 | } 25 | 26 | fn via() -> Option { 27 | Some(super::TargetRoleAssignment::Relation::Role.def().rev()) 28 | } 29 | } 30 | 31 | impl Related for Entity { 32 | fn to() -> RelationDef { 33 | super::UserRoleAssignment::Relation::User.def() 34 | } 35 | 36 | fn via() -> Option { 37 | Some(super::UserRoleAssignment::Relation::Role.def().rev()) 38 | } 39 | } 40 | 41 | impl ActiveModelBehavior for ActiveModel {} 42 | 43 | impl From for Role { 44 | fn from(model: Model) -> Self { 45 | Self { 46 | id: model.id, 47 | name: model.name, 48 | description: model.description, 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/Session.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use sea_orm::entity::prelude::*; 3 | use uuid::Uuid; 4 | 5 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 6 | #[sea_orm(table_name = "sessions")] 7 | pub struct Model { 8 | #[sea_orm(primary_key, auto_increment = false)] 9 | pub id: Uuid, 10 | pub target_snapshot: Option, 11 | pub username: Option, 12 | pub remote_address: String, 13 | pub started: DateTime, 14 | pub ended: Option>, 15 | pub ticket_id: Option, 16 | pub protocol: String, 17 | } 18 | 19 | #[derive(Copy, Clone, Debug, EnumIter)] 20 | pub enum Relation { 21 | Recordings, 22 | Ticket, 23 | } 24 | 25 | impl RelationTrait for Relation { 26 | fn def(&self) -> RelationDef { 27 | match self { 28 | Self::Recordings => Entity::has_many(super::Recording::Entity) 29 | .from(Column::Id) 30 | .to(super::Recording::Column::SessionId) 31 | .into(), 32 | Self::Ticket => Entity::belongs_to(super::Ticket::Entity) 33 | .from(Column::TicketId) 34 | .to(super::Ticket::Column::Id) 35 | .on_delete(ForeignKeyAction::SetNull) 36 | .into(), 37 | } 38 | } 39 | } 40 | 41 | impl Related for Entity { 42 | fn to() -> RelationDef { 43 | Relation::Ticket.def() 44 | } 45 | } 46 | 47 | impl ActiveModelBehavior for ActiveModel {} 48 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/SsoCredential.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::entity::prelude::*; 2 | use sea_orm::sea_query::ForeignKeyAction; 3 | use sea_orm::Set; 4 | use serde::Serialize; 5 | use uuid::Uuid; 6 | use warpgate_common::{UserAuthCredential, UserSsoCredential}; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize)] 9 | #[sea_orm(table_name = "credentials_sso")] 10 | pub struct Model { 11 | #[sea_orm(primary_key, auto_increment = false)] 12 | pub id: Uuid, 13 | pub user_id: Uuid, 14 | pub provider: Option, 15 | pub email: String, 16 | } 17 | 18 | #[derive(Copy, Clone, Debug, EnumIter)] 19 | pub enum Relation { 20 | User, 21 | } 22 | 23 | impl RelationTrait for Relation { 24 | fn def(&self) -> RelationDef { 25 | match self { 26 | Self::User => Entity::belongs_to(super::User::Entity) 27 | .from(Column::UserId) 28 | .to(super::User::Column::Id) 29 | .on_delete(ForeignKeyAction::Cascade) 30 | .into(), 31 | } 32 | } 33 | } 34 | 35 | impl Related for Entity { 36 | fn to() -> RelationDef { 37 | Relation::User.def() 38 | } 39 | } 40 | 41 | impl ActiveModelBehavior for ActiveModel {} 42 | 43 | impl From for UserSsoCredential { 44 | fn from(credential: Model) -> Self { 45 | UserSsoCredential { 46 | provider: credential.provider, 47 | email: credential.email, 48 | } 49 | } 50 | } 51 | 52 | impl From for UserAuthCredential { 53 | fn from(model: Model) -> Self { 54 | Self::Sso(model.into()) 55 | } 56 | } 57 | 58 | impl From for ActiveModel { 59 | fn from(credential: UserSsoCredential) -> Self { 60 | Self { 61 | provider: Set(credential.provider), 62 | email: Set(credential.email), 63 | ..Default::default() 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/TargetRoleAssignment.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::Object; 2 | use sea_orm::entity::prelude::*; 3 | use serde::Serialize; 4 | use uuid::Uuid; 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] 7 | #[sea_orm(table_name = "target_roles")] 8 | #[oai(rename = "TargetRoleAssignment")] 9 | pub struct Model { 10 | #[sea_orm(primary_key, auto_increment = true)] 11 | pub id: i32, 12 | pub target_id: Uuid, 13 | pub role_id: Uuid, 14 | } 15 | 16 | #[derive(Copy, Clone, Debug, EnumIter)] 17 | pub enum Relation { 18 | Target, 19 | Role, 20 | } 21 | 22 | impl RelationTrait for Relation { 23 | fn def(&self) -> RelationDef { 24 | match self { 25 | Self::Target => Entity::belongs_to(super::Target::Entity) 26 | .from(Column::TargetId) 27 | .to(super::Target::Column::Id) 28 | .into(), 29 | Self::Role => Entity::belongs_to(super::Role::Entity) 30 | .from(Column::RoleId) 31 | .to(super::Role::Column::Id) 32 | .into(), 33 | } 34 | } 35 | } 36 | 37 | impl ActiveModelBehavior for ActiveModel {} 38 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/Ticket.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, Utc}; 2 | use poem_openapi::Object; 3 | use sea_orm::entity::prelude::*; 4 | use serde::Serialize; 5 | use uuid::Uuid; 6 | 7 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] 8 | #[sea_orm(table_name = "tickets")] 9 | #[oai(rename = "Ticket")] 10 | pub struct Model { 11 | #[sea_orm(primary_key, auto_increment = false)] 12 | pub id: Uuid, 13 | #[oai(skip)] 14 | pub secret: String, 15 | pub username: String, 16 | #[sea_orm(column_type = "Text")] 17 | pub description: String, 18 | pub target: String, 19 | pub uses_left: Option, 20 | pub expiry: Option>, 21 | pub created: DateTime, 22 | } 23 | 24 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 25 | pub enum Relation { 26 | #[sea_orm(has_many = "super::Session::Entity")] 27 | Sessions, 28 | } 29 | 30 | impl ActiveModelBehavior for ActiveModel {} 31 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/UserRoleAssignment.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::Object; 2 | use sea_orm::entity::prelude::*; 3 | use serde::Serialize; 4 | use uuid::Uuid; 5 | 6 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Object)] 7 | #[sea_orm(table_name = "user_roles")] 8 | #[oai(rename = "UserRoleAssignment")] 9 | pub struct Model { 10 | #[sea_orm(primary_key, auto_increment = true)] 11 | pub id: i32, 12 | pub user_id: Uuid, 13 | pub role_id: Uuid, 14 | } 15 | 16 | #[derive(Copy, Clone, Debug, EnumIter)] 17 | pub enum Relation { 18 | User, 19 | Role, 20 | } 21 | 22 | impl RelationTrait for Relation { 23 | fn def(&self) -> RelationDef { 24 | match self { 25 | Self::User => Entity::belongs_to(super::User::Entity) 26 | .from(Column::UserId) 27 | .to(super::User::Column::Id) 28 | .into(), 29 | Self::Role => Entity::belongs_to(super::Role::Entity) 30 | .from(Column::RoleId) 31 | .to(super::Role::Column::Id) 32 | .into(), 33 | } 34 | } 35 | } 36 | 37 | impl ActiveModelBehavior for ActiveModel {} 38 | -------------------------------------------------------------------------------- /warpgate-db-entities/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![allow(non_snake_case)] 2 | 3 | pub mod ApiToken; 4 | pub mod KnownHost; 5 | pub mod LogEntry; 6 | pub mod OtpCredential; 7 | pub mod Parameters; 8 | pub mod PasswordCredential; 9 | pub mod PublicKeyCredential; 10 | pub mod Recording; 11 | pub mod Role; 12 | pub mod Session; 13 | pub mod SsoCredential; 14 | pub mod Target; 15 | pub mod TargetRoleAssignment; 16 | pub mod Ticket; 17 | pub mod User; 18 | pub mod UserRoleAssignment; 19 | -------------------------------------------------------------------------------- /warpgate-db-migrations/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-db-migrations" 5 | publish = false 6 | version = "0.14.0" 7 | 8 | [lib] 9 | 10 | [dependencies] 11 | tokio = { version = "1.20", features = ["macros", "rt-multi-thread"] } 12 | chrono = { version = "0.4", default-features = false, features = ["serde"] } 13 | data-encoding.workspace = true 14 | sea-orm = { workspace = true, features = [ 15 | "with-chrono", 16 | "with-uuid", 17 | "with-json", 18 | ], default-features = false } 19 | sea-orm-migration.workspace = true 20 | russh.workspace = true 21 | tracing.workspace = true 22 | uuid = { version = "1.3", features = ["v4", "serde"] } 23 | serde_json.workspace = true 24 | serde.workspace = true 25 | 26 | [features] 27 | postgres = ["sea-orm/sqlx-postgres"] 28 | mysql = ["sea-orm/sqlx-mysql"] 29 | sqlite = ["sea-orm/sqlx-sqlite"] 30 | -------------------------------------------------------------------------------- /warpgate-db-migrations/README.md: -------------------------------------------------------------------------------- 1 | # Running Migrator CLI 2 | 3 | - Apply all pending migrations 4 | ```sh 5 | cargo run 6 | ``` 7 | ```sh 8 | cargo run -- up 9 | ``` 10 | - Apply first 10 pending migrations 11 | ```sh 12 | cargo run -- up -n 10 13 | ``` 14 | - Rollback last applied migrations 15 | ```sh 16 | cargo run -- down 17 | ``` 18 | - Rollback last 10 applied migrations 19 | ```sh 20 | cargo run -- down -n 10 21 | ``` 22 | - Drop all tables from the database, then reapply all migrations 23 | ```sh 24 | cargo run -- fresh 25 | ``` 26 | - Rollback all applied migrations, then reapply all migrations 27 | ```sh 28 | cargo run -- refresh 29 | ``` 30 | - Rollback all applied migrations 31 | ```sh 32 | cargo run -- reset 33 | ``` 34 | - Check the status of all migrations 35 | ```sh 36 | cargo run -- status 37 | ``` 38 | -------------------------------------------------------------------------------- /warpgate-db-migrations/src/m00001_create_ticket.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::{DbBackend, Schema}; 2 | use sea_orm_migration::prelude::*; 3 | 4 | pub mod ticket { 5 | use sea_orm::entity::prelude::*; 6 | use uuid::Uuid; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 9 | #[sea_orm(table_name = "tickets")] 10 | pub struct Model { 11 | #[sea_orm(primary_key, auto_increment = false)] 12 | pub id: Uuid, 13 | pub secret: String, 14 | pub username: String, 15 | pub target: String, 16 | pub uses_left: Option, 17 | pub expiry: Option, 18 | pub created: DateTimeUtc, 19 | } 20 | 21 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 22 | pub enum Relation {} 23 | 24 | impl ActiveModelBehavior for ActiveModel {} 25 | } 26 | 27 | pub struct Migration; 28 | 29 | impl MigrationName for Migration { 30 | fn name(&self) -> &str { 31 | "m00001_create_ticket" 32 | } 33 | } 34 | 35 | #[async_trait::async_trait] 36 | impl MigrationTrait for Migration { 37 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 38 | let builder = manager.get_database_backend(); 39 | let schema = Schema::new(builder); 40 | 41 | manager 42 | .create_table(schema.create_table_from_entity(ticket::Entity)) 43 | .await?; 44 | 45 | let connection = manager.get_connection(); 46 | if connection.get_database_backend() == DbBackend::MySql { 47 | // https://github.com/warp-tech/warpgate/issues/857 48 | connection 49 | .execute_unprepared( 50 | "ALTER TABLE `tickets` MODIFY COLUMN `expiry` TIMESTAMP NULL DEFAULT NULL", 51 | ) 52 | .await?; 53 | } 54 | 55 | Ok(()) 56 | } 57 | 58 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 59 | manager 60 | .drop_table(Table::drop().table(ticket::Entity).to_owned()) 61 | .await 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /warpgate-db-migrations/src/m00004_create_known_host.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::Schema; 2 | use sea_orm_migration::prelude::*; 3 | 4 | pub mod known_host { 5 | use sea_orm::entity::prelude::*; 6 | use uuid::Uuid; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 9 | #[sea_orm(table_name = "known_hosts")] 10 | pub struct Model { 11 | #[sea_orm(primary_key, auto_increment = false)] 12 | pub id: Uuid, 13 | pub host: String, 14 | pub port: i32, 15 | pub key_type: String, 16 | pub key_base64: String, 17 | } 18 | 19 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 20 | pub enum Relation {} 21 | 22 | impl ActiveModelBehavior for ActiveModel {} 23 | } 24 | 25 | pub struct Migration; 26 | 27 | impl MigrationName for Migration { 28 | fn name(&self) -> &str { 29 | "m00004_create_known_host" 30 | } 31 | } 32 | 33 | #[async_trait::async_trait] 34 | impl MigrationTrait for Migration { 35 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 36 | let builder = manager.get_database_backend(); 37 | let schema = Schema::new(builder); 38 | manager 39 | .create_table(schema.create_table_from_entity(known_host::Entity)) 40 | .await 41 | } 42 | 43 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 44 | manager 45 | .drop_table(Table::drop().table(known_host::Entity).to_owned()) 46 | .await 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /warpgate-db-migrations/src/m00006_add_session_protocol.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | pub struct Migration; 4 | 5 | impl MigrationName for Migration { 6 | fn name(&self) -> &str { 7 | "m00006_add_session_protocol" 8 | } 9 | } 10 | 11 | use crate::m00002_create_session::session; 12 | 13 | #[async_trait::async_trait] 14 | impl MigrationTrait for Migration { 15 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 16 | manager 17 | .alter_table( 18 | Table::alter() 19 | .table(session::Entity) 20 | .add_column( 21 | ColumnDef::new(Alias::new("protocol")) 22 | .string() 23 | .not_null() 24 | .default("SSH"), 25 | ) 26 | .to_owned(), 27 | ) 28 | .await 29 | } 30 | 31 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 32 | manager 33 | .alter_table( 34 | Table::alter() 35 | .table(session::Entity) 36 | .drop_column(Alias::new("protocol")) 37 | .to_owned(), 38 | ) 39 | .await 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /warpgate-db-migrations/src/m00010_parameters.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::Schema; 2 | use sea_orm_migration::prelude::*; 3 | 4 | pub mod parameters { 5 | use sea_orm::entity::prelude::*; 6 | use uuid::Uuid; 7 | 8 | #[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel)] 9 | #[sea_orm(table_name = "parameters")] 10 | pub struct Model { 11 | #[sea_orm(primary_key, auto_increment = false)] 12 | pub id: Uuid, 13 | pub allow_own_credential_management: bool, 14 | } 15 | 16 | impl ActiveModelBehavior for ActiveModel {} 17 | 18 | #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] 19 | pub enum Relation {} 20 | } 21 | 22 | pub struct Migration; 23 | 24 | impl MigrationName for Migration { 25 | fn name(&self) -> &str { 26 | "m00010_parameters" 27 | } 28 | } 29 | 30 | #[async_trait::async_trait] 31 | impl MigrationTrait for Migration { 32 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 33 | let builder = manager.get_database_backend(); 34 | let schema = Schema::new(builder); 35 | manager 36 | .create_table(schema.create_table_from_entity(parameters::Entity)) 37 | .await?; 38 | Ok(()) 39 | } 40 | 41 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 42 | manager 43 | .drop_table(Table::drop().table(parameters::Entity).to_owned()) 44 | .await?; 45 | Ok(()) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /warpgate-db-migrations/src/m00011_rsa_key_algos.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::{ActiveModelTrait, EntityTrait, IntoActiveModel, Set}; 2 | use sea_orm_migration::prelude::*; 3 | use tracing::error; 4 | 5 | use crate::m00009_credential_models::public_key_credential as PKC; 6 | 7 | pub struct Migration; 8 | 9 | impl MigrationName for Migration { 10 | fn name(&self) -> &str { 11 | "m00011_rsa_key_algos" 12 | } 13 | } 14 | 15 | /// Re-save all keys so that rsa-sha2-* gets replaced with ssh-rsa 16 | /// since ssh-keys never serializes key type as rsa-sha2-* 17 | #[async_trait::async_trait] 18 | impl MigrationTrait for Migration { 19 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 20 | let connection = manager.get_connection(); 21 | let creds = PKC::Entity::find().all(connection).await?; 22 | for cred in creds.into_iter() { 23 | let parsed = match russh::keys::PublicKey::from_openssh(&cred.openssh_public_key) { 24 | Ok(parsed) => parsed, 25 | Err(e) => { 26 | error!("Failed to parse public key '{cred:?}': {e}"); 27 | continue; 28 | } 29 | }; 30 | let serialized = parsed 31 | .to_openssh() 32 | .map_err(|e| DbErr::Custom(format!("Failed to serialize public key: {e}")))?; 33 | let am = PKC::ActiveModel { 34 | openssh_public_key: Set(serialized), 35 | ..cred.into_active_model() 36 | }; 37 | am.update(connection).await?; 38 | } 39 | Ok(()) 40 | } 41 | 42 | async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> { 43 | Ok(()) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /warpgate-db-migrations/src/m00012_add_openssh_public_key_label.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | pub struct Migration; 4 | 5 | impl MigrationName for Migration { 6 | fn name(&self) -> &str { 7 | "m00012_add_openssh_public_key_label" 8 | } 9 | } 10 | 11 | use crate::m00009_credential_models::public_key_credential; 12 | 13 | #[async_trait::async_trait] 14 | impl MigrationTrait for Migration { 15 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 16 | manager 17 | .alter_table( 18 | Table::alter() 19 | .table(public_key_credential::Entity) 20 | .add_column( 21 | ColumnDef::new(Alias::new("label")) 22 | .string() 23 | .not_null() 24 | .default("Public Key"), 25 | ) 26 | .to_owned(), 27 | ) 28 | .await 29 | } 30 | 31 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 32 | manager 33 | .alter_table( 34 | Table::alter() 35 | .table(public_key_credential::Entity) 36 | .drop_column(Alias::new("label")) 37 | .to_owned(), 38 | ) 39 | .await 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /warpgate-db-migrations/src/m00013_add_openssh_public_key_dates.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | pub struct Migration; 4 | 5 | impl MigrationName for Migration { 6 | fn name(&self) -> &str { 7 | "m00013_add_openssh_public_key_dates" 8 | } 9 | } 10 | 11 | use crate::m00009_credential_models::public_key_credential; 12 | 13 | #[async_trait::async_trait] 14 | impl MigrationTrait for Migration { 15 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 16 | // Add 'date_added' column 17 | manager 18 | .alter_table( 19 | Table::alter() 20 | .table(public_key_credential::Entity) 21 | .add_column(ColumnDef::new(Alias::new("date_added")).date_time().null()) 22 | .to_owned(), 23 | ) 24 | .await?; 25 | 26 | // Add 'last_used' column 27 | manager 28 | .alter_table( 29 | Table::alter() 30 | .table(public_key_credential::Entity) 31 | .add_column(ColumnDef::new(Alias::new("last_used")).date_time().null()) 32 | .to_owned(), 33 | ) 34 | .await?; 35 | 36 | Ok(()) 37 | } 38 | 39 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 40 | // Drop 'last_used' column 41 | manager 42 | .alter_table( 43 | Table::alter() 44 | .table(public_key_credential::Entity) 45 | .drop_column(Alias::new("last_used")) 46 | .to_owned(), 47 | ) 48 | .await?; 49 | 50 | // Drop 'date_added' column 51 | manager 52 | .alter_table( 53 | Table::alter() 54 | .table(public_key_credential::Entity) 55 | .drop_column(Alias::new("date_added")) 56 | .to_owned(), 57 | ) 58 | .await?; 59 | 60 | Ok(()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /warpgate-db-migrations/src/m00016_fix_public_key_length.rs: -------------------------------------------------------------------------------- 1 | use sea_orm::DbBackend; 2 | use sea_orm_migration::prelude::*; 3 | 4 | /// The original column type was `String` which defaults to VARCHAR(255) on MySQL 5 | pub struct Migration; 6 | 7 | impl MigrationName for Migration { 8 | fn name(&self) -> &str { 9 | "m00016_fix_public_key_length" 10 | } 11 | } 12 | 13 | use crate::m00009_credential_models::public_key_credential; 14 | 15 | #[async_trait::async_trait] 16 | impl MigrationTrait for Migration { 17 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 18 | let connection = manager.get_connection(); 19 | if connection.get_database_backend() != DbBackend::Sqlite { 20 | manager 21 | .alter_table( 22 | Table::alter() 23 | .table(public_key_credential::Entity) 24 | .modify_column(ColumnDef::new(Alias::new("openssh_public_key")).text()) 25 | .to_owned(), 26 | ) 27 | .await?; 28 | } 29 | Ok(()) 30 | } 31 | 32 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 33 | let connection = manager.get_connection(); 34 | if connection.get_database_backend() != DbBackend::Sqlite { 35 | manager 36 | .alter_table( 37 | Table::alter() 38 | .table(public_key_credential::Entity) 39 | .modify_column(ColumnDef::new(Alias::new("openssh_public_key")).string()) 40 | .to_owned(), 41 | ) 42 | .await?; 43 | } 44 | Ok(()) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /warpgate-db-migrations/src/m00018_ticket_description.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | 3 | use crate::m00001_create_ticket::ticket; 4 | 5 | pub struct Migration; 6 | 7 | impl MigrationName for Migration { 8 | fn name(&self) -> &str { 9 | "m00018_ticket_description" 10 | } 11 | } 12 | 13 | #[async_trait::async_trait] 14 | impl MigrationTrait for Migration { 15 | async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> { 16 | manager 17 | .alter_table( 18 | Table::alter() 19 | .table(ticket::Entity) 20 | .add_column( 21 | ColumnDef::new(Alias::new("description")) 22 | .text() 23 | .not_null() 24 | .default(""), 25 | ) 26 | .to_owned(), 27 | ) 28 | .await?; 29 | Ok(()) 30 | } 31 | 32 | async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> { 33 | manager 34 | .alter_table( 35 | Table::alter() 36 | .table(ticket::Entity) 37 | .drop_column(Alias::new("description")) 38 | .to_owned(), 39 | ) 40 | .await?; 41 | Ok(()) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /warpgate-db-migrations/src/main.rs: -------------------------------------------------------------------------------- 1 | use sea_orm_migration::prelude::*; 2 | use warpgate_db_migrations::Migrator; 3 | 4 | #[tokio::main] 5 | async fn main() { 6 | cli::run_cli(Migrator).await; 7 | } 8 | -------------------------------------------------------------------------------- /warpgate-protocol-http/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-protocol-http" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | anyhow = "1.0" 9 | async-trait = "0.1" 10 | chrono = { version = "0.4", default-features = false, features = ["serde"] } 11 | cookie = "0.18" 12 | data-encoding.workspace = true 13 | delegate.workspace = true 14 | futures.workspace = true 15 | http = "1.0" 16 | once_cell = "1.17" 17 | poem.workspace = true 18 | poem-openapi = { version = "5.1", features = ["swagger-ui"] } 19 | reqwest = { version = "0.12", features = [ 20 | "http2", # required for connecting to targets behind AWS ELB 21 | "rustls-tls-native-roots-no-provider", 22 | "stream", 23 | ], default-features = false } 24 | sea-orm.workspace = true 25 | serde.workspace = true 26 | serde_json.workspace = true 27 | tokio = { version = "1.20", features = ["tracing", "signal"] } 28 | tokio-tungstenite = { version = "0.25", features = ["rustls-tls-native-roots"] } 29 | tracing.workspace = true 30 | warpgate-admin = { version = "*", path = "../warpgate-admin" } 31 | warpgate-common = { version = "*", path = "../warpgate-common" } 32 | warpgate-core = { version = "*", path = "../warpgate-core" } 33 | warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" } 34 | warpgate-web = { version = "*", path = "../warpgate-web" } 35 | warpgate-sso = { version = "*", path = "../warpgate-sso" } 36 | percent-encoding = "2.1" 37 | uuid = { version = "1.3", features = ["v4"] } 38 | regex = "1.6" 39 | url = "2.4" 40 | -------------------------------------------------------------------------------- /warpgate-protocol-http/src/api/common.rs: -------------------------------------------------------------------------------- 1 | use poem::session::Session; 2 | use sea_orm::{ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter}; 3 | use tracing::info; 4 | use warpgate_common::WarpgateError; 5 | use warpgate_db_entities as entities; 6 | 7 | use crate::common::RequestAuthorization; 8 | use crate::session::SessionStore; 9 | 10 | pub fn logout(session: &Session, session_middleware: &mut SessionStore) { 11 | session_middleware.remove_session(session); 12 | session.clear(); 13 | info!("Logged out"); 14 | } 15 | 16 | pub async fn get_user( 17 | auth: &RequestAuthorization, 18 | db: &DatabaseConnection, 19 | ) -> Result, WarpgateError> { 20 | let Some(username) = auth.username() else { 21 | return Ok(None); 22 | }; 23 | 24 | let Some(user_model) = entities::User::Entity::find() 25 | .filter(entities::User::Column::Username.eq(username)) 26 | .one(db) 27 | .await? 28 | else { 29 | return Ok(None); 30 | }; 31 | 32 | Ok(Some(user_model)) 33 | } 34 | -------------------------------------------------------------------------------- /warpgate-protocol-http/src/api/mod.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::auth::ApiKey; 2 | use poem_openapi::{OpenApi, SecurityScheme}; 3 | 4 | mod api_tokens; 5 | pub mod auth; 6 | mod common; 7 | mod credentials; 8 | pub mod info; 9 | pub mod sso_provider_detail; 10 | pub mod sso_provider_list; 11 | pub mod targets_list; 12 | 13 | #[derive(SecurityScheme)] 14 | #[oai(ty = "api_key", key_name = "X-Warpgate-Token", key_in = "header")] 15 | #[allow(dead_code)] 16 | pub struct AnySecurityScheme(ApiKey); 17 | 18 | struct StubApi; 19 | 20 | #[OpenApi] 21 | impl StubApi { 22 | #[oai(path = "/__stub__", method = "get", operation_id = "__stub__")] 23 | async fn stub(&self, _auth: AnySecurityScheme) -> poem::Result<()> { 24 | Ok(()) 25 | } 26 | } 27 | 28 | pub fn get() -> impl OpenApi { 29 | ( 30 | StubApi, 31 | auth::Api, 32 | info::Api, 33 | targets_list::Api, 34 | sso_provider_list::Api, 35 | sso_provider_detail::Api, 36 | credentials::Api, 37 | api_tokens::Api, 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /warpgate-protocol-http/src/error.rs: -------------------------------------------------------------------------------- 1 | use http::StatusCode; 2 | use poem::IntoResponse; 3 | use tracing::error; 4 | 5 | pub fn error_page(e: poem::Error) -> impl IntoResponse { 6 | error!("{:?}", e); 7 | poem::web::Html(format!( 8 | r#" 9 | 23 |
24 | 25 |

Request failed

26 |

{e}

27 |
28 | "# 29 | )).with_status(StatusCode::BAD_GATEWAY) 30 | } 31 | -------------------------------------------------------------------------------- /warpgate-protocol-http/src/main.rs: -------------------------------------------------------------------------------- 1 | use poem_openapi::OpenApiService; 2 | use regex::Regex; 3 | use warpgate_common::version::warpgate_version; 4 | use warpgate_protocol_http::api; 5 | 6 | #[allow(clippy::unwrap_used)] 7 | pub fn main() { 8 | let api_service = OpenApiService::new(api::get(), "Warpgate HTTP proxy", warpgate_version()) 9 | .server("/@warpgate/api"); 10 | 11 | let spec = api_service.spec(); 12 | let re = Regex::new(r"PaginatedResponse<(?P\w+)>").unwrap(); 13 | let spec = re.replace_all(&spec, "Paginated$name"); 14 | 15 | println!("{spec}"); 16 | } 17 | -------------------------------------------------------------------------------- /warpgate-protocol-http/src/middleware/cookie_host.rs: -------------------------------------------------------------------------------- 1 | use http::header::Entry; 2 | use poem::web::cookie::Cookie; 3 | use poem::{Endpoint, IntoResponse, Middleware, Request, Response}; 4 | 5 | use crate::common::SESSION_COOKIE_NAME; 6 | 7 | pub struct CookieHostMiddleware {} 8 | 9 | impl CookieHostMiddleware { 10 | pub fn new() -> Self { 11 | Self {} 12 | } 13 | } 14 | 15 | pub struct CookieHostMiddlewareEndpoint { 16 | inner: E, 17 | } 18 | 19 | impl Middleware for CookieHostMiddleware { 20 | type Output = CookieHostMiddlewareEndpoint; 21 | 22 | fn transform(&self, inner: E) -> Self::Output { 23 | CookieHostMiddlewareEndpoint { inner } 24 | } 25 | } 26 | 27 | impl Endpoint for CookieHostMiddlewareEndpoint { 28 | type Output = Response; 29 | 30 | async fn call(&self, req: Request) -> poem::Result { 31 | let host = req.original_uri().host().map(|x| x.to_string()); 32 | 33 | let mut resp = self.inner.call(req).await?.into_response(); 34 | 35 | if let Some(host) = host { 36 | if let Entry::Occupied(mut entry) = resp.headers_mut().entry(http::header::SET_COOKIE) { 37 | if let Ok(cookie_str) = entry.get().to_str() { 38 | if let Ok(mut cookie) = Cookie::parse(cookie_str) { 39 | if cookie.name() == SESSION_COOKIE_NAME { 40 | cookie.set_domain(host); 41 | if let Ok(value) = cookie.to_string().parse() { 42 | entry.insert(value); 43 | } 44 | } 45 | } 46 | } 47 | } 48 | } 49 | Ok(resp) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /warpgate-protocol-http/src/middleware/mod.rs: -------------------------------------------------------------------------------- 1 | mod cookie_host; 2 | mod ticket; 3 | 4 | pub use cookie_host::*; 5 | pub use ticket::*; 6 | -------------------------------------------------------------------------------- /warpgate-protocol-http/src/session_handle.rs: -------------------------------------------------------------------------------- 1 | use std::any::type_name; 2 | use std::sync::Arc; 3 | 4 | use poem::error::GetDataError; 5 | use poem::session::Session; 6 | use poem::web::Data; 7 | use poem::{FromRequest, Request, RequestBody}; 8 | use tokio::sync::{mpsc, Mutex}; 9 | use warpgate_core::{SessionHandle, WarpgateServerHandle}; 10 | 11 | use crate::session::SessionStore; 12 | 13 | #[derive(Clone, Debug, PartialEq, Eq)] 14 | pub enum SessionHandleCommand { 15 | Close, 16 | } 17 | 18 | pub struct HttpSessionHandle { 19 | sender: mpsc::UnboundedSender, 20 | } 21 | 22 | impl HttpSessionHandle { 23 | pub fn new() -> (Self, mpsc::UnboundedReceiver) { 24 | let (sender, receiver) = mpsc::unbounded_channel(); 25 | (HttpSessionHandle { sender }, receiver) 26 | } 27 | } 28 | 29 | impl SessionHandle for HttpSessionHandle { 30 | fn close(&mut self) { 31 | let _ = self.sender.send(SessionHandleCommand::Close); 32 | } 33 | } 34 | 35 | #[derive(Clone)] 36 | pub struct WarpgateServerHandleFromRequest(pub Arc>); 37 | 38 | impl std::ops::Deref for WarpgateServerHandleFromRequest { 39 | type Target = Arc>; 40 | 41 | fn deref(&self) -> &Self::Target { 42 | &self.0 43 | } 44 | } 45 | 46 | impl<'a> FromRequest<'a> for WarpgateServerHandleFromRequest { 47 | async fn from_request(req: &'a Request, _: &mut RequestBody) -> poem::Result { 48 | let sm = Data::<&Arc>>::from_request_without_body(req).await?; 49 | let session = <&Session>::from_request_without_body(req).await?; 50 | Ok(sm 51 | .lock() 52 | .await 53 | .handle_for(session) 54 | .map(WarpgateServerHandleFromRequest) 55 | .ok_or_else(|| GetDataError(type_name::()))?) 56 | } 57 | } 58 | 59 | impl From>> for WarpgateServerHandleFromRequest { 60 | fn from(handle: Arc>) -> Self { 61 | WarpgateServerHandleFromRequest(handle) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /warpgate-protocol-mysql/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-protocol-mysql" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | warpgate-common = { version = "*", path = "../warpgate-common" } 9 | warpgate-core = { version = "*", path = "../warpgate-core" } 10 | warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" } 11 | warpgate-database-protocols = { version = "*", path = "../warpgate-database-protocols" } 12 | anyhow = { version = "1.0", features = ["std"] } 13 | async-trait = "0.1" 14 | futures.workspace = true 15 | tokio = { version = "1.20", features = ["tracing", "signal"] } 16 | tracing.workspace = true 17 | uuid = { version = "1.3", features = ["v4"] } 18 | bytes.workspace = true 19 | mysql_common = { version = "0.34", default-features = false } 20 | flate2 = { version = "1", features = ["zlib"] } # flate2 requires a backend selection feature, but mysql_common does not depend on any when default-features = false 21 | rand = "0.8" 22 | sha1 = "0.10" 23 | password-hash.workspace = true 24 | rustls.workspace = true 25 | rustls-pemfile = "1.0" 26 | tokio-rustls.workspace = true 27 | thiserror = "1.0" 28 | webpki = "0.22" 29 | once_cell = "1.17" 30 | -------------------------------------------------------------------------------- /warpgate-protocol-mysql/src/common.rs: -------------------------------------------------------------------------------- 1 | use sha1::Digest; 2 | use warpgate_common::ProtocolName; 3 | 4 | pub const PROTOCOL_NAME: ProtocolName = "MySQL"; 5 | 6 | pub fn compute_auth_challenge_response( 7 | challenge: [u8; 20], 8 | password: &str, 9 | ) -> Result { 10 | password_hash::Output::new( 11 | &{ 12 | let password_sha: [u8; 20] = sha1::Sha1::digest(password).into(); 13 | let password_sha_sha: [u8; 20] = sha1::Sha1::digest(password_sha).into(); 14 | let password_seed_2sha_sha: [u8; 20] = 15 | sha1::Sha1::digest([challenge, password_sha_sha].concat()).into(); 16 | 17 | let mut result = password_sha; 18 | result 19 | .iter_mut() 20 | .zip(password_seed_2sha_sha.iter()) 21 | .for_each(|(x1, x2)| *x1 ^= *x2); 22 | result 23 | }[..], 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /warpgate-protocol-mysql/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use warpgate_common::{MaybeTlsStreamError, RustlsSetupError, WarpgateError}; 4 | use warpgate_database_protocols::error::Error as SqlxError; 5 | 6 | use crate::stream::MySqlStreamError; 7 | 8 | #[derive(thiserror::Error, Debug)] 9 | pub enum MySqlError { 10 | #[error("protocol error: {0}")] 11 | ProtocolError(String), 12 | #[error("sudden disconnection")] 13 | Eof, 14 | #[error("server doesn't offer TLS")] 15 | TlsNotSupported, 16 | #[error("client doesn't support TLS")] 17 | TlsNotSupportedByClient, 18 | #[error("TLS setup failed: {0}")] 19 | TlsSetup(#[from] RustlsSetupError), 20 | #[error("TLS stream error: {0}")] 21 | Tls(#[from] MaybeTlsStreamError), 22 | #[error("Invalid domain name")] 23 | InvalidDomainName, 24 | #[error("sqlx error: {0}")] 25 | Sqlx(#[from] SqlxError), 26 | #[error("MySQL stream error: {0}")] 27 | MySqlStream(#[from] MySqlStreamError), 28 | #[error("I/O: {0}")] 29 | Io(#[from] std::io::Error), 30 | #[error("packet decode error: {0}")] 31 | Decode(Box), 32 | #[error(transparent)] 33 | Warpgate(#[from] WarpgateError), 34 | #[error(transparent)] 35 | Other(Box), 36 | } 37 | 38 | impl MySqlError { 39 | pub fn other(err: E) -> Self { 40 | Self::Other(Box::new(err)) 41 | } 42 | 43 | pub fn decode(err: SqlxError) -> Self { 44 | match err { 45 | SqlxError::Decode(err) => Self::Decode(err), 46 | _ => Self::Sqlx(err), 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /warpgate-protocol-mysql/src/session_handle.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::mpsc; 2 | use warpgate_core::SessionHandle; 3 | 4 | pub struct MySqlSessionHandle { 5 | abort_tx: mpsc::UnboundedSender<()>, 6 | } 7 | 8 | impl MySqlSessionHandle { 9 | pub fn new() -> (Self, mpsc::UnboundedReceiver<()>) { 10 | let (abort_tx, abort_rx) = mpsc::unbounded_channel(); 11 | (MySqlSessionHandle { abort_tx }, abort_rx) 12 | } 13 | } 14 | 15 | impl SessionHandle for MySqlSessionHandle { 16 | fn close(&mut self) { 17 | let _ = self.abort_tx.send(()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /warpgate-protocol-postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-protocol-postgres" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | warpgate-common = { version = "*", path = "../warpgate-common" } 9 | warpgate-core = { version = "*", path = "../warpgate-core" } 10 | anyhow = { version = "1.0", features = ["std"] } 11 | async-trait = "0.1" 12 | tokio = { version = "1.20", features = ["tracing", "signal"] } 13 | tracing.workspace = true 14 | uuid = { version = "1.2" } 15 | bytes.workspace = true 16 | rustls.workspace = true 17 | rustls-pemfile = "1.0" 18 | tokio-rustls.workspace = true 19 | thiserror = "1.0" 20 | rustls-native-certs = "0.8" 21 | pgwire = { version = "0.28" } 22 | rsasl = { version = "2.1.0", default-features = false, features = ["config_builder", "scram-sha-2", "std", "plain", "provider"] } 23 | futures.workspace = true 24 | -------------------------------------------------------------------------------- /warpgate-protocol-postgres/src/common.rs: -------------------------------------------------------------------------------- 1 | use warpgate_common::ProtocolName; 2 | 3 | pub const PROTOCOL_NAME: ProtocolName = "PostgreSQL"; 4 | -------------------------------------------------------------------------------- /warpgate-protocol-postgres/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::string::FromUtf8Error; 3 | 4 | use pgwire::error::PgWireError; 5 | use pgwire::messages::response::ErrorResponse; 6 | use rsasl::prelude::{SASLError, SessionError}; 7 | use warpgate_common::{MaybeTlsStreamError, RustlsSetupError, WarpgateError}; 8 | 9 | use crate::stream::PostgresStreamError; 10 | 11 | #[derive(thiserror::Error, Debug)] 12 | pub enum PostgresError { 13 | #[error("protocol error: {0}")] 14 | ProtocolError(String), 15 | #[error("remote error: {0:?}")] 16 | RemoteError(ErrorResponse), 17 | #[error("decode: {0}")] 18 | Decode(#[from] PgWireError), 19 | #[error("sudden disconnection")] 20 | Eof, 21 | #[error("stream: {0}")] 22 | Stream(#[from] PostgresStreamError), 23 | #[error("server doesn't offer TLS")] 24 | TlsNotSupported, 25 | #[error("TLS setup failed: {0}")] 26 | TlsSetup(#[from] RustlsSetupError), 27 | #[error("TLS stream error: {0}")] 28 | Tls(#[from] MaybeTlsStreamError), 29 | #[error("Invalid domain name")] 30 | InvalidDomainName, 31 | #[error("I/O: {0}")] 32 | Io(#[from] std::io::Error), 33 | #[error("UTF-8: {0}")] 34 | Utf8(#[from] FromUtf8Error), 35 | #[error("SASL: {0}")] 36 | Sasl(#[from] SASLError), 37 | #[error("SASL session: {0}")] 38 | SaslSession(#[from] SessionError), 39 | #[error("Password is required for authentication")] 40 | PasswordRequired, 41 | #[error(transparent)] 42 | Warpgate(#[from] WarpgateError), 43 | #[error(transparent)] 44 | Other(Box), 45 | } 46 | 47 | impl PostgresError { 48 | pub fn other(err: E) -> Self { 49 | Self::Other(Box::new(err)) 50 | } 51 | } 52 | 53 | impl From for PostgresError { 54 | fn from(e: ErrorResponse) -> Self { 55 | PostgresError::RemoteError(e) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /warpgate-protocol-postgres/src/session_handle.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::mpsc; 2 | use warpgate_core::SessionHandle; 3 | 4 | pub struct PostgresSessionHandle { 5 | abort_tx: mpsc::UnboundedSender<()>, 6 | } 7 | 8 | impl PostgresSessionHandle { 9 | pub fn new() -> (Self, mpsc::UnboundedReceiver<()>) { 10 | let (abort_tx, abort_rx) = mpsc::unbounded_channel(); 11 | (PostgresSessionHandle { abort_tx }, abort_rx) 12 | } 13 | } 14 | 15 | impl SessionHandle for PostgresSessionHandle { 16 | fn close(&mut self) { 17 | let _ = self.abort_tx.send(()); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /warpgate-protocol-ssh/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-protocol-ssh" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | ansi_term = "0.12" 9 | anyhow = { version = "1.0", features = ["std"] } 10 | async-trait = "0.1" 11 | bimap = "0.6" 12 | bytes.workspace = true 13 | dialoguer = "0.10" 14 | curve25519-dalek = "4.0.0" # pin due to build fail on x86 15 | ed25519-dalek = "2.0.0" # pin due to build fail on x86 in 2.1 16 | futures.workspace = true 17 | russh.workspace = true 18 | sea-orm.workspace = true 19 | thiserror = "1.0" 20 | time = "0.3" 21 | tokio = { version = "1.20", features = ["tracing", "signal"] } 22 | tracing.workspace = true 23 | uuid = { version = "1.3", features = ["v4"] } 24 | warpgate-common = { version = "*", path = "../warpgate-common" } 25 | warpgate-core = { version = "*", path = "../warpgate-core" } 26 | warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" } 27 | zeroize = "^1.5" 28 | -------------------------------------------------------------------------------- /warpgate-protocol-ssh/src/client/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use warpgate_common::WarpgateError; 4 | 5 | #[derive(thiserror::Error, Debug)] 6 | pub enum SshClientError { 7 | #[error("mpsc error")] 8 | MpscError, 9 | #[error("russh error: {0}")] 10 | Russh(#[from] russh::Error), 11 | #[error(transparent)] 12 | Warpgate(#[from] WarpgateError), 13 | #[error(transparent)] 14 | Other(Box), 15 | } 16 | 17 | impl SshClientError { 18 | pub fn other(err: E) -> Self { 19 | Self::Other(Box::new(err)) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /warpgate-protocol-ssh/src/common.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::{Display, Formatter}; 2 | 3 | use bytes::Bytes; 4 | use russh::{ChannelId, Pty, Sig}; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct PtyRequest { 8 | pub term: String, 9 | pub col_width: u32, 10 | pub row_height: u32, 11 | pub pix_width: u32, 12 | pub pix_height: u32, 13 | pub modes: Vec<(Pty, u32)>, 14 | } 15 | 16 | #[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] 17 | pub struct ServerChannelId(pub ChannelId); 18 | 19 | impl Display for ServerChannelId { 20 | fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { 21 | write!(f, "{}", self.0) 22 | } 23 | } 24 | 25 | #[derive(Clone, Debug)] 26 | pub struct DirectTCPIPParams { 27 | pub host_to_connect: String, 28 | pub port_to_connect: u32, 29 | pub originator_address: String, 30 | pub originator_port: u32, 31 | } 32 | 33 | #[derive(Clone, Debug)] 34 | pub struct ForwardedTcpIpParams { 35 | pub connected_address: String, 36 | pub connected_port: u32, 37 | pub originator_address: String, 38 | pub originator_port: u32, 39 | } 40 | 41 | #[derive(Clone, Debug)] 42 | pub struct ForwardedStreamlocalParams { 43 | pub socket_path: String, 44 | } 45 | 46 | #[derive(Clone, Debug)] 47 | pub struct X11Request { 48 | pub single_conection: bool, 49 | pub x11_auth_protocol: String, 50 | pub x11_auth_cookie: String, 51 | pub x11_screen_number: u32, 52 | } 53 | 54 | #[derive(Clone, Debug)] 55 | pub enum ChannelOperation { 56 | OpenShell, 57 | OpenDirectTCPIP(DirectTCPIPParams), 58 | OpenX11(String, u32), 59 | RequestPty(PtyRequest), 60 | ResizePty(PtyRequest), 61 | RequestShell, 62 | RequestEnv(String, String), 63 | RequestExec(String), 64 | RequestX11(X11Request), 65 | AgentForward, 66 | RequestSubsystem(String), 67 | Data(Bytes), 68 | ExtendedData { data: Bytes, ext: u32 }, 69 | Close, 70 | Eof, 71 | Signal(Sig), 72 | } 73 | -------------------------------------------------------------------------------- /warpgate-protocol-ssh/src/compat.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Display; 2 | 3 | pub trait ContextExt { 4 | fn context(self, context: C) -> anyhow::Result; 5 | } 6 | 7 | impl ContextExt for Result 8 | where 9 | C: Display + Send + Sync + 'static, 10 | { 11 | fn context(self, context: C) -> anyhow::Result { 12 | self.map_err(|_| anyhow::anyhow!("unspecified error").context(context)) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /warpgate-protocol-ssh/src/server/channel_writer.rs: -------------------------------------------------------------------------------- 1 | use russh::server::Handle; 2 | use russh::{ChannelId, CryptoVec}; 3 | use tokio::sync::mpsc; 4 | 5 | /// Sequences data writes and runs them in background to avoid lockups 6 | pub struct ChannelWriter { 7 | tx: mpsc::UnboundedSender<(Handle, ChannelId, CryptoVec)>, 8 | } 9 | 10 | impl ChannelWriter { 11 | pub fn new() -> Self { 12 | let (tx, mut rx) = mpsc::unbounded_channel::<(Handle, ChannelId, CryptoVec)>(); 13 | tokio::spawn(async move { 14 | while let Some((handle, channel, data)) = rx.recv().await { 15 | let _ = handle.data(channel, data).await; 16 | } 17 | }); 18 | ChannelWriter { tx } 19 | } 20 | 21 | pub fn write(&self, handle: Handle, channel: ChannelId, data: CryptoVec) { 22 | let _ = self.tx.send((handle, channel, data)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /warpgate-protocol-ssh/src/server/session_handle.rs: -------------------------------------------------------------------------------- 1 | use tokio::sync::mpsc; 2 | use warpgate_core::SessionHandle; 3 | 4 | #[derive(Clone, Debug, PartialEq, Eq)] 5 | pub enum SessionHandleCommand { 6 | Close, 7 | } 8 | 9 | pub struct SSHSessionHandle { 10 | sender: mpsc::UnboundedSender, 11 | } 12 | 13 | impl SSHSessionHandle { 14 | pub fn new() -> (Self, mpsc::UnboundedReceiver) { 15 | let (sender, receiver) = mpsc::unbounded_channel(); 16 | (SSHSessionHandle { sender }, receiver) 17 | } 18 | } 19 | 20 | impl SessionHandle for SSHSessionHandle { 21 | fn close(&mut self) { 22 | let _ = self.sender.send(SessionHandleCommand::Close); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /warpgate-sso/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-sso" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | bytes.workspace = true 9 | thiserror = "1.0" 10 | tokio = { version = "1.20", features = ["tracing", "macros"] } 11 | tracing.workspace = true 12 | openidconnect = { version = "4.0", default-features = false, features = [ 13 | "reqwest", 14 | "accept-string-booleans", 15 | ] } 16 | serde.workspace = true 17 | serde_json.workspace = true 18 | once_cell = "1.17" 19 | jsonwebtoken = "9" 20 | data-encoding.workspace = true 21 | futures.workspace = true 22 | -------------------------------------------------------------------------------- /warpgate-sso/src/error.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | 3 | use openidconnect::{ 4 | reqwest, ClaimsVerificationError, ConfigurationError, SignatureVerificationError, SigningError, 5 | }; 6 | 7 | #[derive(thiserror::Error, Debug)] 8 | pub enum SsoError { 9 | #[error("provider is OAuth2, not OIDC")] 10 | NotOidc, 11 | #[error("the token was replaced in flight")] 12 | Mitm, 13 | #[error("config parse error: {0}")] 14 | UrlParse(#[from] openidconnect::url::ParseError), 15 | #[error("config error: {0}")] 16 | ConfigError(String), 17 | #[error("provider discovery error: {0}")] 18 | Discovery(String), 19 | #[error("code verification error: {0}")] 20 | Verification(String), 21 | #[error("claims verification error: {0}")] 22 | ClaimsVerification(#[from] ClaimsVerificationError), 23 | #[error("signing error: {0}")] 24 | Signing(#[from] SigningError), 25 | #[error("reqwest: {0}")] 26 | Reqwest(#[from] reqwest::Error), 27 | #[error("I/O: {0}")] 28 | Io(#[from] std::io::Error), 29 | #[error("JWT error: {0}")] 30 | Jwt(#[from] jsonwebtoken::errors::Error), 31 | #[error("signature verification: {0}")] 32 | SignatureVerification(#[from] SignatureVerificationError), 33 | #[error("configuration: {0}")] 34 | Configuration(#[from] ConfigurationError), 35 | #[error("the OIDC provider doesn't support RP-initiated logout")] 36 | LogoutNotSupported, 37 | #[error(transparent)] 38 | Other(Box), 39 | } 40 | -------------------------------------------------------------------------------- /warpgate-sso/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod error; 3 | mod request; 4 | mod response; 5 | mod sso; 6 | 7 | pub use config::*; 8 | pub use error::*; 9 | pub use openidconnect::core::CoreIdToken; 10 | pub use request::*; 11 | pub use response::*; 12 | pub use sso::*; 13 | -------------------------------------------------------------------------------- /warpgate-sso/src/response.rs: -------------------------------------------------------------------------------- 1 | use openidconnect::core::CoreIdToken; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct SsoLoginResponse { 5 | pub name: Option, 6 | pub email: Option, 7 | pub email_verified: Option, 8 | pub groups: Option>, 9 | pub id_token: CoreIdToken, 10 | pub preferred_username: Option, 11 | } 12 | -------------------------------------------------------------------------------- /warpgate-web/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | end_of_line = lf 10 | -------------------------------------------------------------------------------- /warpgate-web/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | #--- 27 | 28 | api-client 29 | -------------------------------------------------------------------------------- /warpgate-web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate-web" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | serde.workspace = true 9 | rust-embed = "8.3" 10 | serde_json.workspace = true 11 | thiserror = "1.0" 12 | -------------------------------------------------------------------------------- /warpgate-web/openapitools.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/@openapitools/openapi-generator-cli/config.schema.json", 3 | "spaces": 2, 4 | "generator-cli": { 5 | "version": "7.7.0" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/CreatePasswordModal.svelte: -------------------------------------------------------------------------------- 1 | 39 | 40 | field?.focus()}> 41 |
{ 42 | _save() 43 | e.preventDefault() 44 | }}> 45 | 46 | 47 | 53 | 54 | 55 | 56 | 61 | 62 | 67 | 68 |
69 |
70 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/Log.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 |
6 |

log

7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/Recording.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 | 32 |
33 |

session recording

34 |
35 | 36 | {#if !recording && !error} 37 | 38 | {/if} 39 | 40 | {#if error} 41 | {error} 42 | {/if} 43 | 44 | {#if recording?.kind === 'Traffic'} 45 | Download tcpdump file 46 | {/if} 47 | {#if recording?.kind === 'Terminal'} 48 | 49 | {/if} 50 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/RelativeDate.svelte: -------------------------------------------------------------------------------- 1 | 9 | 10 | {timeAgo(date)} 11 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/TlsConfiguration.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
14 |
15 | 16 | 21 | 22 |
23 | {#if value.mode !== TlsMode.Disabled} 24 |
25 | 26 |
27 | {/if} 28 |
29 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/config/CreateRole.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 | {#if error} 29 | {error} 30 | {/if} 31 | 32 |
33 |

add a role

34 |
35 | 36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | Create role 47 |
48 |
49 |
50 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/config/CreateUser.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 | {#if error} 29 | {error} 30 | {/if} 31 | 32 |
33 |

add a user

34 |
35 |
36 |
37 | 38 | 39 | 40 | 41 | Create user 45 |
46 |
47 |
48 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/config/Parameters.svelte: -------------------------------------------------------------------------------- 1 | 14 | 15 |
16 |

global parameters

17 |
18 | 19 | 20 | {#if parameters} 21 | 40 | {/if} 41 | 42 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/config/Roles.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |
20 |

roles

21 | 25 | Add a role 26 | 27 |
28 | 29 | 30 | {#snippet item(role)} 31 | 35 |
36 | 37 | {role.name} 38 | 39 | {#if role.description} 40 | {role.description} 41 | {/if} 42 |
43 |
44 | {/snippet} 45 |
46 |
47 | 48 | 54 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/config/Users.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 |
19 |
20 |

users

21 | 25 | Add a user 26 | 27 |
28 | 29 | 30 | {#snippet item(user)} 31 | 35 |
36 | 37 | {user.username} 38 | 39 | {#if user.description} 40 | {user.description} 41 | {/if} 42 |
43 |
44 | {/snippet} 45 |
46 |
47 | 48 | 54 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Warpgate 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/index.ts: -------------------------------------------------------------------------------- 1 | import { mount } from 'svelte' 2 | import '../theme' 3 | import App from './App.svelte' 4 | 5 | mount(App, { 6 | target: document.getElementById('app')!, 7 | }) 8 | 9 | export { } 10 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/lib/api.ts: -------------------------------------------------------------------------------- 1 | import { DefaultApi, Configuration, ResponseError } from './api-client/dist' 2 | 3 | const configuration = new Configuration({ 4 | basePath: '/@warpgate/admin/api', 5 | }) 6 | 7 | export const api = new DefaultApi(configuration) 8 | export * from './api-client' 9 | 10 | export async function stringifyError (err: ResponseError): Promise { 11 | return `API error: ${await err.response.text()}` 12 | } 13 | -------------------------------------------------------------------------------- /warpgate-web/src/admin/lib/time.ts: -------------------------------------------------------------------------------- 1 | import { formatDistanceToNow } from 'date-fns' 2 | 3 | export function timeAgo(t: Date): string { 4 | return formatDistanceToNow(t, { addSuffix: true }) 5 | } 6 | -------------------------------------------------------------------------------- /warpgate-web/src/common/AuthBar.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | {#if $serverInfo?.username} 22 |
23 | 24 | {$serverInfo.username} 25 | 26 | {#if $serverInfo.authorizedViaTicket} 27 | (ticket auth) 28 | {/if} 29 |
30 | 31 | {#if $serverInfo?.authorizedViaSsoWithSingleLogout} 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | Log out of Warpgate 40 | 41 | 42 | 43 | Log out everywhere 44 | 45 | 46 | 47 | {:else} 48 | 51 | {/if} 52 | {/if} 53 | -------------------------------------------------------------------------------- /warpgate-web/src/common/Brand.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 | 34 |
35 | 36 | {@html logo} 37 |
38 | 39 | 40 | 51 | -------------------------------------------------------------------------------- /warpgate-web/src/common/CredentialUsedStateBadge.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | {#if credential.lastUsed} 20 | {#if credential.lastUsed.getTime() < lastUseThreshold.getTime()} 21 | Not used recently 22 | {:else} 23 | Used recently 24 | {/if} 25 | {:else} 26 | Never used 27 | {/if} 28 | 29 | 30 | {#if credential.dateAdded || credential.lastUsed} 31 | 32 | {#if credential.dateAdded} 33 |
Added on: {new Date(credential.dateAdded).toLocaleString()}
34 | {/if} 35 | {#if credential.lastUsed} 36 |
Last used: {new Date(credential.lastUsed).toLocaleString()}
37 | {/if} 38 |
39 | {/if} 40 | -------------------------------------------------------------------------------- /warpgate-web/src/common/DelayedSpinner.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | {#if visible} 12 | 13 | {/if} 14 | -------------------------------------------------------------------------------- /warpgate-web/src/common/EmptyState.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |

{title}

8 | {#if hint} 9 |

{hint}

10 | {/if} 11 |
12 | 13 | 34 | -------------------------------------------------------------------------------- /warpgate-web/src/common/Loadable.svelte: -------------------------------------------------------------------------------- 1 | 31 | 32 | {#if !loaded} 33 | 34 | {:else} 35 | {#if !error} 36 | {@render children?.(data!)} 37 | {:else} 38 | 39 | {error} 40 | 41 | {/if} 42 | {/if} 43 | -------------------------------------------------------------------------------- /warpgate-web/src/common/Pagination.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 | 31 | 32 | page--} href="#"> 33 | 34 | 35 | 36 | {#each pages as i} 37 | {#if i !== null} 38 | 39 | page = i} href="#">{i + 1} 40 | 41 | {:else} 42 | 43 | ... 44 | 45 | {/if} 46 | {/each} 47 | = total}> 48 | page++} href="#"> 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /warpgate-web/src/common/RadioButton.svelte: -------------------------------------------------------------------------------- 1 | 23 | 24 | 36 | 37 | 38 | 47 | -------------------------------------------------------------------------------- /warpgate-web/src/common/ThemeSwitcher.svelte: -------------------------------------------------------------------------------- 1 | 20 | 21 | 30 | 31 | {#if $currentTheme === 'dark'} 32 | Dark theme 33 | {:else if $currentTheme === 'light'} 34 | Light theme 35 | {:else} 36 | Automatic theme 37 | {/if} 38 | 39 | -------------------------------------------------------------------------------- /warpgate-web/src/common/autosave.ts: -------------------------------------------------------------------------------- 1 | import { BehaviorSubject } from 'rxjs' 2 | import { get, writable, type Writable } from 'svelte/store' 3 | 4 | export function autosave (key: string, initial: T): ([Writable, BehaviorSubject]) { 5 | key = `warpgate:${key}` 6 | const v = writable(JSON.parse(localStorage.getItem(key) ?? JSON.stringify(initial))) 7 | const v$ = new BehaviorSubject(get(v)) 8 | v.subscribe(value => { 9 | localStorage.setItem(key, JSON.stringify(value)) 10 | v$.next(value) 11 | }) 12 | return [v, v$] 13 | } 14 | -------------------------------------------------------------------------------- /warpgate-web/src/common/errors.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'admin/lib/api' 2 | import * as gw from 'gateway/lib/api' 3 | 4 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 5 | export async function stringifyError (err: any): Promise { 6 | if (err instanceof gw.ResponseError) { 7 | return gw.stringifyError(err) 8 | } 9 | if (err instanceof admin.ResponseError) { 10 | return admin.stringifyError(err) 11 | } 12 | return err.toString() 13 | } 14 | -------------------------------------------------------------------------------- /warpgate-web/src/common/sveltestrap-s5-ports/ModalHeader.svelte: -------------------------------------------------------------------------------- 1 | 26 | 27 |
28 | 31 | {#if close}{@render close()}{:else} 32 | {#if typeof toggle === 'function'} 33 | 34 | {/if} 35 | {/if} 36 |
37 | -------------------------------------------------------------------------------- /warpgate-web/src/common/sveltestrap-s5-ports/_sveltestrapUtils.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 2 | export function toClassName(value: any) { 3 | let result = '' 4 | 5 | if (typeof value === 'string' || typeof value === 'number') { 6 | result += value 7 | } else if (typeof value === 'object') { 8 | if (Array.isArray(value)) { 9 | result = value.map(toClassName).filter(Boolean).join(' ') 10 | } else { 11 | for (const key in value) { 12 | if (value[key]) { 13 | // eslint-disable-next-line @typescript-eslint/no-unused-expressions 14 | result && (result += ' ') 15 | result += key 16 | } 17 | } 18 | } 19 | } 20 | 21 | return result 22 | } 23 | 24 | // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types 25 | export const classnames = (...args: any[]) => args.map(toClassName).filter(Boolean).join(' ') 26 | 27 | 28 | export function uuid(): string { 29 | return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => { 30 | const r = (Math.random() * 16) | 0 31 | const v = c === 'x' ? r : (r & 0x3) | 0x8 32 | return v.toString(16) 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /warpgate-web/src/embed/index.ts: -------------------------------------------------------------------------------- 1 | import { api } from 'gateway/lib/api' 2 | import EmbeddedUI from './EmbeddedUI.svelte' 3 | 4 | export { } 5 | 6 | navigator.serviceWorker.getRegistrations().then(registrations => { 7 | for (const registration of registrations) { 8 | registration.unregister() 9 | } 10 | }) 11 | 12 | api.getInfo().then(info => { 13 | console.log(`Warpgate v${info.version}, logged in as ${info.username}`) 14 | }) 15 | 16 | const container = document.createElement('div') 17 | container.id = 'warpgate-embedded-ui' 18 | document.body.appendChild(container) 19 | 20 | setTimeout(() => new EmbeddedUI({ 21 | target: container, 22 | })) 23 | -------------------------------------------------------------------------------- /warpgate-web/src/gateway/Profile.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
7 |

{$serverInfo!.username}

8 |
9 | 10 | 15 | 16 | {#if $serverInfo} 17 | {#if $serverInfo.ownCredentialManagementAllowed} 18 | 23 | {/if} 24 | {/if} 25 | -------------------------------------------------------------------------------- /warpgate-web/src/gateway/ProfileApiTokens.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | 13 |
14 | 25 | 26 | 37 |
38 | 39 | 49 | -------------------------------------------------------------------------------- /warpgate-web/src/gateway/ProfileCredentials.svelte: -------------------------------------------------------------------------------- 1 | 6 | 7 |
8 |

Credentials

9 |
10 | 11 | {#if $serverInfo} 12 | {#if $serverInfo.ownCredentialManagementAllowed} 13 | 14 | {:else} 15 | 16 | Credential management is disabled by your administrator 17 | 18 | {/if} 19 | {/if} 20 | -------------------------------------------------------------------------------- /warpgate-web/src/gateway/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Warpgate 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /warpgate-web/src/gateway/index.ts: -------------------------------------------------------------------------------- 1 | import { mount } from 'svelte' 2 | import '../theme' 3 | import App from './App.svelte' 4 | 5 | mount(App, { 6 | target: document.getElementById('app')!, 7 | }) 8 | 9 | export { } 10 | -------------------------------------------------------------------------------- /warpgate-web/src/gateway/lib/api.ts: -------------------------------------------------------------------------------- 1 | import { DefaultApi, Configuration, ResponseError } from './api-client' 2 | 3 | const configuration = new Configuration({ 4 | basePath: '/@warpgate/api', 5 | }) 6 | 7 | export const api = new DefaultApi(configuration) 8 | export * from './api-client' 9 | 10 | export async function stringifyError (err: ResponseError): Promise { 11 | return `API error: ${await err.response.text()}` 12 | } 13 | -------------------------------------------------------------------------------- /warpgate-web/src/gateway/lib/shellEscape.ts: -------------------------------------------------------------------------------- 1 | import { UAParser } from 'ua-parser-js' 2 | 3 | function escapeUnix (arg: string): string { 4 | if (!/^[A-Za-z0-9_/-]+$/.test(arg)) { 5 | return ('\'' + arg.replace(/'/g, '\'"\'"\'') + '\'').replace(/''/g, '') 6 | } 7 | return arg 8 | } 9 | 10 | function escapeWin (arg: string): string { 11 | if (!/^[A-Za-z0-9_/-]+$/.test(arg)) { 12 | return '"' + arg.replace(/"/g, '""') + '"' 13 | } 14 | return arg 15 | } 16 | 17 | const isWin = new UAParser().getOS().name === 'Windows' 18 | 19 | export function shellEscape (stringOrArray: string[]|string): string { 20 | const ret: string[] = [] 21 | 22 | const escapePath = isWin ? escapeWin : escapeUnix 23 | 24 | if (typeof stringOrArray == 'string') { 25 | return escapePath(stringOrArray) 26 | } else { 27 | stringOrArray.forEach(function (member) { 28 | ret.push(escapePath(member)) 29 | }) 30 | return ret.join(' ') 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /warpgate-web/src/gateway/lib/store.ts: -------------------------------------------------------------------------------- 1 | import { writable } from 'svelte/store' 2 | import { api, type Info } from './api' 3 | 4 | export const serverInfo = writable(undefined) 5 | 6 | export async function reloadServerInfo (): Promise { 7 | serverInfo.set(await api.getInfo()) 8 | } 9 | -------------------------------------------------------------------------------- /warpgate-web/src/gateway/login.ts: -------------------------------------------------------------------------------- 1 | import { mount } from 'svelte' 2 | import Login from './Login.svelte' 3 | 4 | const app = {} 5 | mount(Login, { 6 | target: document.getElementById('app')!, 7 | }) 8 | 9 | export default app 10 | -------------------------------------------------------------------------------- /warpgate-web/src/lib.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use rust_embed::RustEmbed; 4 | use serde::Deserialize; 5 | 6 | #[derive(RustEmbed)] 7 | #[folder = "../warpgate-web/dist"] 8 | pub struct Assets; 9 | 10 | #[derive(thiserror::Error, Debug)] 11 | pub enum LookupError { 12 | #[error("I/O")] 13 | Io(#[from] std::io::Error), 14 | 15 | #[error("Serde")] 16 | Serde(#[from] serde_json::Error), 17 | 18 | #[error("File not found in manifest")] 19 | FileNotFound, 20 | 21 | #[error("Manifest not found")] 22 | ManifestNotFound, 23 | } 24 | 25 | #[derive(Deserialize, Clone)] 26 | pub struct ManifestEntry { 27 | pub file: String, 28 | pub css: Option>, 29 | } 30 | 31 | pub fn lookup_built_file(source: &str) -> Result { 32 | let file = Assets::get(".vite/manifest.json").ok_or(LookupError::ManifestNotFound)?; 33 | 34 | let obj: HashMap = serde_json::from_slice(&file.data)?; 35 | 36 | obj.get(source).cloned().ok_or(LookupError::FileNotFound) 37 | } 38 | -------------------------------------------------------------------------------- /warpgate-web/src/theme/fonts.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Poppins'; 3 | font-style: normal; 4 | font-display: block; /* changed */ 5 | font-weight: 700; 6 | src: url(./../../node_modules/@fontsource/poppins/files/poppins-latin-700-normal.woff2) format('woff2'), url(./../../node_modules/@fontsource/poppins/files/poppins-latin-700-normal.woff) format('woff'); 7 | } 8 | -------------------------------------------------------------------------------- /warpgate-web/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | import '@fontsource/work-sans' 2 | import './fonts.css' 3 | 4 | import { get, writable } from 'svelte/store' 5 | 6 | type ThemeFileName = 'dark'|'light' 7 | type ThemeName = ThemeFileName|'auto' 8 | 9 | const savedTheme = (localStorage.getItem('theme') ?? 'auto') as ThemeName 10 | export const currentTheme = writable(savedTheme) 11 | export const currentThemeFile = writable('dark') 12 | 13 | const styleElement = document.createElement('style') 14 | document.head.appendChild(styleElement) 15 | 16 | function loadThemeFile (name: ThemeFileName) { 17 | currentThemeFile.set(name) 18 | if (name === 'dark') { 19 | return import('./theme.dark.scss?inline') 20 | } 21 | return import('./theme.light.scss?inline') 22 | } 23 | 24 | async function loadTheme (name: ThemeFileName) { 25 | const theme = (await loadThemeFile(name)).default 26 | styleElement.innerHTML = theme 27 | } 28 | 29 | 30 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => { 31 | if (get(currentTheme) === 'auto') { 32 | loadTheme(event.matches ? 'dark' : 'light') 33 | } 34 | }) 35 | 36 | 37 | export function setCurrentTheme (theme: ThemeName): void { 38 | localStorage.setItem('theme', theme) 39 | currentTheme.set(theme) 40 | if (theme === 'auto') { 41 | if (window.matchMedia?.('(prefers-color-scheme: dark)').matches) { 42 | loadTheme('dark') 43 | } else { 44 | loadTheme('light') 45 | } 46 | } else { 47 | loadTheme(theme) 48 | } 49 | } 50 | 51 | setCurrentTheme(savedTheme) 52 | -------------------------------------------------------------------------------- /warpgate-web/src/theme/theme.dark.scss: -------------------------------------------------------------------------------- 1 | @import "./vars.dark"; 2 | @import "bootstrap/scss/functions"; 3 | @import "bootstrap/scss/maps"; 4 | @import "bootstrap/scss/mixins"; 5 | 6 | @mixin button-outline-variant( 7 | $color, 8 | $color-hover: color-contrast($color), 9 | $active-background: $color, 10 | $active-border: $color, 11 | $active-color: color-contrast($active-background) 12 | ) { 13 | --#{$prefix}btn-color: lighten(#{$color}, 50%); 14 | --#{$prefix}btn-border-color: #{$color}; 15 | --#{$prefix}btn-hover-color: #{$color-hover}; 16 | --#{$prefix}btn-hover-bg: #{$active-background}; 17 | --#{$prefix}btn-hover-border-color: #{$active-border}; 18 | --#{$prefix}btn-focus-shadow-rgb: #{to-rgb($color)}; 19 | --#{$prefix}btn-active-color: #{$active-color}; 20 | --#{$prefix}btn-active-bg: #{$active-background}; 21 | --#{$prefix}btn-active-border-color: #{$active-border}; 22 | --#{$prefix}btn-active-shadow: #{$btn-active-box-shadow}; 23 | --#{$prefix}btn-disabled-color: #{$color}; 24 | --#{$prefix}btn-disabled-bg: transparent; 25 | --#{$prefix}gradient: none; 26 | } 27 | 28 | @import "bootstrap/scss/utilities"; 29 | 30 | @import "theme"; 31 | 32 | header { 33 | border-bottom: 1px solid rgba($body-color, .2); 34 | } 35 | 36 | .list-group-item-action { 37 | transition: 0.125s ease-out background; 38 | } 39 | 40 | body { 41 | color-scheme: dark; 42 | } 43 | -------------------------------------------------------------------------------- /warpgate-web/src/theme/theme.light.scss: -------------------------------------------------------------------------------- 1 | @import "./vars.light"; 2 | @import "bootstrap/scss/functions"; 3 | @import "bootstrap/scss/maps"; 4 | @import "bootstrap/scss/mixins"; 5 | @import "bootstrap/scss/utilities"; 6 | @import "theme"; 7 | 8 | header { 9 | border-bottom: 1px solid rgba($body-color, .75); 10 | } 11 | 12 | body { 13 | color-scheme: light; 14 | } 15 | -------------------------------------------------------------------------------- /warpgate-web/src/theme/vars.common.scss: -------------------------------------------------------------------------------- 1 | $pagination-bg: transparent; 2 | $pagination-disabled-bg: transparent; 3 | $pagination-disabled-color: $btn-link-disabled-color; 4 | $pagination-border-width: 0; 5 | $pagination-active-color: $link-hover-color; 6 | $pagination-active-bg: transparent; 7 | $pagination-hover-bg: transparent; 8 | $pagination-focus-bg: transparent; 9 | $modal-header-border-color: transparent; 10 | $dropdown-link-hover-bg: transparent; 11 | 12 | $btn-padding-x: 1.5rem; 13 | 14 | $btn-bg-shade-amount: 75%; 15 | $btn-bg-tint-amount: 60%; 16 | $btn-border-shade-amount: 75%; 17 | $btn-border-tint-amount: 65%; 18 | $btn-color-shade-amount: 10%; 19 | $btn-color-tint-amount: 50%; 20 | 21 | $btn-hover-bg-shade-amount: 60%; 22 | $btn-hover-bg-tint-amount: 50%; 23 | $btn-hover-border-shade-amount: 60%; 24 | $btn-hover-border-tint-amount: 10%; 25 | 26 | $btn-active-bg-shade-amount: 50%; 27 | // $btn-active-bg-tint-amount 28 | $btn-active-border-shade-amount: 40%; 29 | // $btn-active-border-tint-amount 30 | 31 | $tooltip-color: #c1c9e4; 32 | 33 | $badge-font-size: .8em; 34 | $badge-font-weight: 400; 35 | $badge-padding-y: .55em; 36 | $badge-padding-x: .85em; 37 | 38 | $alert-border-width: 0; 39 | $alert-border-scale: -30%; 40 | -------------------------------------------------------------------------------- /warpgate-web/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // eslint-disable-next-line @typescript-eslint/no-type-alias 5 | declare type GlobalFetch = WindowOrWorkerGlobalScope 6 | -------------------------------------------------------------------------------- /warpgate-web/svelte.config.js: -------------------------------------------------------------------------------- 1 | import sveltePreprocess from 'svelte-preprocess' 2 | 3 | /** @type {import('@sveltejs/kit').Config} */ 4 | const config = { 5 | compilerOptions: { 6 | enableSourcemap: true, 7 | dev: true, 8 | compatibility: { 9 | componentApi: 4, 10 | }, 11 | }, 12 | preprocess: sveltePreprocess({ 13 | sourceMap: true, 14 | }), 15 | vitePlugin: { 16 | prebundleSvelteLibraries: true, 17 | }, 18 | } 19 | 20 | export default config 21 | -------------------------------------------------------------------------------- /warpgate-web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/svelte/tsconfig.json", 3 | "compilerOptions": { 4 | "target": "esnext", 5 | "useDefineForClassFields": true, 6 | "module": "esnext", 7 | "resolveJsonModule": true, 8 | "strictNullChecks": true, 9 | "baseUrl": ".", 10 | "verbatimModuleSyntax": true, 11 | "noUnusedLocals": false, 12 | "noUncheckedIndexedAccess": true, 13 | /** 14 | * Typecheck JS in `.svelte` and `.js` files by default. 15 | * Disable checkJs if you'd like to use dynamic types in JS. 16 | * Note that setting allowJs false does not prevent the use 17 | * of JS in `.svelte` files. 18 | */ 19 | "types": [], 20 | "allowJs": true, 21 | "checkJs": true, 22 | "paths": { 23 | "*": [ 24 | "src/*" 25 | ] 26 | } 27 | }, 28 | "include": [ 29 | "src/**/*.d.ts", 30 | "src/**/*.ts", 31 | "src/*.ts", 32 | "src/**/*.js", 33 | "src/**/*.svelte" 34 | ], 35 | "exclude": [ 36 | "node_modules/@types/node/**", 37 | "src/*/lib/api-client", 38 | ], 39 | "references": [ 40 | { 41 | "path": "./tsconfig.node.json" 42 | } 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /warpgate-web/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "esnext", 5 | "moduleResolution": "node" 6 | }, 7 | "include": ["vite.config.ts"] 8 | } 9 | -------------------------------------------------------------------------------- /warpgate-web/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import { svelte } from '@sveltejs/vite-plugin-svelte' 3 | import tsconfigPaths from 'vite-tsconfig-paths' 4 | import { checker } from 'vite-plugin-checker' 5 | 6 | // https://vitejs.dev/config/ 7 | export default defineConfig({ 8 | plugins: [ 9 | svelte(), 10 | tsconfigPaths(), 11 | // checker({ typescript: true }), 12 | ], 13 | base: '/@warpgate', 14 | build: { 15 | sourcemap: true, 16 | manifest: true, 17 | commonjsOptions: { 18 | include: [ 19 | 'src/gateway/lib/api-client/dist/*.js', 20 | 'src/admin/lib/api-client/dist/*.js', 21 | '**/*.js', 22 | ], 23 | transformMixedEsModules: true, 24 | }, 25 | rollupOptions: { 26 | input: { 27 | admin: 'src/admin/index.html', 28 | gateway: 'src/gateway/index.html', 29 | embed: 'src/embed/index.ts', 30 | }, 31 | }, 32 | }, 33 | }) 34 | -------------------------------------------------------------------------------- /warpgate/.gitignore: -------------------------------------------------------------------------------- 1 | !Cargo.lock 2 | -------------------------------------------------------------------------------- /warpgate/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | edition = "2021" 3 | license = "Apache-2.0" 4 | name = "warpgate" 5 | version = "0.14.0" 6 | 7 | [dependencies] 8 | ansi_term = "0.12" 9 | anyhow = { version = "1.0", features = ["backtrace"] } 10 | async-trait = "0.1" 11 | bytes.workspace = true 12 | clap = { version = "4.0", features = ["derive"] } 13 | config = { version = "0.15", features = ["yaml"], default-features = false } 14 | console = { version = "0.15", default-features = false } 15 | console-subscriber = { version = "0.1", optional = true } 16 | data-encoding.workspace = true 17 | dialoguer = "0.10" 18 | enum_dispatch.workspace = true 19 | futures.workspace = true 20 | notify = "5.1" 21 | rcgen = { version = "0.13", features = ["zeroize"] } 22 | rustls.workspace = true 23 | serde_json.workspace = true 24 | serde_yaml = "0.9" 25 | sea-orm.workspace = true 26 | time = "0.3" 27 | tokio = { version = "1.20", features = ["tracing", "signal", "macros"] } 28 | tracing.workspace = true 29 | tracing-subscriber = { version = "0.3", features = [ 30 | "env-filter", 31 | "local-time", 32 | ] } 33 | uuid = "1.3" 34 | warpgate-admin = { version = "*", path = "../warpgate-admin" } 35 | warpgate-common = { version = "*", path = "../warpgate-common" } 36 | warpgate-core = { version = "*", path = "../warpgate-core" } 37 | warpgate-db-entities = { version = "*", path = "../warpgate-db-entities" } 38 | warpgate-protocol-http = { version = "*", path = "../warpgate-protocol-http" } 39 | warpgate-protocol-mysql = { version = "*", path = "../warpgate-protocol-mysql" } 40 | warpgate-protocol-postgres = { version = "*", path = "../warpgate-protocol-postgres" } 41 | warpgate-protocol-ssh = { version = "*", path = "../warpgate-protocol-ssh" } 42 | 43 | [target.'cfg(target_os = "linux")'.dependencies] 44 | sd-notify = "0.4" 45 | 46 | [features] 47 | default = ["sqlite"] 48 | tokio-console = ["dep:console-subscriber", "tokio/tracing"] 49 | postgres = ["warpgate-core/postgres"] 50 | mysql = ["warpgate-core/mysql"] 51 | sqlite = ["warpgate-core/sqlite"] 52 | -------------------------------------------------------------------------------- /warpgate/src/commands/check.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use tracing::*; 3 | use warpgate_common::{TlsCertificateBundle, TlsPrivateKey}; 4 | 5 | use crate::config::load_config; 6 | 7 | pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { 8 | let config = load_config(&cli.config, true)?; 9 | if config.store.http.enable { 10 | TlsCertificateBundle::from_file( 11 | config 12 | .paths_relative_to 13 | .join(&config.store.http.certificate), 14 | ) 15 | .await 16 | .with_context(|| "Checking HTTPS certificate".to_string())?; 17 | TlsPrivateKey::from_file(config.paths_relative_to.join(&config.store.http.key)) 18 | .await 19 | .with_context(|| "Checking HTTPS key".to_string())?; 20 | } 21 | if config.store.mysql.enable { 22 | TlsCertificateBundle::from_file( 23 | config 24 | .paths_relative_to 25 | .join(&config.store.mysql.certificate), 26 | ) 27 | .await 28 | .with_context(|| "Checking MySQL certificate".to_string())?; 29 | TlsPrivateKey::from_file(config.paths_relative_to.join(&config.store.mysql.key)) 30 | .await 31 | .with_context(|| "Checking MySQL key".to_string())?; 32 | } 33 | if config.store.postgres.enable { 34 | TlsCertificateBundle::from_file( 35 | config 36 | .paths_relative_to 37 | .join(&config.store.postgres.certificate), 38 | ) 39 | .await 40 | .with_context(|| "Checking PostgreSQL certificate".to_string())?; 41 | TlsPrivateKey::from_file(config.paths_relative_to.join(&config.store.postgres.key)) 42 | .await 43 | .with_context(|| "Checking PostgreSQL key".to_string())?; 44 | } 45 | info!("No problems found"); 46 | Ok(()) 47 | } 48 | -------------------------------------------------------------------------------- /warpgate/src/commands/client_keys.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | 3 | use crate::config::load_config; 4 | 5 | pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { 6 | let config = load_config(&cli.config, true)?; 7 | let keys = warpgate_protocol_ssh::load_client_keys(&config)?; 8 | println!("Warpgate SSH client keys:"); 9 | println!("(add these to your target's authorized_keys file)"); 10 | println!(); 11 | for key in keys { 12 | println!("{}", key.public_key().to_openssh()?); 13 | } 14 | Ok(()) 15 | } 16 | -------------------------------------------------------------------------------- /warpgate/src/commands/common.rs: -------------------------------------------------------------------------------- 1 | use std::io::IsTerminal; 2 | 3 | use tracing::*; 4 | 5 | pub(crate) fn assert_interactive_terminal() { 6 | if !std::io::stdin().is_terminal() { 7 | error!("Please run this command from an interactive terminal."); 8 | if is_docker() { 9 | info!("(have you forgotten `-it`?)"); 10 | } 11 | std::process::exit(1); 12 | } 13 | } 14 | 15 | pub(crate) fn is_docker() -> bool { 16 | std::env::var("DOCKER").is_ok() 17 | } 18 | -------------------------------------------------------------------------------- /warpgate/src/commands/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod check; 2 | pub mod client_keys; 3 | mod common; 4 | pub mod recover_access; 5 | pub mod run; 6 | pub mod setup; 7 | pub mod test_target; 8 | -------------------------------------------------------------------------------- /warpgate/src/protocols.rs: -------------------------------------------------------------------------------- 1 | use enum_dispatch::enum_dispatch; 2 | use warpgate_common::ListenEndpoint; 3 | use warpgate_core::{ProtocolServer, TargetTestError}; 4 | use warpgate_protocol_http::HTTPProtocolServer; 5 | use warpgate_protocol_mysql::MySQLProtocolServer; 6 | use warpgate_protocol_postgres::PostgresProtocolServer; 7 | use warpgate_protocol_ssh::SSHProtocolServer; 8 | 9 | #[enum_dispatch(ProtocolServer)] 10 | #[allow(clippy::enum_variant_names)] 11 | pub enum ProtocolServerEnum { 12 | SSHProtocolServer, 13 | HTTPProtocolServer, 14 | MySQLProtocolServer, 15 | PostgresProtocolServer, 16 | } 17 | 18 | impl ProtocolServer for ProtocolServerEnum { 19 | async fn run(self, address: ListenEndpoint) -> anyhow::Result<()> { 20 | match self { 21 | ProtocolServerEnum::SSHProtocolServer(s) => s.run(address).await, 22 | ProtocolServerEnum::HTTPProtocolServer(s) => s.run(address).await, 23 | ProtocolServerEnum::MySQLProtocolServer(s) => s.run(address).await, 24 | ProtocolServerEnum::PostgresProtocolServer(s) => s.run(address).await, 25 | } 26 | } 27 | 28 | async fn test_target( 29 | &self, 30 | target: warpgate_common::Target, 31 | ) -> anyhow::Result<(), TargetTestError> { 32 | match self { 33 | ProtocolServerEnum::SSHProtocolServer(s) => s.test_target(target).await, 34 | ProtocolServerEnum::HTTPProtocolServer(s) => s.test_target(target).await, 35 | ProtocolServerEnum::MySQLProtocolServer(s) => s.test_target(target).await, 36 | ProtocolServerEnum::PostgresProtocolServer(s) => s.test_target(target).await, 37 | } 38 | } 39 | } 40 | --------------------------------------------------------------------------------