mirror of
https://github.com/warp-tech/warpgate.git
synced 2024-09-20 06:46:17 +08:00
added e2e tests
This commit is contained in:
parent
1a9bd89b44
commit
c5cf5bf1d1
7
.flake8
Normal file
7
.flake8
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
[flake8]
|
||||||
|
ignore=E501,D103,C901,D203,W504,S607,S603,S404,S606,S322,S410,S320,B010
|
||||||
|
exclude = .git,__pycache__,help,static,misc,locale,templates,tests,deployment,migrations,elements/ai/scripts
|
||||||
|
max-complexity = 40
|
||||||
|
builtins = _
|
||||||
|
per-file-ignores = scripts/*:T001,E402
|
||||||
|
select = C,E,F,W,B,B902
|
55
.github/workflows/test.yml
vendored
Normal file
55
.github/workflows/test.yml
vendored
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
name: Test
|
||||||
|
|
||||||
|
on: [push, pull_request]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
Tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Cache
|
||||||
|
uses: actions/cache@v2
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/.cargo/registry
|
||||||
|
~/.cargo/git
|
||||||
|
target
|
||||||
|
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
|
- name: Install build deps
|
||||||
|
run: |
|
||||||
|
sudo apt-get install openssh-client expect
|
||||||
|
cargo install just
|
||||||
|
cargo install cargo-llvm-cov
|
||||||
|
cargo clean
|
||||||
|
rustup component add llvm-tools-preview
|
||||||
|
|
||||||
|
- name: Build admin UI
|
||||||
|
run: |
|
||||||
|
just yarn
|
||||||
|
just openapi
|
||||||
|
just yarn build
|
||||||
|
|
||||||
|
- name: Build images
|
||||||
|
working-directory: tests
|
||||||
|
run: |
|
||||||
|
make all
|
||||||
|
|
||||||
|
- name: Install deps
|
||||||
|
working-directory: tests
|
||||||
|
run: |
|
||||||
|
pip3 install poetry
|
||||||
|
poetry install
|
||||||
|
|
||||||
|
- name: Run
|
||||||
|
working-directory: tests
|
||||||
|
run: |
|
||||||
|
poetry run ./run.sh
|
||||||
|
cargo llvm-cov --no-run --hide-instantiations --lcov > coverage.lcov
|
||||||
|
|
||||||
|
- name: SonarCloud Scan
|
||||||
|
uses: SonarSource/sonarcloud-github-action@master
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any
|
||||||
|
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -18,3 +18,5 @@ host_key*
|
||||||
data
|
data
|
||||||
config.*.yaml
|
config.*.yaml
|
||||||
config.yaml
|
config.yaml
|
||||||
|
__pycache__
|
||||||
|
.pytest_cache
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
cargo-features = ["profile-rustflags"]
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
"warpgate",
|
"warpgate",
|
||||||
|
@ -18,3 +20,7 @@ default-members = ["warpgate"]
|
||||||
lto = true
|
lto = true
|
||||||
panic = "abort"
|
panic = "abort"
|
||||||
strip = "debuginfo"
|
strip = "debuginfo"
|
||||||
|
|
||||||
|
[profile.coverage]
|
||||||
|
inherits = "dev"
|
||||||
|
rustflags = ["-Cinstrument-coverage"]
|
||||||
|
|
7
sonar-project.properties
Normal file
7
sonar-project.properties
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
sonar.projectKey=warp-tech_warpgate
|
||||||
|
sonar.organization=warp-tech
|
||||||
|
|
||||||
|
sonar.sources=.
|
||||||
|
sonar.inclusions=warpgate-*/**/*
|
||||||
|
|
||||||
|
sonar.cfamily.llvm-cov.reportPath=coverage.lcov
|
7
tests/Makefile
Normal file
7
tests/Makefile
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
image-ssh-server:
|
||||||
|
cd images/ssh-server && docker build -t warpgate-e2e-ssh-server .
|
||||||
|
|
||||||
|
image-mysql-server:
|
||||||
|
cd images/mysql-server && docker build -t warpgate-e2e-mysql-server .
|
||||||
|
|
||||||
|
all: image-ssh-server image-mysql-server
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
10
tests/certs/tls.certificate.pem
Normal file
10
tests/certs/tls.certificate.pem
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBYjCCAQmgAwIBAgIJAKXIp8GepnCzMAoGCCqGSM49BAMCMCExHzAdBgNVBAMM
|
||||||
|
FnJjZ2VuIHNlbGYgc2lnbmVkIGNlcnQwIBcNNzUwMTAxMDAwMDAwWhgPNDA5NjAx
|
||||||
|
MDEwMDAwMDBaMCExHzAdBgNVBAMMFnJjZ2VuIHNlbGYgc2lnbmVkIGNlcnQwWTAT
|
||||||
|
BgcqhkjOPQIBBggqhkjOPQMBBwNCAARAtRfTqyH8+eXf12Vftm6VcMhhYG6Ape3O
|
||||||
|
tcLfIWJo1krsOP+96r5U20ya7YVVFmYFPoQToAOoio2dxlX3jOL/oygwJjAkBgNV
|
||||||
|
HREEHTAbgg53YXJwZ2F0ZS5sb2NhbIIJbG9jYWxob3N0MAoGCCqGSM49BAMCA0cA
|
||||||
|
MEQCICTt3I/PsgF8Rvu6aKwY2LTouZyxReDMiCePzsqdAxXAAiATNw61MBylNaAF
|
||||||
|
FGkPqR0VZIR6sIFHZnib9JQNhka2Fg==
|
||||||
|
-----END CERTIFICATE-----
|
5
tests/certs/tls.key.pem
Normal file
5
tests/certs/tls.key.pem
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg0tJmr/OSF7neTQOV
|
||||||
|
gQn+qHCdVsOENdMc86RlWPiWDlKhRANCAARAtRfTqyH8+eXf12Vftm6VcMhhYG6A
|
||||||
|
pe3OtcLfIWJo1krsOP+96r5U20ya7YVVFmYFPoQToAOoio2dxlX3jOL/
|
||||||
|
-----END PRIVATE KEY-----
|
273
tests/conftest.py
Normal file
273
tests/conftest.py
Normal file
|
@ -0,0 +1,273 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import psutil
|
||||||
|
import pytest
|
||||||
|
import requests
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import urllib3
|
||||||
|
import uuid
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
from textwrap import dedent
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from .util import alloc_port
|
||||||
|
from .test_http_common import http_common_wg_port, echo_server_port # noqa
|
||||||
|
|
||||||
|
|
||||||
|
cargo_root = Path(os.getcwd()).parent
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Context:
|
||||||
|
tmpdir: Path
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Child:
|
||||||
|
process: subprocess.Popen
|
||||||
|
stop_signal: signal.Signals
|
||||||
|
stop_timeout: float
|
||||||
|
|
||||||
|
|
||||||
|
class ProcessManager:
|
||||||
|
children: List[Child]
|
||||||
|
|
||||||
|
def __init__(self, ctx: Context) -> None:
|
||||||
|
self.children = []
|
||||||
|
self.ctx = ctx
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
for child in self.children:
|
||||||
|
try:
|
||||||
|
p = psutil.Process(child.process.pid)
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
continue
|
||||||
|
|
||||||
|
p.send_signal(child.stop_signal)
|
||||||
|
|
||||||
|
for sp in p.children(recursive=True):
|
||||||
|
try:
|
||||||
|
sp.terminate()
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
p.wait(timeout=child.stop_timeout)
|
||||||
|
except psutil.TimeoutExpired:
|
||||||
|
for sp in p.children(recursive=True):
|
||||||
|
try:
|
||||||
|
sp.kill()
|
||||||
|
except psutil.NoSuchProcess:
|
||||||
|
pass
|
||||||
|
p.kill()
|
||||||
|
|
||||||
|
def start_ssh_server(self, trusted_keys=[]):
|
||||||
|
port = alloc_port()
|
||||||
|
data_dir = self.ctx.tmpdir / f'sshd-{uuid.uuid4()}'
|
||||||
|
data_dir.mkdir(parents=True)
|
||||||
|
authorized_keys_path = data_dir / 'authorized_keys'
|
||||||
|
authorized_keys_path.write_text('\n'.join(trusted_keys))
|
||||||
|
config_path = data_dir / 'sshd_config'
|
||||||
|
config_path.write_text(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
Port 22
|
||||||
|
AuthorizedKeysFile {authorized_keys_path}
|
||||||
|
AllowAgentForwarding yes
|
||||||
|
AllowTcpForwarding yes
|
||||||
|
GatewayPorts yes
|
||||||
|
X11Forwarding yes
|
||||||
|
UseDNS no
|
||||||
|
PermitTunnel yes
|
||||||
|
StrictModes no
|
||||||
|
PermitRootLogin yes
|
||||||
|
HostKey /ssh-keys/id_ed25519
|
||||||
|
Subsystem sftp /usr/lib/ssh/sftp-server
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
)
|
||||||
|
data_dir.chmod(0o700)
|
||||||
|
authorized_keys_path.chmod(0o600)
|
||||||
|
config_path.chmod(0o600)
|
||||||
|
|
||||||
|
self.start(
|
||||||
|
[
|
||||||
|
'docker',
|
||||||
|
'run',
|
||||||
|
'--rm',
|
||||||
|
'-p',
|
||||||
|
f'{port}:22',
|
||||||
|
'-v',
|
||||||
|
f'{data_dir}:{data_dir}',
|
||||||
|
'-v',
|
||||||
|
f'{os.getcwd()}/ssh-keys:/ssh-keys',
|
||||||
|
'warpgate-e2e-ssh-server',
|
||||||
|
'-f',
|
||||||
|
str(config_path),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return port
|
||||||
|
|
||||||
|
def start_mysql_server(self):
|
||||||
|
port = alloc_port()
|
||||||
|
self.start(
|
||||||
|
[
|
||||||
|
'docker',
|
||||||
|
'run',
|
||||||
|
'--rm',
|
||||||
|
'-p',
|
||||||
|
f'{port}:3306',
|
||||||
|
'warpgate-e2e-mysql-server'
|
||||||
|
]
|
||||||
|
)
|
||||||
|
return port
|
||||||
|
|
||||||
|
def start_wg(self, config='', args=None):
|
||||||
|
ssh_port = alloc_port()
|
||||||
|
http_port = alloc_port()
|
||||||
|
mysql_port = alloc_port()
|
||||||
|
data_dir = self.ctx.tmpdir / f'wg-data-{uuid.uuid4()}'
|
||||||
|
data_dir.mkdir(parents=True)
|
||||||
|
keys_dir = data_dir / 'keys'
|
||||||
|
keys_dir.mkdir(parents=True)
|
||||||
|
for k in [
|
||||||
|
Path('ssh-keys/wg/client-ed25519'),
|
||||||
|
Path('ssh-keys/wg/client-rsa'),
|
||||||
|
Path('ssh-keys/wg/host-ed25519'),
|
||||||
|
Path('ssh-keys/wg/host-rsa'),
|
||||||
|
Path('certs/tls.certificate.pem'),
|
||||||
|
Path('certs/tls.key.pem'),
|
||||||
|
]:
|
||||||
|
shutil.copy(k, keys_dir / k.name)
|
||||||
|
config_path = data_dir / 'warpgate.yaml'
|
||||||
|
config_path.write_text(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
ssh:
|
||||||
|
enable: true
|
||||||
|
listen: 0.0.0.0:{ssh_port}
|
||||||
|
keys: {keys_dir}
|
||||||
|
host_key_verification: auto_accept
|
||||||
|
http:
|
||||||
|
enable: true
|
||||||
|
listen: 0.0.0.0:{http_port}
|
||||||
|
certificate: {keys_dir}/tls.certificate.pem
|
||||||
|
key: {keys_dir}/tls.key.pem
|
||||||
|
mysql:
|
||||||
|
enable: true
|
||||||
|
listen: 0.0.0.0:{mysql_port}
|
||||||
|
certificate: {keys_dir}/tls.certificate.pem
|
||||||
|
key: {keys_dir}/tls.key.pem
|
||||||
|
recordings:
|
||||||
|
enable: false
|
||||||
|
roles:
|
||||||
|
- name: role
|
||||||
|
- name: admin
|
||||||
|
'''
|
||||||
|
) + config
|
||||||
|
)
|
||||||
|
args = args or ['run']
|
||||||
|
p = self.start(
|
||||||
|
[
|
||||||
|
f'{cargo_root}/target/llvm-cov-target/debug/warpgate',
|
||||||
|
'--config',
|
||||||
|
str(config_path),
|
||||||
|
*args,
|
||||||
|
],
|
||||||
|
cwd=cargo_root,
|
||||||
|
env={
|
||||||
|
**os.environ,
|
||||||
|
'LLVM_PROFILE_FILE': f'{cargo_root}/target/llvm-cov-target/warpgate-%m.profraw',
|
||||||
|
},
|
||||||
|
stop_signal=signal.SIGINT,
|
||||||
|
stop_timeout=5,
|
||||||
|
)
|
||||||
|
return p, {
|
||||||
|
'ssh': ssh_port,
|
||||||
|
'http': http_port,
|
||||||
|
'mysql': mysql_port,
|
||||||
|
}
|
||||||
|
|
||||||
|
def start_ssh_client(self, *args, password=None, **kwargs):
|
||||||
|
preargs = []
|
||||||
|
if password:
|
||||||
|
preargs = ['sshpass', '-p', password]
|
||||||
|
p = self.start(
|
||||||
|
[
|
||||||
|
*preargs,
|
||||||
|
'ssh',
|
||||||
|
# '-v',
|
||||||
|
'-o',
|
||||||
|
'IdentitiesOnly=yes',
|
||||||
|
'-o',
|
||||||
|
'StrictHostKeychecking=no',
|
||||||
|
'-o',
|
||||||
|
'UserKnownHostsFile=/dev/null',
|
||||||
|
*args,
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
return p
|
||||||
|
|
||||||
|
def start(self, args, stop_timeout=3, stop_signal=signal.SIGTERM, **kwargs):
|
||||||
|
p = subprocess.Popen(args, **kwargs)
|
||||||
|
self.children.append(Child(process=p, stop_signal=stop_signal, stop_timeout=stop_timeout))
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def ctx():
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
ctx = Context(tmpdir=Path(tmpdir))
|
||||||
|
yield ctx
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def processes(ctx, report_generation):
|
||||||
|
mgr = ProcessManager(ctx)
|
||||||
|
try:
|
||||||
|
yield mgr
|
||||||
|
finally:
|
||||||
|
mgr.stop()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session', autouse=True)
|
||||||
|
def report_generation():
|
||||||
|
# subprocess.call(['cargo', 'llvm-cov', 'clean', '--workspace'])
|
||||||
|
subprocess.check_call(['cargo', 'llvm-cov', 'run', '--no-report', '--', '--version'], cwd=cargo_root)
|
||||||
|
yield
|
||||||
|
# subprocess.check_call(['cargo', 'llvm-cov', '--no-run', '--hide-instantiations', '--html'], cwd=cargo_root)
|
||||||
|
|
||||||
|
|
||||||
|
# ----
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def wg_c_ed25519_pubkey():
|
||||||
|
return Path(os.getcwd()) / 'ssh-keys/wg/client-ed25519.pub'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def otp_key_base64():
|
||||||
|
return 'Isj0ekwF1YsKW8VUUQiU4awp/9dMnyMcTPH9rlr1OsE='
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def otp_key_base32():
|
||||||
|
return 'ELEPI6SMAXKYWCS3YVKFCCEU4GWCT76XJSPSGHCM6H624WXVHLAQ'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def password_123_hash():
|
||||||
|
return '$argon2id$v=19$m=4096,t=3,p=1$cxT6YKZS7r3uBT4nPJXEJQ$GhjTXyGi5vD2H/0X8D3VgJCZSXM4I8GiXRzl4k5ytk0'
|
||||||
|
|
||||||
|
|
||||||
|
logging.basicConfig(level=logging.DEBUG)
|
||||||
|
requests.packages.urllib3.disable_warnings()
|
||||||
|
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||||
|
subprocess.call('chmod 600 ssh-keys/id*', shell=True)
|
6
tests/images/mysql-server/Dockerfile
Normal file
6
tests/images/mysql-server/Dockerfile
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
FROM mariadb:10.8
|
||||||
|
|
||||||
|
ENV MYSQL_DATABASE=db
|
||||||
|
ENV MYSQL_ROOT_PASSWORD=123
|
||||||
|
|
||||||
|
ADD init.sql /docker-entrypoint-initdb.d
|
4
tests/images/mysql-server/init.sql
Normal file
4
tests/images/mysql-server/init.sql
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
CREATE TABLE `db`.`table` (
|
||||||
|
`id` int(11) NOT NULL,
|
||||||
|
`name` varchar(1023) NOT NULL
|
||||||
|
) ENGINE=InnoDB;
|
4
tests/images/ssh-server/Dockerfile
Normal file
4
tests/images/ssh-server/Dockerfile
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
FROM alpine:3.14
|
||||||
|
RUN apk add openssh
|
||||||
|
RUN passwd -u root
|
||||||
|
ENTRYPOINT ["/usr/sbin/sshd", "-De"]
|
553
tests/poetry.lock
generated
Normal file
553
tests/poetry.lock
generated
Normal file
|
@ -0,0 +1,553 @@
|
||||||
|
[[package]]
|
||||||
|
name = "atomicwrites"
|
||||||
|
version = "1.4.1"
|
||||||
|
description = "Atomic file writes."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "21.4.0"
|
||||||
|
description = "Classes Without Boilerplate"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
|
||||||
|
docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
|
||||||
|
tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
|
||||||
|
tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "bcrypt"
|
||||||
|
version = "3.2.2"
|
||||||
|
description = "Modern password hashing for your software and your servers"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = ">=1.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
tests = ["pytest (>=3.2.1,!=3.3.0)"]
|
||||||
|
typecheck = ["mypy"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "certifi"
|
||||||
|
version = "2022.6.15"
|
||||||
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cffi"
|
||||||
|
version = "1.15.1"
|
||||||
|
description = "Foreign Function Interface for Python calling C code."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pycparser = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "charset-normalizer"
|
||||||
|
version = "2.1.0"
|
||||||
|
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
unicode_backport = ["unicodedata2"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.1.3"
|
||||||
|
description = "Composable command line interface toolkit"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.5"
|
||||||
|
description = "Cross-platform colored terminal text."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "cryptography"
|
||||||
|
version = "37.0.4"
|
||||||
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = ">=1.12"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
|
||||||
|
docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
|
||||||
|
pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
|
||||||
|
sdist = ["setuptools_rust (>=0.11.4)"]
|
||||||
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
|
test = ["pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flake8"
|
||||||
|
version = "5.0.2"
|
||||||
|
description = "the modular source code checker: pep8 pyflakes and co"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.1"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
importlib-metadata = {version = "<4.3", markers = "python_version < \"3.8\""}
|
||||||
|
mccabe = ">=0.7.0,<0.8.0"
|
||||||
|
pycodestyle = ">=2.9.0,<2.10.0"
|
||||||
|
pyflakes = ">=2.5.0,<2.6.0"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask"
|
||||||
|
version = "2.2.1"
|
||||||
|
description = "A simple framework for building complex web applications."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=8.0"
|
||||||
|
importlib-metadata = {version = ">=3.6.0", markers = "python_version < \"3.10\""}
|
||||||
|
itsdangerous = ">=2.0"
|
||||||
|
Jinja2 = ">=3.0"
|
||||||
|
Werkzeug = ">=2.2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
async = ["asgiref (>=3.2)"]
|
||||||
|
dotenv = ["python-dotenv"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "flask-sock"
|
||||||
|
version = "0.5.2"
|
||||||
|
description = "WebSocket support for Flask"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
flask = ">=2"
|
||||||
|
simple-websocket = ">=0.5.1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.13.0"
|
||||||
|
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = {version = "*", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "idna"
|
||||||
|
version = "3.3"
|
||||||
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "importlib-metadata"
|
||||||
|
version = "4.2.0"
|
||||||
|
description = "Read metadata from Python packages"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
|
||||||
|
zipp = ">=0.5"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
|
||||||
|
testing = ["pytest (>=4.6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pep517", "pyfakefs", "flufl.flake8", "pytest-black (>=0.3.7)", "pytest-mypy", "importlib-resources (>=1.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "1.1.1"
|
||||||
|
description = "iniconfig: brain-dead simple config-ini parsing"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "itsdangerous"
|
||||||
|
version = "2.1.2"
|
||||||
|
description = "Safely pass data to untrusted environments and back."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jinja2"
|
||||||
|
version = "3.1.2"
|
||||||
|
description = "A very fast and expressive template engine."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
MarkupSafe = ">=2.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
i18n = ["Babel (>=2.7)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "markupsafe"
|
||||||
|
version = "2.1.1"
|
||||||
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "mccabe"
|
||||||
|
version = "0.7.0"
|
||||||
|
description = "McCabe checker, plugin for flake8"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "21.3"
|
||||||
|
description = "Core utilities for Python packages"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "paramiko"
|
||||||
|
version = "2.11.0"
|
||||||
|
description = "SSH2 protocol library"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
bcrypt = ">=3.1.3"
|
||||||
|
cryptography = ">=2.5"
|
||||||
|
pynacl = ">=1.0.1"
|
||||||
|
six = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
all = ["pyasn1 (>=0.1.7)", "pynacl (>=1.0.1)", "bcrypt (>=3.1.3)", "invoke (>=1.3)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"]
|
||||||
|
ed25519 = ["pynacl (>=1.0.1)", "bcrypt (>=3.1.3)"]
|
||||||
|
gssapi = ["pyasn1 (>=0.1.7)", "gssapi (>=1.4.1)", "pywin32 (>=2.1.8)"]
|
||||||
|
invoke = ["invoke (>=1.3)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "plugin and hook calling mechanisms for python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["pytest-benchmark", "pytest"]
|
||||||
|
dev = ["tox", "pre-commit"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "psutil"
|
||||||
|
version = "5.9.1"
|
||||||
|
description = "Cross-platform lib for process and system monitoring in Python."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
test = ["ipaddress", "mock", "enum34", "pywin32", "wmi"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "py"
|
||||||
|
version = "1.11.0"
|
||||||
|
description = "library with cross-python path, ini-parsing, io, code, log facilities"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycodestyle"
|
||||||
|
version = "2.9.0"
|
||||||
|
description = "Python style guide checker"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pycparser"
|
||||||
|
version = "2.21"
|
||||||
|
description = "C parser in Python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyflakes"
|
||||||
|
version = "2.5.0"
|
||||||
|
description = "passive checker of Python programs"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pynacl"
|
||||||
|
version = "1.5.0"
|
||||||
|
description = "Python binding to the Networking and Cryptography (NaCl) library"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
cffi = ">=1.4.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"]
|
||||||
|
tests = ["pytest (>=3.2.1,!=3.3.0)", "hypothesis (>=3.27.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyotp"
|
||||||
|
version = "2.6.0"
|
||||||
|
description = "Python One Time Password Library"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyparsing"
|
||||||
|
version = "3.0.9"
|
||||||
|
description = "pyparsing module - Classes and methods to define and execute parsing grammars"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6.8"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
diagrams = ["railroad-diagrams", "jinja2"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "7.1.2"
|
||||||
|
description = "pytest: simple powerful testing with Python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
|
||||||
|
attrs = ">=19.2.0"
|
||||||
|
colorama = {version = "*", markers = "sys_platform == \"win32\""}
|
||||||
|
importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
|
||||||
|
iniconfig = "*"
|
||||||
|
packaging = "*"
|
||||||
|
pluggy = ">=0.12,<2.0"
|
||||||
|
py = ">=1.8.2"
|
||||||
|
tomli = ">=1.0.0"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "requests"
|
||||||
|
version = "2.28.1"
|
||||||
|
description = "Python HTTP for Humans."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7, <4"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = ">=2017.4.17"
|
||||||
|
charset-normalizer = ">=2,<3"
|
||||||
|
idna = ">=2.5,<4"
|
||||||
|
urllib3 = ">=1.21.1,<1.27"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7)"]
|
||||||
|
use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple-websocket"
|
||||||
|
version = "0.8.0"
|
||||||
|
description = "Simple WebSocket server and client for Python"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.6"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
wsproto = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "six"
|
||||||
|
version = "1.16.0"
|
||||||
|
description = "Python 2 and 3 compatibility utilities"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ssh2-python"
|
||||||
|
version = "1.0.0"
|
||||||
|
description = "Bindings for libssh2 C library"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.0.1"
|
||||||
|
description = "A lil' TOML parser"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.3.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.7+"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "urllib3"
|
||||||
|
version = "1.26.11"
|
||||||
|
description = "HTTP library with thread-safe connection pooling, file post, and more."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
|
||||||
|
secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
|
||||||
|
socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "websocket-client"
|
||||||
|
version = "1.3.3"
|
||||||
|
description = "WebSocket client for Python with low level API options"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["Sphinx (>=3.4)", "sphinx-rtd-theme (>=0.5)"]
|
||||||
|
optional = ["python-socks", "wsaccel"]
|
||||||
|
test = ["websockets"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "werkzeug"
|
||||||
|
version = "2.2.1"
|
||||||
|
description = "The comprehensive WSGI web application library."
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
MarkupSafe = ">=2.1.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
watchdog = ["watchdog"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "wsproto"
|
||||||
|
version = "1.1.0"
|
||||||
|
description = "WebSockets state-machine based protocol implementation"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7.0"
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
h11 = ">=0.9.0,<1"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "zipp"
|
||||||
|
version = "3.8.1"
|
||||||
|
description = "Backport of pathlib-compatible object wrapper for zip files"
|
||||||
|
category = "main"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
|
||||||
|
testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"]
|
||||||
|
|
||||||
|
[metadata]
|
||||||
|
lock-version = "1.1"
|
||||||
|
python-versions = "^3.7"
|
||||||
|
content-hash = "5f35fb40904521418ed8cf0d0c2bbe36011c91f7aaeb672603572d930ff42763"
|
||||||
|
|
||||||
|
[metadata.files]
|
||||||
|
atomicwrites = []
|
||||||
|
attrs = []
|
||||||
|
bcrypt = []
|
||||||
|
certifi = []
|
||||||
|
cffi = []
|
||||||
|
charset-normalizer = []
|
||||||
|
click = []
|
||||||
|
colorama = []
|
||||||
|
cryptography = []
|
||||||
|
flake8 = []
|
||||||
|
flask = []
|
||||||
|
flask-sock = []
|
||||||
|
h11 = []
|
||||||
|
idna = []
|
||||||
|
importlib-metadata = []
|
||||||
|
iniconfig = [
|
||||||
|
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||||
|
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||||
|
]
|
||||||
|
itsdangerous = []
|
||||||
|
jinja2 = []
|
||||||
|
markupsafe = []
|
||||||
|
mccabe = []
|
||||||
|
packaging = []
|
||||||
|
paramiko = []
|
||||||
|
pluggy = []
|
||||||
|
psutil = []
|
||||||
|
py = []
|
||||||
|
pycodestyle = []
|
||||||
|
pycparser = []
|
||||||
|
pyflakes = []
|
||||||
|
pynacl = []
|
||||||
|
pyotp = []
|
||||||
|
pyparsing = []
|
||||||
|
pytest = []
|
||||||
|
requests = []
|
||||||
|
simple-websocket = []
|
||||||
|
six = []
|
||||||
|
ssh2-python = []
|
||||||
|
tomli = []
|
||||||
|
typing-extensions = []
|
||||||
|
urllib3 = []
|
||||||
|
websocket-client = []
|
||||||
|
werkzeug = []
|
||||||
|
wsproto = []
|
||||||
|
zipp = []
|
29
tests/pyproject.toml
Normal file
29
tests/pyproject.toml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
[tool.poetry]
|
||||||
|
name = "tests"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = ""
|
||||||
|
authors = ["Your Name <you@example.com>"]
|
||||||
|
|
||||||
|
[tool.poetry.dependencies]
|
||||||
|
python = "^3.7"
|
||||||
|
pytest = "^7.1.2"
|
||||||
|
psutil = "^5.9.1"
|
||||||
|
pyotp = "^2.6.0"
|
||||||
|
paramiko = "^2.11.0"
|
||||||
|
Flask = "^2.2.1"
|
||||||
|
requests = "^2.28.1"
|
||||||
|
flask-sock = "^0.5.2"
|
||||||
|
websocket-client = "^1.3.3"
|
||||||
|
|
||||||
|
[tool.poetry.dev-dependencies]
|
||||||
|
flake8 = "^5.0.2"
|
||||||
|
pytest = "^7.1.2"
|
||||||
|
ssh2-python = "^1.0.0"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
minversion = "6.0"
|
||||||
|
filterwarnings = ["ignore::urllib3.exceptions.InsecureRequestWarning"]
|
||||||
|
|
||||||
|
[build-system]
|
||||||
|
requires = ["poetry-core>=1.0.0"]
|
||||||
|
build-backend = "poetry.core.masonry.api"
|
9
tests/run.sh
Executable file
9
tests/run.sh
Executable file
|
@ -0,0 +1,9 @@
|
||||||
|
#!/bin/sh
|
||||||
|
set -e
|
||||||
|
cd ..
|
||||||
|
rm target/llvm-cov-target/* || true
|
||||||
|
cargo llvm-cov clean --workspace
|
||||||
|
cargo llvm-cov --no-report --workspace
|
||||||
|
cd tests
|
||||||
|
poetry run pytest $@
|
||||||
|
cargo llvm-cov --no-run --hide-instantiations --html
|
7
tests/ssh-keys/id_ed25519
Normal file
7
tests/ssh-keys/id_ed25519
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACAz/wkEtOQGGLxHmd1+hD0hOJYri7j+8Oqex4CMnTr26gAAAKC8ieiIvIno
|
||||||
|
iAAAAAtzc2gtZWQyNTUxOQAAACAz/wkEtOQGGLxHmd1+hD0hOJYri7j+8Oqex4CMnTr26g
|
||||||
|
AAAEBe+1fXsb/WtCsxt6nR5fVqIX9WHQqbpiVxxNTy41IsFDP/CQS05AYYvEeZ3X6EPSE4
|
||||||
|
liuLuP7w6p7HgIydOvbqAAAAHGV1Z2VuZUBFdWdlbmVzLU1CUC5mcml0ei5ib3gB
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
1
tests/ssh-keys/id_ed25519.pub
Normal file
1
tests/ssh-keys/id_ed25519.pub
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDP/CQS05AYYvEeZ3X6EPSE4liuLuP7w6p7HgIydOvbq
|
38
tests/ssh-keys/id_rsa
Normal file
38
tests/ssh-keys/id_rsa
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
1
tests/ssh-keys/id_rsa.pub
Normal file
1
tests/ssh-keys/id_rsa.pub
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCd30zPwWdhmb+lcHUn7djHwgOxvy+6CAaYT/Elmb8fS+4IPi8D9oOzeB2pcuNTX+7IPQn8JA8fga82oCQnwCK8zZWGcWwbVAkuSWRcQd4EWVPzWbE7a3QMHhLpBBeBtgLte3JjqBWpFi4xP5ahqfMlxgy/eBIYWv9ifxadViXffOwPxi0uXcq7vX7mTKQUL/z8uXE5RjvdfNM9W4KERO/PsqEquUGIeE5DCPLHYHUl0ZCnmFKsFNKY7f4iYgywKWxDDd8/S9zuy3svTp44E1OuE86JCqPKfkygJ+ghEC7q4TugfKuwt9iJmslbJLP5N1lk150p9DFYNcFwrn7SnxxjJL9T7IYdGtKNIe2X6qpLxBgJN2W15H8QNK487vITre1Gk5GDovmSq4C6mt4fqu2A7QenP/pleP1uwBH4az4nozHOslZ4OIDFQCS+D1qdiCPeyQMUGx/KVV7Z57LV9C3HT+KIo2MXKumoc8BQjIldrlBOmWlk+Jwoylr5ffyrqB8=
|
7
tests/ssh-keys/wg/client-ed25519
Normal file
7
tests/ssh-keys/wg/client-ed25519
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
-----BEGIN OPENSSH PRIVATE KEY-----
|
||||||
|
b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW
|
||||||
|
QyNTUxOQAAACB8c0a+GzS7QQTMt4qvepvzMRZx9t5OvKj2o2HxmOVMAQAAAKAK02MACtNj
|
||||||
|
AAAAAAtzc2gtZWQyNTUxOQAAACB8c0a+GzS7QQTMt4qvepvzMRZx9t5OvKj2o2HxmOVMAQ
|
||||||
|
AAAEDuH0qssNc+ANqiQp0MsgyLECAPmDjkUeNpUbahI+AagnxzRr4bNLtBBMy3iq96m/Mx
|
||||||
|
FnH23k68qPajYfGY5UwBAAAAHGV1Z2VuZUBFdWdlbmVzLU1CUC5mcml0ei5ib3gB
|
||||||
|
-----END OPENSSH PRIVATE KEY-----
|
1
tests/ssh-keys/wg/client-ed25519.pub
Normal file
1
tests/ssh-keys/wg/client-ed25519.pub
Normal file
|
@ -0,0 +1 @@
|
||||||
|
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxzRr4bNLtBBMy3iq96m/MxFnH23k68qPajYfGY5UwB eugene@Eugenes-MBP.fritz.box
|
45
tests/ssh-keys/wg/client-rsa
Normal file
45
tests/ssh-keys/wg/client-rsa
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
|
||||||
|
-----END PRIVATE KEY-----
|
6
tests/ssh-keys/wg/host-ed25519
Normal file
6
tests/ssh-keys/wg/host-ed25519
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MHMCAQEwBQYDK2VwBEIEQBHw4cXPpGgA+KdvPF5gxrzML+oa3yQk0JzIbWvmqM5H30RyBF8GrOWz
|
||||||
|
p77UAd3O4PgYzzFcUc79g8yKtbKhzJGhIwMhAN9EcgRfBqzls6e+1AHdzuD4GM8xXFHO/YPMirWy
|
||||||
|
ocyR
|
||||||
|
|
||||||
|
-----END PRIVATE KEY-----
|
45
tests/ssh-keys/wg/host-rsa
Normal file
45
tests/ssh-keys/wg/host-rsa
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
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
|
||||||
|
|
||||||
|
-----END PRIVATE KEY-----
|
46
tests/test_http_basic.py
Normal file
46
tests/test_http_basic.py
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test_basic(
|
||||||
|
self,
|
||||||
|
http_common_wg_port,
|
||||||
|
):
|
||||||
|
wait_port(http_common_wg_port, recv=False)
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
url = f'https://localhost:{http_common_wg_port}'
|
||||||
|
|
||||||
|
response = session.get(f'{url}/?warpgate-target=echo', allow_redirects=False)
|
||||||
|
assert response.status_code == 307
|
||||||
|
redirect = response.headers['location']
|
||||||
|
assert redirect == '/@warpgate#/login?next=%2F%3Fwarpgate%2Dtarget%3Decho'
|
||||||
|
|
||||||
|
response = session.get(f'{url}/@warpgate/api/info').json()
|
||||||
|
assert response['username'] is None
|
||||||
|
|
||||||
|
response = session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/login',
|
||||||
|
json={
|
||||||
|
'username': 'user',
|
||||||
|
'password': '123',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
|
||||||
|
response = session.get(f'{url}/@warpgate/api/info').json()
|
||||||
|
assert response['username'] == 'user'
|
||||||
|
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?a=b&warpgate-target=echo&c=d', allow_redirects=False
|
||||||
|
)
|
||||||
|
print(response)
|
||||||
|
print(response.text)
|
||||||
|
assert response.status_code == 200
|
||||||
|
assert response.json()['method'] == 'GET'
|
||||||
|
assert response.json()['path'] == '/some/path'
|
||||||
|
assert response.json()['args']['a'] == 'b'
|
||||||
|
assert response.json()['args']['c'] == 'd'
|
91
tests/test_http_common.py
Normal file
91
tests/test_http_common.py
Normal file
|
@ -0,0 +1,91 @@
|
||||||
|
import pytest
|
||||||
|
import threading
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from .util import alloc_port
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def http_common_wg_port(processes, echo_server_port, password_123_hash, otp_key_base64):
|
||||||
|
_, wg_ports = processes.start_wg(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
targets:
|
||||||
|
- name: echo
|
||||||
|
allow_roles: [role]
|
||||||
|
http:
|
||||||
|
url: http://localhost:{echo_server_port}
|
||||||
|
- name: baddomain
|
||||||
|
allow_roles: [role]
|
||||||
|
http:
|
||||||
|
url: http://localhostfoobar
|
||||||
|
- name: admin
|
||||||
|
allow_roles: [admin]
|
||||||
|
web_admin: {{}}
|
||||||
|
users:
|
||||||
|
- username: admin
|
||||||
|
roles: [admin]
|
||||||
|
credentials:
|
||||||
|
- type: password
|
||||||
|
hash: '{password_123_hash}'
|
||||||
|
- username: user
|
||||||
|
roles: [role]
|
||||||
|
credentials:
|
||||||
|
- type: password
|
||||||
|
hash: '{password_123_hash}'
|
||||||
|
- username: userwithotp
|
||||||
|
roles: [role]
|
||||||
|
credentials:
|
||||||
|
- type: password
|
||||||
|
hash: '{password_123_hash}'
|
||||||
|
- type: otp
|
||||||
|
key: {otp_key_base64}
|
||||||
|
require:
|
||||||
|
http: [password, otp]
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
)
|
||||||
|
yield wg_ports['http']
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='session')
|
||||||
|
def echo_server_port():
|
||||||
|
from flask import Flask, request, jsonify, redirect
|
||||||
|
from flask_sock import Sock
|
||||||
|
app = Flask(__name__)
|
||||||
|
sock = Sock(app)
|
||||||
|
|
||||||
|
@app.route('/set-cookie')
|
||||||
|
def set_cookie():
|
||||||
|
response = jsonify({})
|
||||||
|
response.set_cookie('cookie', 'value')
|
||||||
|
return response
|
||||||
|
|
||||||
|
@app.route('/redirect/<path:url>')
|
||||||
|
def r(url):
|
||||||
|
return redirect(url)
|
||||||
|
|
||||||
|
@app.route('/', defaults={'path': ''})
|
||||||
|
@app.route('/<path:path>')
|
||||||
|
def echo(path):
|
||||||
|
return jsonify({
|
||||||
|
'method': request.method,
|
||||||
|
'args': request.args,
|
||||||
|
'path': request.path,
|
||||||
|
})
|
||||||
|
|
||||||
|
@sock.route('/socket')
|
||||||
|
def ws_echo(ws):
|
||||||
|
while True:
|
||||||
|
data = ws.receive()
|
||||||
|
ws.send(data)
|
||||||
|
|
||||||
|
port = alloc_port()
|
||||||
|
|
||||||
|
def runner():
|
||||||
|
app.run(port=port, load_dotenv=False)
|
||||||
|
|
||||||
|
thread = threading.Thread(target=runner, daemon=True)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
yield port
|
41
tests/test_http_conntest.py
Normal file
41
tests/test_http_conntest.py
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test_success(
|
||||||
|
self,
|
||||||
|
processes,
|
||||||
|
echo_server_port,
|
||||||
|
):
|
||||||
|
proc, _ = processes.start_wg(
|
||||||
|
config=dedent(
|
||||||
|
f'''\
|
||||||
|
users: []
|
||||||
|
targets:
|
||||||
|
- name: target
|
||||||
|
allow_roles: [role]
|
||||||
|
http:
|
||||||
|
url: http://localhost:{echo_server_port}
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
args=['test-target', 'target'],
|
||||||
|
)
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
assert proc.returncode == 0
|
||||||
|
|
||||||
|
def test_fail_no_connection(self, processes):
|
||||||
|
proc, _ = processes.start_wg(
|
||||||
|
config=dedent(
|
||||||
|
'''\
|
||||||
|
users: []
|
||||||
|
targets:
|
||||||
|
- name: target
|
||||||
|
allow_roles: [role]
|
||||||
|
http:
|
||||||
|
url: http://localhostbaddomain
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
args=['test-target', 'target'],
|
||||||
|
)
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
assert proc.returncode != 0
|
31
tests/test_http_cookies.py
Normal file
31
tests/test_http_cookies.py
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class TestHTTPCookies:
|
||||||
|
def test(
|
||||||
|
self,
|
||||||
|
http_common_wg_port,
|
||||||
|
):
|
||||||
|
wait_port(http_common_wg_port, recv=False)
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
url = f'https://localhost:{http_common_wg_port}'
|
||||||
|
headers = {'Host': f'localhost:{http_common_wg_port}'}
|
||||||
|
|
||||||
|
session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/login',
|
||||||
|
json={
|
||||||
|
'username': 'user',
|
||||||
|
'password': '123',
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = session.get(f'{url}/set-cookie?warpgate-target=echo', headers=headers)
|
||||||
|
print(response.headers)
|
||||||
|
|
||||||
|
cookies = session.cookies.get_dict()
|
||||||
|
assert cookies['cookie'] == 'value'
|
30
tests/test_http_redirects.py
Normal file
30
tests/test_http_redirects.py
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class TestHTTPRedirects:
|
||||||
|
def test(
|
||||||
|
self,
|
||||||
|
http_common_wg_port,
|
||||||
|
echo_server_port,
|
||||||
|
):
|
||||||
|
wait_port(http_common_wg_port, recv=False)
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
url = f'https://localhost:{http_common_wg_port}'
|
||||||
|
headers = {'Host': f'localhost:{http_common_wg_port}'}
|
||||||
|
|
||||||
|
session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/login',
|
||||||
|
json={
|
||||||
|
'username': 'user',
|
||||||
|
'password': '123',
|
||||||
|
},
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
response = session.get(f'{url}/redirect/http://localhost:{echo_server_port}/test?warpgate-target=echo', headers=headers, allow_redirects=False)
|
||||||
|
print(response.headers)
|
||||||
|
|
||||||
|
assert response.headers['location'] == '/test'
|
35
tests/test_http_user_auth_logout.py
Normal file
35
tests/test_http_user_auth_logout.py
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test(
|
||||||
|
self,
|
||||||
|
http_common_wg_port,
|
||||||
|
):
|
||||||
|
wait_port(http_common_wg_port, recv=False)
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
url = f'https://localhost:{http_common_wg_port}'
|
||||||
|
|
||||||
|
response = session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/login',
|
||||||
|
json={
|
||||||
|
'username': 'user',
|
||||||
|
'password': '123',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 == 2
|
||||||
|
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?a=b&warpgate-target=echo&c=d', allow_redirects=False
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 == 2
|
||||||
|
assert response.json()['path'] == '/some/path'
|
||||||
|
|
||||||
|
response = session.post(f'{url}/@warpgate/api/auth/logout')
|
||||||
|
|
||||||
|
response = session.get(f'{url}/?warpgate-target=echo', allow_redirects=False)
|
||||||
|
assert response.status_code // 100 != 2
|
78
tests/test_http_user_auth_otp.py
Normal file
78
tests/test_http_user_auth_otp.py
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import requests
|
||||||
|
import pyotp
|
||||||
|
|
||||||
|
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class TestHTTPUserAuthOTP:
|
||||||
|
def test_auth_otp_success(
|
||||||
|
self,
|
||||||
|
http_common_wg_port,
|
||||||
|
otp_key_base32,
|
||||||
|
):
|
||||||
|
wait_port(http_common_wg_port, recv=False)
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
url = f'https://localhost:{http_common_wg_port}'
|
||||||
|
|
||||||
|
totp = pyotp.TOTP(otp_key_base32)
|
||||||
|
|
||||||
|
response = session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/login',
|
||||||
|
json={
|
||||||
|
'username': 'userwithotp',
|
||||||
|
'password': '123',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 != 2
|
||||||
|
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?a=b&warpgate-target=echo&c=d', allow_redirects=False
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 != 2
|
||||||
|
|
||||||
|
response = session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/otp',
|
||||||
|
json={
|
||||||
|
'otp': totp.now(),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 == 2
|
||||||
|
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?a=b&warpgate-target=echo&c=d', allow_redirects=False
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 == 2
|
||||||
|
assert response.json()['path'] == '/some/path'
|
||||||
|
|
||||||
|
def test_auth_otp_fail(
|
||||||
|
self,
|
||||||
|
http_common_wg_port,
|
||||||
|
):
|
||||||
|
wait_port(http_common_wg_port, recv=False)
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
url = f'https://localhost:{http_common_wg_port}'
|
||||||
|
|
||||||
|
response = session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/login',
|
||||||
|
json={
|
||||||
|
'username': 'userwithotp',
|
||||||
|
'password': '123',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 != 2
|
||||||
|
|
||||||
|
response = session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/otp',
|
||||||
|
json={
|
||||||
|
'otp': '00000000',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 != 2
|
||||||
|
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?a=b&warpgate-target=echo&c=d', allow_redirects=False
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 != 2
|
56
tests/test_http_user_auth_password.py
Normal file
56
tests/test_http_user_auth_password.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class TestHTTPUserAuthPassword:
|
||||||
|
def test_auth_password_success(
|
||||||
|
self,
|
||||||
|
http_common_wg_port,
|
||||||
|
):
|
||||||
|
wait_port(http_common_wg_port, recv=False)
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
url = f'https://localhost:{http_common_wg_port}'
|
||||||
|
|
||||||
|
response = session.get(f'{url}/?warpgate-target=echo', allow_redirects=False)
|
||||||
|
assert response.status_code // 100 != 2
|
||||||
|
|
||||||
|
response = session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/login',
|
||||||
|
json={
|
||||||
|
'username': 'user',
|
||||||
|
'password': '123',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 == 2
|
||||||
|
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?a=b&warpgate-target=echo&c=d', allow_redirects=False
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 == 2
|
||||||
|
assert response.json()['path'] == '/some/path'
|
||||||
|
|
||||||
|
def test_auth_password_fail(
|
||||||
|
self,
|
||||||
|
http_common_wg_port,
|
||||||
|
):
|
||||||
|
wait_port(http_common_wg_port, recv=False)
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
url = f'https://localhost:{http_common_wg_port}'
|
||||||
|
|
||||||
|
response = session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/login',
|
||||||
|
json={
|
||||||
|
'username': 'user',
|
||||||
|
'password': '321321',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 != 2
|
||||||
|
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?a=b&warpgate-target=echo&c=d', allow_redirects=False
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 != 2
|
56
tests/test_http_user_auth_ticket.py
Normal file
56
tests/test_http_user_auth_ticket.py
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import requests
|
||||||
|
|
||||||
|
from .util import create_ticket, wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class TestHTTPUserAuthTicket:
|
||||||
|
def test_auth_password_success(
|
||||||
|
self,
|
||||||
|
http_common_wg_port,
|
||||||
|
):
|
||||||
|
wait_port(http_common_wg_port, recv=False)
|
||||||
|
url = f'https://localhost:{http_common_wg_port}'
|
||||||
|
|
||||||
|
secret = create_ticket(url, 'user', 'echo')
|
||||||
|
|
||||||
|
# ---
|
||||||
|
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?warpgate-target=echo',
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 != 2
|
||||||
|
|
||||||
|
# Ticket as a header
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?warpgate-target=echo',
|
||||||
|
allow_redirects=False,
|
||||||
|
headers={
|
||||||
|
'Authorization': f'Warpgate {secret}',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 == 2
|
||||||
|
assert response.json()['path'] == '/some/path'
|
||||||
|
|
||||||
|
# Ticket as a GET param
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?warpgate-ticket={secret}',
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 == 2
|
||||||
|
assert response.json()['path'] == '/some/path'
|
||||||
|
|
||||||
|
# Ensure no access to other targets
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
response = session.get(
|
||||||
|
f'{url}/some/path?warpgate-ticket={secret}&warpgate-target=admin',
|
||||||
|
allow_redirects=False,
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 == 2
|
||||||
|
assert response.json()['path'] == '/some/path'
|
39
tests/test_http_websocket.py
Normal file
39
tests/test_http_websocket.py
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
import ssl
|
||||||
|
import requests
|
||||||
|
from websocket import create_connection
|
||||||
|
|
||||||
|
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class TestHTTPWebsocket:
|
||||||
|
def test_basic(
|
||||||
|
self,
|
||||||
|
http_common_wg_port,
|
||||||
|
):
|
||||||
|
wait_port(http_common_wg_port, recv=False)
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
url = f'https://localhost:{http_common_wg_port}'
|
||||||
|
|
||||||
|
session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/login',
|
||||||
|
json={
|
||||||
|
'username': 'user',
|
||||||
|
'password': '123',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
cookies = session.cookies.get_dict()
|
||||||
|
cookie = '; '.join([f'{k}={v}' for k, v in cookies.items()])
|
||||||
|
ws = create_connection(
|
||||||
|
f'wss://localhost:{http_common_wg_port}/socket?warpgate-target=echo',
|
||||||
|
cookie=cookie,
|
||||||
|
sslopt={"cert_reqs": ssl.CERT_NONE},
|
||||||
|
)
|
||||||
|
ws.send('test')
|
||||||
|
assert ws.recv() == 'test'
|
||||||
|
ws.send_binary(b'test')
|
||||||
|
assert ws.recv() == b'test'
|
||||||
|
ws.ping()
|
||||||
|
ws.close()
|
74
tests/test_mysql_user_auth_password.py
Normal file
74
tests/test_mysql_user_auth_password.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
import subprocess
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from .conftest import ProcessManager
|
||||||
|
from .util import wait_port, wait_mysql_port, mysql_client_ssl_opt
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test(self, processes: ProcessManager, password_123_hash):
|
||||||
|
db_port = processes.start_mysql_server()
|
||||||
|
|
||||||
|
_, wg_ports = processes.start_wg(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
targets:
|
||||||
|
- name: db
|
||||||
|
allow_roles: [role]
|
||||||
|
mysql:
|
||||||
|
host: localhost
|
||||||
|
port: {db_port}
|
||||||
|
user: root
|
||||||
|
password: '123'
|
||||||
|
users:
|
||||||
|
- username: user
|
||||||
|
roles: [role]
|
||||||
|
credentials:
|
||||||
|
- type: password
|
||||||
|
hash: '{password_123_hash}'
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
wait_mysql_port(db_port)
|
||||||
|
wait_port(wg_ports['mysql'])
|
||||||
|
|
||||||
|
client = processes.start(
|
||||||
|
[
|
||||||
|
'mysql',
|
||||||
|
'--user',
|
||||||
|
'user#db',
|
||||||
|
'-p123',
|
||||||
|
'--host',
|
||||||
|
'127.0.0.1',
|
||||||
|
'--port',
|
||||||
|
str(wg_ports["mysql"]),
|
||||||
|
'--enable-cleartext-plugin',
|
||||||
|
mysql_client_ssl_opt,
|
||||||
|
'db',
|
||||||
|
],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
assert b'\ndb\n' in client.communicate(b'show schemas;')[0]
|
||||||
|
assert client.returncode == 0
|
||||||
|
|
||||||
|
client = processes.start(
|
||||||
|
[
|
||||||
|
'mysql',
|
||||||
|
'--user',
|
||||||
|
'user:db',
|
||||||
|
'-pwrong',
|
||||||
|
'--host',
|
||||||
|
'127.0.0.1',
|
||||||
|
'--port',
|
||||||
|
str(wg_ports["mysql"]),
|
||||||
|
'--enable-cleartext-plugin',
|
||||||
|
mysql_client_ssl_opt,
|
||||||
|
'db',
|
||||||
|
],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
client.communicate(b'show schemas;')
|
||||||
|
assert client.returncode != 0
|
50
tests/test_ssh_conntest.py
Normal file
50
tests/test_ssh_conntest.py
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from .conftest import ProcessManager
|
||||||
|
from .util import alloc_port, wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test_success(
|
||||||
|
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path
|
||||||
|
):
|
||||||
|
ssh_port = processes.start_ssh_server(
|
||||||
|
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
|
||||||
|
)
|
||||||
|
wait_port(ssh_port)
|
||||||
|
proc, _ = processes.start_wg(
|
||||||
|
config=dedent(
|
||||||
|
f'''\
|
||||||
|
users: []
|
||||||
|
targets:
|
||||||
|
- name: ssh
|
||||||
|
allow_roles: [role]
|
||||||
|
ssh:
|
||||||
|
host: localhost
|
||||||
|
port: {ssh_port}
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
args=['test-target', 'ssh'],
|
||||||
|
)
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
assert proc.returncode == 0
|
||||||
|
|
||||||
|
def test_fail(self, processes: ProcessManager):
|
||||||
|
ssh_port = alloc_port()
|
||||||
|
proc, _ = processes.start_wg(
|
||||||
|
config=dedent(
|
||||||
|
f'''\
|
||||||
|
users: []
|
||||||
|
targets:
|
||||||
|
- name: ssh
|
||||||
|
allow_roles: [role]
|
||||||
|
ssh:
|
||||||
|
host: localhost
|
||||||
|
port: {ssh_port}
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
args=['test-target', 'ssh'],
|
||||||
|
)
|
||||||
|
proc.wait(timeout=5)
|
||||||
|
assert proc.returncode != 0
|
221
tests/test_ssh_proto.py
Normal file
221
tests/test_ssh_proto.py
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import pytest
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from .conftest import ProcessManager
|
||||||
|
from .util import wait_port, alloc_port
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='class')
|
||||||
|
def ssh_port(processes, wg_c_ed25519_pubkey):
|
||||||
|
yield processes.start_ssh_server(trusted_keys=[wg_c_ed25519_pubkey.read_text()])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope='class')
|
||||||
|
def wg_port(processes, ssh_port, password_123_hash):
|
||||||
|
_, wg_ports = processes.start_wg(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
targets:
|
||||||
|
- name: ssh
|
||||||
|
allow_roles: [role]
|
||||||
|
ssh:
|
||||||
|
host: localhost
|
||||||
|
port: {ssh_port}
|
||||||
|
- name: ssh-bad-domain
|
||||||
|
allow_roles: [role]
|
||||||
|
ssh:
|
||||||
|
host: baddomainsomething
|
||||||
|
users:
|
||||||
|
- username: user
|
||||||
|
roles: [role]
|
||||||
|
credentials:
|
||||||
|
- type: password
|
||||||
|
hash: '{password_123_hash}'
|
||||||
|
- type: publickey
|
||||||
|
key: {open('ssh-keys/id_rsa.pub').read().strip()}
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
)
|
||||||
|
wait_port(ssh_port)
|
||||||
|
wait_port(wg_ports['ssh'])
|
||||||
|
yield wg_ports['ssh']
|
||||||
|
|
||||||
|
|
||||||
|
common_args = [
|
||||||
|
'user:ssh@localhost',
|
||||||
|
'-i',
|
||||||
|
'/dev/null',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=password',
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test_stdout_stderr(
|
||||||
|
self,
|
||||||
|
processes: ProcessManager,
|
||||||
|
wg_port,
|
||||||
|
):
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'-p',
|
||||||
|
str(wg_port),
|
||||||
|
*common_args,
|
||||||
|
'sh',
|
||||||
|
'-c',
|
||||||
|
'"echo -n stdout; echo -n stderr >&2"',
|
||||||
|
password='123',
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout, stderr = ssh_client.communicate()
|
||||||
|
assert b'stdout' == stdout
|
||||||
|
assert stderr.endswith(b'stderr')
|
||||||
|
|
||||||
|
def test_pty(
|
||||||
|
self,
|
||||||
|
processes: ProcessManager,
|
||||||
|
wg_port,
|
||||||
|
):
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'-p',
|
||||||
|
str(wg_port),
|
||||||
|
'-tt',
|
||||||
|
*common_args,
|
||||||
|
'echo',
|
||||||
|
'hello',
|
||||||
|
password='123',
|
||||||
|
)
|
||||||
|
|
||||||
|
output = ssh_client.communicate()[0]
|
||||||
|
assert b'Warpgate' in output
|
||||||
|
assert b'Selected target:' in output
|
||||||
|
assert b'hello\r\n' in output
|
||||||
|
|
||||||
|
def test_signals(
|
||||||
|
self,
|
||||||
|
processes: ProcessManager,
|
||||||
|
wg_port,
|
||||||
|
):
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'-p',
|
||||||
|
str(wg_port),
|
||||||
|
'-v',
|
||||||
|
*common_args,
|
||||||
|
'sh', '-c',
|
||||||
|
'"pkill -9 sh"',
|
||||||
|
password='123',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ssh_client.returncode != 0
|
||||||
|
|
||||||
|
def test_direct_tcpip(
|
||||||
|
self,
|
||||||
|
processes: ProcessManager,
|
||||||
|
wg_port,
|
||||||
|
):
|
||||||
|
local_port = alloc_port()
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'-p',
|
||||||
|
str(wg_port),
|
||||||
|
'-v',
|
||||||
|
*common_args,
|
||||||
|
'-L', f'{local_port}:localhost:22',
|
||||||
|
'sleep', '15',
|
||||||
|
password='123',
|
||||||
|
)
|
||||||
|
|
||||||
|
data = wait_port(local_port)
|
||||||
|
assert b'SSH-2.0' in data
|
||||||
|
ssh_client.kill()
|
||||||
|
|
||||||
|
def test_shell(
|
||||||
|
self,
|
||||||
|
processes: ProcessManager,
|
||||||
|
wg_port,
|
||||||
|
):
|
||||||
|
script = dedent(
|
||||||
|
f'''
|
||||||
|
set timeout 10
|
||||||
|
|
||||||
|
spawn ssh -tt user:ssh@localhost -p {wg_port} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o PreferredAuthentications=password
|
||||||
|
|
||||||
|
expect "password:"
|
||||||
|
sleep 0.5
|
||||||
|
send "123\\r"
|
||||||
|
|
||||||
|
expect "#"
|
||||||
|
sleep 0.5
|
||||||
|
send "ls /bin/sh\\r"
|
||||||
|
send "exit\\r"
|
||||||
|
|
||||||
|
expect {{
|
||||||
|
"/bin/sh" {{ exit 0; }}
|
||||||
|
eof {{ exit 1; }}
|
||||||
|
}}
|
||||||
|
|
||||||
|
exit 1
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
ssh_client = processes.start(
|
||||||
|
['expect', '-d'], stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
output = ssh_client.communicate(script.encode())[0]
|
||||||
|
assert ssh_client.returncode == 0, output
|
||||||
|
|
||||||
|
def test_connection_error(
|
||||||
|
self,
|
||||||
|
processes: ProcessManager,
|
||||||
|
wg_port,
|
||||||
|
):
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'-p',
|
||||||
|
str(wg_port),
|
||||||
|
'-tt',
|
||||||
|
'user:ssh-bad-domain@localhost',
|
||||||
|
'-i',
|
||||||
|
'/dev/null',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=password',
|
||||||
|
'echo',
|
||||||
|
'hello',
|
||||||
|
password='123',
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
stdout = ssh_client.communicate()[0]
|
||||||
|
assert b'Selected target: ssh-bad-domain' in stdout
|
||||||
|
assert ssh_client.returncode != 0
|
||||||
|
|
||||||
|
def test_sftp(
|
||||||
|
self,
|
||||||
|
wg_port,
|
||||||
|
):
|
||||||
|
with tempfile.TemporaryDirectory() as f:
|
||||||
|
subprocess.check_call(
|
||||||
|
[
|
||||||
|
'sftp',
|
||||||
|
'-P',
|
||||||
|
str(wg_port),
|
||||||
|
'-o',
|
||||||
|
'User=user:ssh',
|
||||||
|
'-o',
|
||||||
|
'IdentitiesOnly=yes',
|
||||||
|
'-o',
|
||||||
|
'IdentityFile=ssh-keys/id_rsa',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=publickey',
|
||||||
|
'-o',
|
||||||
|
'StrictHostKeychecking=no',
|
||||||
|
'-o',
|
||||||
|
'UserKnownHostsFile=/dev/null',
|
||||||
|
'localhost:/etc/passwd',
|
||||||
|
f,
|
||||||
|
],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert 'root:x:0:0:root' in open(f + '/passwd').read()
|
52
tests/test_ssh_target_selection.py
Normal file
52
tests/test_ssh_target_selection.py
Normal file
|
@ -0,0 +1,52 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from .conftest import ProcessManager
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test_bad_target(
|
||||||
|
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path, password_123_hash
|
||||||
|
):
|
||||||
|
ssh_port = processes.start_ssh_server(
|
||||||
|
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
|
||||||
|
)
|
||||||
|
|
||||||
|
_, wg_ports = processes.start_wg(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
targets:
|
||||||
|
- name: ssh
|
||||||
|
allow_roles: [role]
|
||||||
|
ssh:
|
||||||
|
host: localhost
|
||||||
|
port: {ssh_port}
|
||||||
|
users:
|
||||||
|
- username: user
|
||||||
|
roles: [role]
|
||||||
|
credentials:
|
||||||
|
- type: password
|
||||||
|
hash: '{password_123_hash}'
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
wait_port(ssh_port)
|
||||||
|
wait_port(wg_ports['ssh'])
|
||||||
|
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'-t',
|
||||||
|
'user:badtarget@localhost',
|
||||||
|
'-p',
|
||||||
|
str(wg_ports['ssh']),
|
||||||
|
'-i',
|
||||||
|
'/dev/null',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=password',
|
||||||
|
'echo',
|
||||||
|
'hello',
|
||||||
|
password='123',
|
||||||
|
)
|
||||||
|
|
||||||
|
assert ssh_client.returncode != 0
|
97
tests/test_ssh_user_auth_otp.py
Normal file
97
tests/test_ssh_user_auth_otp.py
Normal file
|
@ -0,0 +1,97 @@
|
||||||
|
from asyncio import subprocess
|
||||||
|
import pyotp
|
||||||
|
from pathlib import Path
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from .conftest import ProcessManager
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test_otp(
|
||||||
|
self,
|
||||||
|
processes: ProcessManager,
|
||||||
|
wg_c_ed25519_pubkey: Path,
|
||||||
|
otp_key_base32: str,
|
||||||
|
otp_key_base64: str,
|
||||||
|
):
|
||||||
|
ssh_port = processes.start_ssh_server(
|
||||||
|
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
|
||||||
|
)
|
||||||
|
|
||||||
|
_, wg_ports = processes.start_wg(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
targets:
|
||||||
|
- name: ssh
|
||||||
|
allow_roles: [role]
|
||||||
|
ssh:
|
||||||
|
host: localhost
|
||||||
|
port: {ssh_port}
|
||||||
|
users:
|
||||||
|
- username: user
|
||||||
|
roles: [role]
|
||||||
|
credentials:
|
||||||
|
- type: publickey
|
||||||
|
key: {open('ssh-keys/id_ed25519.pub').read().strip()}
|
||||||
|
- type: otp
|
||||||
|
key: {otp_key_base64}
|
||||||
|
require:
|
||||||
|
ssh: [publickey, otp]
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
wait_port(ssh_port)
|
||||||
|
wait_port(wg_ports['ssh'])
|
||||||
|
|
||||||
|
totp = pyotp.TOTP(otp_key_base32)
|
||||||
|
|
||||||
|
script = dedent(
|
||||||
|
f'''
|
||||||
|
set timeout 10
|
||||||
|
|
||||||
|
spawn ssh user:ssh@localhost -p {wg_ports['ssh']} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile=ssh-keys/id_ed25519 -o PreferredAuthentications=publickey,keyboard-interactive ls /bin/sh
|
||||||
|
|
||||||
|
expect "Two-factor authentication"
|
||||||
|
sleep 0.5
|
||||||
|
send "{totp.now()}\\r"
|
||||||
|
|
||||||
|
expect {{
|
||||||
|
"/bin/sh" {{ exit 0; }}
|
||||||
|
eof {{ exit 1; }}
|
||||||
|
}}
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
ssh_client = processes.start(
|
||||||
|
['expect'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
output, stderr = ssh_client.communicate(script.encode())
|
||||||
|
assert ssh_client.returncode == 0, output + stderr
|
||||||
|
|
||||||
|
script = dedent(
|
||||||
|
f'''
|
||||||
|
set timeout 10
|
||||||
|
|
||||||
|
spawn ssh user:ssh@localhost -p {[wg_ports['ssh']]} -o StrictHostKeychecking=no -o UserKnownHostsFile=/dev/null -o IdentitiesOnly=yes -o IdentityFile=ssh-keys/id_ed25519 -o PreferredAuthentications=publickey,keyboard-interactive ls /bin/sh
|
||||||
|
|
||||||
|
expect "Two-factor authentication"
|
||||||
|
sleep 0.5
|
||||||
|
send "12345678\\r"
|
||||||
|
|
||||||
|
expect {{
|
||||||
|
"/bin/sh" {{ exit 0; }}
|
||||||
|
"Two-factor authentication" {{ exit 1; }}
|
||||||
|
eof {{ exit 1; }}
|
||||||
|
}}
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
|
||||||
|
ssh_client = processes.start(
|
||||||
|
['expect'], stdin=subprocess.PIPE, stdout=subprocess.PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
output = ssh_client.communicate(script.encode())[0]
|
||||||
|
assert ssh_client.returncode != 0, output
|
66
tests/test_ssh_user_auth_password.py
Normal file
66
tests/test_ssh_user_auth_password.py
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from .conftest import ProcessManager
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test(
|
||||||
|
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path, password_123_hash
|
||||||
|
):
|
||||||
|
ssh_port = processes.start_ssh_server(
|
||||||
|
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
|
||||||
|
)
|
||||||
|
|
||||||
|
_, wg_ports = processes.start_wg(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
targets:
|
||||||
|
- name: ssh
|
||||||
|
allow_roles: [role]
|
||||||
|
ssh:
|
||||||
|
host: localhost
|
||||||
|
port: {ssh_port}
|
||||||
|
users:
|
||||||
|
- username: user
|
||||||
|
roles: [role]
|
||||||
|
credentials:
|
||||||
|
- type: password
|
||||||
|
hash: '{password_123_hash}'
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
wait_port(ssh_port)
|
||||||
|
wait_port(wg_ports['ssh'])
|
||||||
|
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'user:ssh@localhost',
|
||||||
|
'-p',
|
||||||
|
str(wg_ports['ssh']),
|
||||||
|
'-i',
|
||||||
|
'/dev/null',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=password',
|
||||||
|
'ls',
|
||||||
|
'/bin/sh',
|
||||||
|
password='123',
|
||||||
|
)
|
||||||
|
assert ssh_client.communicate()[0] == b'/bin/sh\n'
|
||||||
|
assert ssh_client.returncode == 0
|
||||||
|
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'user:ssh@localhost',
|
||||||
|
'-p',
|
||||||
|
str(wg_ports['ssh']),
|
||||||
|
'-i',
|
||||||
|
'/dev/null',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=password',
|
||||||
|
'ls',
|
||||||
|
'/bin/sh',
|
||||||
|
password='321',
|
||||||
|
)
|
||||||
|
ssh_client.communicate()
|
||||||
|
assert ssh_client.returncode != 0
|
121
tests/test_ssh_user_auth_pubkey.py
Normal file
121
tests/test_ssh_user_auth_pubkey.py
Normal file
|
@ -0,0 +1,121 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from .conftest import ProcessManager
|
||||||
|
from .util import wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test_ed25519(
|
||||||
|
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path
|
||||||
|
):
|
||||||
|
ssh_port = processes.start_ssh_server(
|
||||||
|
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
|
||||||
|
)
|
||||||
|
|
||||||
|
_, wg_ports = processes.start_wg(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
targets:
|
||||||
|
- name: ssh
|
||||||
|
allow_roles: [role]
|
||||||
|
ssh:
|
||||||
|
host: localhost
|
||||||
|
port: {ssh_port}
|
||||||
|
users:
|
||||||
|
- username: user
|
||||||
|
roles: [role]
|
||||||
|
credentials:
|
||||||
|
- type: publickey
|
||||||
|
key: {open('ssh-keys/id_ed25519.pub').read().strip()}
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
wait_port(ssh_port)
|
||||||
|
wait_port(wg_ports['ssh'])
|
||||||
|
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'user:ssh@localhost',
|
||||||
|
'-p',
|
||||||
|
str(wg_ports['ssh']),
|
||||||
|
'-o',
|
||||||
|
'IdentityFile=ssh-keys/id_ed25519',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=publickey',
|
||||||
|
'ls',
|
||||||
|
'/bin/sh',
|
||||||
|
)
|
||||||
|
assert ssh_client.communicate()[0] == b'/bin/sh\n'
|
||||||
|
assert ssh_client.returncode == 0
|
||||||
|
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'user:ssh@localhost',
|
||||||
|
'-p',
|
||||||
|
str(wg_ports['ssh']),
|
||||||
|
'-o',
|
||||||
|
'IdentityFile=ssh-keys/id_rsa',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=publickey',
|
||||||
|
'ls',
|
||||||
|
'/bin/sh',
|
||||||
|
)
|
||||||
|
assert ssh_client.communicate()[0] == b''
|
||||||
|
assert ssh_client.returncode != 0
|
||||||
|
|
||||||
|
def test_rsa(
|
||||||
|
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path
|
||||||
|
):
|
||||||
|
ssh_port = processes.start_ssh_server(
|
||||||
|
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
|
||||||
|
)
|
||||||
|
|
||||||
|
_, wg_ports = processes.start_wg(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
targets:
|
||||||
|
- name: ssh
|
||||||
|
allow_roles: [role]
|
||||||
|
ssh:
|
||||||
|
host: localhost
|
||||||
|
port: {ssh_port}
|
||||||
|
users:
|
||||||
|
- username: user
|
||||||
|
roles: [role]
|
||||||
|
credentials:
|
||||||
|
- type: publickey
|
||||||
|
key: {open('ssh-keys/id_rsa.pub').read().strip()}
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
wait_port(ssh_port)
|
||||||
|
wait_port(wg_ports['ssh'])
|
||||||
|
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'user:ssh@localhost',
|
||||||
|
'-p',
|
||||||
|
str(wg_ports['ssh']),
|
||||||
|
'-o',
|
||||||
|
'IdentityFile=ssh-keys/id_rsa',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=publickey',
|
||||||
|
'ls',
|
||||||
|
'/bin/sh',
|
||||||
|
)
|
||||||
|
assert ssh_client.communicate()[0] == b'/bin/sh\n'
|
||||||
|
assert ssh_client.returncode == 0
|
||||||
|
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
'user:ssh@localhost',
|
||||||
|
'-p',
|
||||||
|
str(wg_ports['ssh']),
|
||||||
|
'-o',
|
||||||
|
'IdentityFile=ssh-keys/id_ed25519',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=publickey',
|
||||||
|
'ls',
|
||||||
|
'/bin/sh',
|
||||||
|
)
|
||||||
|
assert ssh_client.communicate()[0] == b''
|
||||||
|
assert ssh_client.returncode != 0
|
63
tests/test_ssh_user_auth_ticket.py
Normal file
63
tests/test_ssh_user_auth_ticket.py
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from textwrap import dedent
|
||||||
|
|
||||||
|
from .conftest import ProcessManager
|
||||||
|
from .util import create_ticket, wait_port
|
||||||
|
|
||||||
|
|
||||||
|
class Test:
|
||||||
|
def test(
|
||||||
|
self, processes: ProcessManager, wg_c_ed25519_pubkey: Path, password_123_hash
|
||||||
|
):
|
||||||
|
ssh_port = processes.start_ssh_server(
|
||||||
|
trusted_keys=[wg_c_ed25519_pubkey.read_text()]
|
||||||
|
)
|
||||||
|
|
||||||
|
_, wg_ports = processes.start_wg(
|
||||||
|
dedent(
|
||||||
|
f'''\
|
||||||
|
targets:
|
||||||
|
- name: ssh
|
||||||
|
allow_roles: [role]
|
||||||
|
ssh:
|
||||||
|
host: localhost
|
||||||
|
port: {ssh_port}
|
||||||
|
- name: admin
|
||||||
|
allow_roles: [admin]
|
||||||
|
web_admin: {{}}
|
||||||
|
users:
|
||||||
|
- username: user
|
||||||
|
roles: [role]
|
||||||
|
credentials:
|
||||||
|
- type: password
|
||||||
|
hash: '{password_123_hash}'
|
||||||
|
- username: admin
|
||||||
|
roles: [admin]
|
||||||
|
credentials:
|
||||||
|
- type: password
|
||||||
|
hash: '{password_123_hash}'
|
||||||
|
'''
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
wait_port(ssh_port)
|
||||||
|
wait_port(wg_ports['ssh'])
|
||||||
|
wait_port(wg_ports['http'], recv=False)
|
||||||
|
|
||||||
|
url = f'https://localhost:{wg_ports["http"]}'
|
||||||
|
secret = create_ticket(url, 'user', 'ssh')
|
||||||
|
|
||||||
|
ssh_client = processes.start_ssh_client(
|
||||||
|
f'ticket-{secret}@localhost',
|
||||||
|
'-p',
|
||||||
|
str(wg_ports['ssh']),
|
||||||
|
'-i',
|
||||||
|
'/dev/null',
|
||||||
|
'-o',
|
||||||
|
'PreferredAuthentications=password',
|
||||||
|
'ls',
|
||||||
|
'/bin/sh',
|
||||||
|
password='123',
|
||||||
|
)
|
||||||
|
assert ssh_client.communicate()[0] == b'/bin/sh\n'
|
||||||
|
assert ssh_client.returncode == 0
|
92
tests/util.py
Normal file
92
tests/util.py
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
|
||||||
|
last_port = 1234
|
||||||
|
|
||||||
|
mysql_client_ssl_opt = '--ssl'
|
||||||
|
if 'GITHUB_ACTION' in os.environ:
|
||||||
|
# Github uses MySQL instead of MariaDB
|
||||||
|
mysql_client_ssl_opt = '--ssl-mode=REQUIRED'
|
||||||
|
|
||||||
|
|
||||||
|
def alloc_port():
|
||||||
|
global last_port
|
||||||
|
last_port += 1
|
||||||
|
return last_port
|
||||||
|
|
||||||
|
|
||||||
|
def wait_port(port, recv=True):
|
||||||
|
logging.debug(f'Waiting for port {port}')
|
||||||
|
|
||||||
|
data = b''
|
||||||
|
|
||||||
|
def wait():
|
||||||
|
nonlocal data
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
s = socket.create_connection(('localhost', port), timeout=5)
|
||||||
|
if recv:
|
||||||
|
data = s.recv(100)
|
||||||
|
else:
|
||||||
|
data = b''
|
||||||
|
s.close()
|
||||||
|
logging.debug(f'Port {port} is up')
|
||||||
|
return data
|
||||||
|
except socket.error:
|
||||||
|
time.sleep(0.1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
t = threading.Thread(target=wait, daemon=True)
|
||||||
|
t.start()
|
||||||
|
t.join(timeout=5)
|
||||||
|
if t.is_alive():
|
||||||
|
raise Exception(f'Port {port} is not up')
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def wait_mysql_port(port):
|
||||||
|
logging.debug(f'Waiting for MySQL port {port}')
|
||||||
|
|
||||||
|
def wait():
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
subprocess.check_call(f'mysql --user=root --password=123 --host=127.0.0.1 --port={port} --execute="show schemas;"', shell=True)
|
||||||
|
logging.debug(f'Port {port} is up')
|
||||||
|
break
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
time.sleep(1)
|
||||||
|
continue
|
||||||
|
|
||||||
|
t = threading.Thread(target=wait, daemon=True)
|
||||||
|
t.start()
|
||||||
|
t.join(timeout=60)
|
||||||
|
if t.is_alive():
|
||||||
|
raise Exception(f'Port {port} is not up')
|
||||||
|
|
||||||
|
|
||||||
|
def create_ticket(url, username, target_name):
|
||||||
|
session = requests.Session()
|
||||||
|
session.verify = False
|
||||||
|
response = session.post(
|
||||||
|
f'{url}/@warpgate/api/auth/login',
|
||||||
|
json={
|
||||||
|
'username': 'admin',
|
||||||
|
'password': '123',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code // 100 == 2
|
||||||
|
response = session.post(
|
||||||
|
f'{url}/@warpgate/admin/api/tickets',
|
||||||
|
json={
|
||||||
|
'username': username,
|
||||||
|
'target_name': target_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assert response.status_code == 201
|
||||||
|
return response.json()['secret']
|
|
@ -18,7 +18,7 @@ use russh_keys::PublicKeyBase64;
|
||||||
pub use server::run_server;
|
pub use server::run_server;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use warpgate_common::{
|
use warpgate_common::{
|
||||||
ProtocolName, ProtocolServer, Services, Target, TargetOptions, TargetTestError,
|
ProtocolName, ProtocolServer, Services, Target, TargetOptions, TargetTestError, SshHostKeyVerificationMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::client::{RCCommand, RemoteClient};
|
use crate::client::{RCCommand, RemoteClient};
|
||||||
|
@ -81,13 +81,24 @@ impl ProtocolServer for SSHProtocolServer {
|
||||||
RCEvent::HostKeyUnknown(key, reply) => {
|
RCEvent::HostKeyUnknown(key, reply) => {
|
||||||
println!("\nHost key ({}): {}", key.name(), key.public_key_base64());
|
println!("\nHost key ({}): {}", key.name(), key.public_key_base64());
|
||||||
println!("There is no trusted {} key for this host.", key.name());
|
println!("There is no trusted {} key for this host.", key.name());
|
||||||
if dialoguer::Confirm::new()
|
|
||||||
.with_prompt("Trust this key?")
|
match self.services.config.lock().await.store.ssh.host_key_verification {
|
||||||
.interact()?
|
SshHostKeyVerificationMode::AutoAccept => {
|
||||||
{
|
let _ = reply.send(true);
|
||||||
let _ = reply.send(true);
|
}
|
||||||
} else {
|
SshHostKeyVerificationMode::AutoReject => {
|
||||||
let _ = reply.send(false);
|
let _ = reply.send(false);
|
||||||
|
}
|
||||||
|
SshHostKeyVerificationMode::Prompt => {
|
||||||
|
if dialoguer::Confirm::new()
|
||||||
|
.with_prompt("Trust this key?")
|
||||||
|
.interact()?
|
||||||
|
{
|
||||||
|
let _ = reply.send(true);
|
||||||
|
} else {
|
||||||
|
let _ = reply.send(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RCEvent::State(state) => match state {
|
RCEvent::State(state) => match state {
|
||||||
|
|
|
@ -936,9 +936,9 @@ impl ServerSession {
|
||||||
let selector: AuthSelector = (&ssh_username).into();
|
let selector: AuthSelector = (&ssh_username).into();
|
||||||
|
|
||||||
info!(
|
info!(
|
||||||
"Public key auth as {:?} with key FP {}",
|
"Public key auth as {:?} with key {}",
|
||||||
selector,
|
selector,
|
||||||
key.fingerprint()
|
key.public_key_base64()
|
||||||
);
|
);
|
||||||
|
|
||||||
match self
|
match self
|
||||||
|
@ -1065,7 +1065,7 @@ impl ServerSession {
|
||||||
|
|
||||||
login_url.set_path("@warpgate");
|
login_url.set_path("@warpgate");
|
||||||
login_url
|
login_url
|
||||||
.set_fragment(Some(&format!("/login?next=%2Flogin%2F{auth_state_id}")));
|
.set_fragment(Some(&format!("/login/{auth_state_id}")));
|
||||||
|
|
||||||
russh::server::Auth::Partial {
|
russh::server::Auth::Partial {
|
||||||
name: Cow::Owned(format!(
|
name: Cow::Owned(format!(
|
||||||
|
|
|
@ -49,8 +49,6 @@ pub enum SsoInternalProviderConfig {
|
||||||
},
|
},
|
||||||
#[serde(rename = "custom")]
|
#[serde(rename = "custom")]
|
||||||
Custom {
|
Custom {
|
||||||
name: String,
|
|
||||||
label: String,
|
|
||||||
client_id: ClientId,
|
client_id: ClientId,
|
||||||
client_secret: ClientSecret,
|
client_secret: ClientSecret,
|
||||||
issuer_url: IssuerUrl,
|
issuer_url: IssuerUrl,
|
||||||
|
|
|
@ -13,8 +13,8 @@
|
||||||
"postinstall": "yarn run openapi:client:gateway && yarn run openapi:client:admin",
|
"postinstall": "yarn run openapi:client:gateway && yarn run openapi:client:admin",
|
||||||
"openapi:schema:gateway": "cargo run -p warpgate-protocol-http > src/gateway/lib/openapi-schema.json",
|
"openapi:schema:gateway": "cargo run -p warpgate-protocol-http > src/gateway/lib/openapi-schema.json",
|
||||||
"openapi:schema:admin": "cargo run -p warpgate-admin > src/admin/lib/openapi-schema.json",
|
"openapi:schema:admin": "cargo run -p warpgate-admin > src/admin/lib/openapi-schema.json",
|
||||||
"openapi:client:gateway": "openapi-generator-cli generate -g typescript-fetch -i src/gateway/lib/openapi-schema.json -o src/gateway/lib/api-client -p npmName=warpgate-gateway-api-client -p useSingleRequestParameter=true && cd src/gateway/lib/api-client && npm i typescript@3.5 && npm i && yarn tsc --target esnext --module esnext && rm -rf src",
|
"openapi:client:gateway": "openapi-generator-cli generate -g typescript-fetch -i src/gateway/lib/openapi-schema.json -o src/gateway/lib/api-client -p npmName=warpgate-gateway-api-client -p useSingleRequestParameter=true && cd src/gateway/lib/api-client && npm i typescript@3.5 && npm i && yarn tsc --target esnext --module esnext && rm -rf src tsconfig.json",
|
||||||
"openapi:client:admin": "openapi-generator-cli generate -g typescript-fetch -i src/admin/lib/openapi-schema.json -o src/admin/lib/api-client -p npmName=warpgate-admin-api-client -p useSingleRequestParameter=true && cd src/admin/lib/api-client && npm i typescript@3.5 && npm i && yarn tsc --target esnext --module esnext && rm -rf src",
|
"openapi:client:admin": "openapi-generator-cli generate -g typescript-fetch -i src/admin/lib/openapi-schema.json -o src/admin/lib/api-client -p npmName=warpgate-admin-api-client -p useSingleRequestParameter=true && cd src/admin/lib/api-client && npm i typescript@3.5 && npm i && yarn tsc --target esnext --module esnext && rm -rf src tsconfig.json",
|
||||||
"openapi": "yarn run openapi:schema:admin && yarn run openapi:schema:gateway && yarn run openapi:client:admin && yarn run openapi:client:gateway"
|
"openapi": "yarn run openapi:schema:admin && yarn run openapi:schema:gateway && yarn run openapi:client:admin && yarn run openapi:client:gateway"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
@ -183,7 +183,7 @@ async function startSSO (provider: SsoProviderDescription) {
|
||||||
click={login}
|
click={login}
|
||||||
>
|
>
|
||||||
Login
|
Login
|
||||||
<Fa class="ms-2" icon={faArrowRight} />
|
<Fa class="ms-2" fw icon={faArrowRight} />
|
||||||
</AsyncButton>
|
</AsyncButton>
|
||||||
|
|
||||||
{#if authState === ApiAuthState.Failed}
|
{#if authState === ApiAuthState.Failed}
|
||||||
|
|
|
@ -4,6 +4,7 @@ use anyhow::Result;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
#[cfg(target_os = "linux")]
|
#[cfg(target_os = "linux")]
|
||||||
use sd_notify::NotifyState;
|
use sd_notify::NotifyState;
|
||||||
|
use tokio::signal::unix::SignalKind;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
use warpgate_common::db::cleanup_db;
|
use warpgate_common::db::cleanup_db;
|
||||||
use warpgate_common::logging::install_database_logger;
|
use warpgate_common::logging::install_database_logger;
|
||||||
|
@ -117,11 +118,16 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> {
|
||||||
services.clone(),
|
services.clone(),
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let mut sigint = tokio::signal::unix::signal(SignalKind::interrupt())?;
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
tokio::select! {
|
tokio::select! {
|
||||||
_ = tokio::signal::ctrl_c() => {
|
_ = tokio::signal::ctrl_c() => {
|
||||||
std::process::exit(1);
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
|
_ = sigint.recv() => {
|
||||||
|
break
|
||||||
|
}
|
||||||
result = protocol_futures.next() => {
|
result = protocol_futures.next() => {
|
||||||
match result {
|
match result {
|
||||||
Some(Err(error)) => {
|
Some(Err(error)) => {
|
||||||
|
|
|
@ -53,8 +53,9 @@ pub(crate) async fn command(cli: &crate::Cli, target_name: &String) -> Result<()
|
||||||
}
|
}
|
||||||
Ok(()) => {
|
Ok(()) => {
|
||||||
info!("Connection successful!");
|
info!("Connection successful!");
|
||||||
|
return Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
anyhow::bail!("Connection test failed")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue