diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..4375cfb --- /dev/null +++ b/.flake8 @@ -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 diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..08bcc8d --- /dev/null +++ b/.github/workflows/test.yml @@ -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 }} diff --git a/.gitignore b/.gitignore index 3333315..829960b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,5 @@ host_key* data config.*.yaml config.yaml +__pycache__ +.pytest_cache diff --git a/Cargo.toml b/Cargo.toml index e9070f3..596fc93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,5 @@ +cargo-features = ["profile-rustflags"] + [workspace] members = [ "warpgate", @@ -18,3 +20,7 @@ default-members = ["warpgate"] lto = true panic = "abort" strip = "debuginfo" + +[profile.coverage] +inherits = "dev" +rustflags = ["-Cinstrument-coverage"] diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..aa518b1 --- /dev/null +++ b/sonar-project.properties @@ -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 diff --git a/tests/Makefile b/tests/Makefile new file mode 100644 index 0000000..4702f5e --- /dev/null +++ b/tests/Makefile @@ -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 diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/certs/tls.certificate.pem b/tests/certs/tls.certificate.pem new file mode 100644 index 0000000..978d8dd --- /dev/null +++ b/tests/certs/tls.certificate.pem @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBYjCCAQmgAwIBAgIJAKXIp8GepnCzMAoGCCqGSM49BAMCMCExHzAdBgNVBAMM +FnJjZ2VuIHNlbGYgc2lnbmVkIGNlcnQwIBcNNzUwMTAxMDAwMDAwWhgPNDA5NjAx +MDEwMDAwMDBaMCExHzAdBgNVBAMMFnJjZ2VuIHNlbGYgc2lnbmVkIGNlcnQwWTAT +BgcqhkjOPQIBBggqhkjOPQMBBwNCAARAtRfTqyH8+eXf12Vftm6VcMhhYG6Ape3O +tcLfIWJo1krsOP+96r5U20ya7YVVFmYFPoQToAOoio2dxlX3jOL/oygwJjAkBgNV +HREEHTAbgg53YXJwZ2F0ZS5sb2NhbIIJbG9jYWxob3N0MAoGCCqGSM49BAMCA0cA +MEQCICTt3I/PsgF8Rvu6aKwY2LTouZyxReDMiCePzsqdAxXAAiATNw61MBylNaAF +FGkPqR0VZIR6sIFHZnib9JQNhka2Fg== +-----END CERTIFICATE----- diff --git a/tests/certs/tls.key.pem b/tests/certs/tls.key.pem new file mode 100644 index 0000000..2bd7bcf --- /dev/null +++ b/tests/certs/tls.key.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg0tJmr/OSF7neTQOV +gQn+qHCdVsOENdMc86RlWPiWDlKhRANCAARAtRfTqyH8+eXf12Vftm6VcMhhYG6A +pe3OtcLfIWJo1krsOP+96r5U20ya7YVVFmYFPoQToAOoio2dxlX3jOL/ +-----END PRIVATE KEY----- diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..96f0d0c --- /dev/null +++ b/tests/conftest.py @@ -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) diff --git a/tests/images/mysql-server/Dockerfile b/tests/images/mysql-server/Dockerfile new file mode 100644 index 0000000..ac9c581 --- /dev/null +++ b/tests/images/mysql-server/Dockerfile @@ -0,0 +1,6 @@ +FROM mariadb:10.8 + +ENV MYSQL_DATABASE=db +ENV MYSQL_ROOT_PASSWORD=123 + +ADD init.sql /docker-entrypoint-initdb.d diff --git a/tests/images/mysql-server/init.sql b/tests/images/mysql-server/init.sql new file mode 100644 index 0000000..3f80726 --- /dev/null +++ b/tests/images/mysql-server/init.sql @@ -0,0 +1,4 @@ +CREATE TABLE `db`.`table` ( + `id` int(11) NOT NULL, + `name` varchar(1023) NOT NULL +) ENGINE=InnoDB; diff --git a/tests/images/ssh-server/Dockerfile b/tests/images/ssh-server/Dockerfile new file mode 100644 index 0000000..d83c363 --- /dev/null +++ b/tests/images/ssh-server/Dockerfile @@ -0,0 +1,4 @@ +FROM alpine:3.14 +RUN apk add openssh +RUN passwd -u root +ENTRYPOINT ["/usr/sbin/sshd", "-De"] diff --git a/tests/poetry.lock b/tests/poetry.lock new file mode 100644 index 0000000..5f65c83 --- /dev/null +++ b/tests/poetry.lock @@ -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 = [] diff --git a/tests/pyproject.toml b/tests/pyproject.toml new file mode 100644 index 0000000..15d6a96 --- /dev/null +++ b/tests/pyproject.toml @@ -0,0 +1,29 @@ +[tool.poetry] +name = "tests" +version = "0.1.0" +description = "" +authors = ["Your Name "] + +[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" diff --git a/tests/run.sh b/tests/run.sh new file mode 100755 index 0000000..64f994f --- /dev/null +++ b/tests/run.sh @@ -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 diff --git a/tests/ssh-keys/id_ed25519 b/tests/ssh-keys/id_ed25519 new file mode 100644 index 0000000..79db2ae --- /dev/null +++ b/tests/ssh-keys/id_ed25519 @@ -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----- diff --git a/tests/ssh-keys/id_ed25519.pub b/tests/ssh-keys/id_ed25519.pub new file mode 100644 index 0000000..1dc40df --- /dev/null +++ b/tests/ssh-keys/id_ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDP/CQS05AYYvEeZ3X6EPSE4liuLuP7w6p7HgIydOvbq diff --git a/tests/ssh-keys/id_rsa b/tests/ssh-keys/id_rsa new file mode 100644 index 0000000..100bc4a --- /dev/null +++ b/tests/ssh-keys/id_rsa @@ -0,0 +1,38 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABlwAAAAdzc2gtcn +NhAAAAAwEAAQAAAYEAnd9Mz8FnYZm/pXB1J+3Yx8IDsb8vuggGmE/xJZm/H0vuCD4vA/aD +s3gdqXLjU1/uyD0J/CQPH4GvNqAkJ8AivM2VhnFsG1QJLklkXEHeBFlT81mxO2t0DB4S6Q +QXgbYC7XtyY6gVqRYuMT+WoanzJcYMv3gSGFr/Yn8WnVYl33zsD8YtLl3Ku71+5kykFC/8 +/LlxOUY73XzTPVuChETvz7KhKrlBiHhOQwjyx2B1JdGQp5hSrBTSmO3+ImIMsClsQw3fP0 +vc7st7L06eOBNTrhPOiQqjyn5MoCfoIRAu6uE7oHyrsLfYiZrJWySz+TdZZNedKfQxWDXB +cK5+0p8cYyS/U+yGHRrSjSHtl+qqS8QYCTdlteR/EDSuPO7yE63tRpORg6L5kquAupreH6 +rtgO0Hpz/6ZXj9bsAR+Gs+J6MxzrJWeDiAxUAkvg9anYgj3skDFBsfylVe2eey1fQtx0/i +iKNjFyrpqHPAUIyJXa5QTplpZPicKMpa+X38q6gfAAAFmAwAuXkMALl5AAAAB3NzaC1yc2 +EAAAGBAJ3fTM/BZ2GZv6VwdSft2MfCA7G/L7oIBphP8SWZvx9L7gg+LwP2g7N4Haly41Nf +7sg9CfwkDx+BrzagJCfAIrzNlYZxbBtUCS5JZFxB3gRZU/NZsTtrdAweEukEF4G2Au17cm +OoFakWLjE/lqGp8yXGDL94Ehha/2J/Fp1WJd987A/GLS5dyru9fuZMpBQv/Py5cTlGO918 +0z1bgoRE78+yoSq5QYh4TkMI8sdgdSXRkKeYUqwU0pjt/iJiDLApbEMN3z9L3O7Ley9Onj +gTU64TzokKo8p+TKAn6CEQLurhO6B8q7C32ImayVsks/k3WWTXnSn0MVg1wXCuftKfHGMk +v1Pshh0a0o0h7ZfqqkvEGAk3ZbXkfxA0rjzu8hOt7UaTkYOi+ZKrgLqa3h+q7YDtB6c/+m +V4/W7AEfhrPiejMc6yVng4gMVAJL4PWp2II97JAxQbH8pVXtnnstX0LcdP4oijYxcq6ahz +wFCMiV2uUE6ZaWT4nCjKWvl9/KuoHwAAAAMBAAEAAAGAV8re70XRVOBoR/sy24KUI/oLje +QRCXX/HOKP6uYF98SE2YajJKQI91vbuuiN7EaUBjyTeekfk9jNdCY4FPbvGmmFNl+Ky+O+ +u0PLENb8PRTj75c4TR/jR/3NbFF/NP3fwOr+YNcPPJl+FJsVDE/zTFVHr455GZw5GzArhl +Fq/E5/BAKkC33TCPZHRJDoSeWp3WzOvxgEoJYS7rMd8KpZZfojUBv3ionEk9i9Egzc+KwC +soCtsM5fkvX+dmZqQei2UTE7gAQfTdzo7kLrLqeCS5UrVEDEDp94Wf6ZQuEPjKioKfpoHr +bPiLhcsIH3N7aBpXzXok4dM2U+UgzQW5HfJtgrkUkvlNV3kgtUEw37X9kOhowt/yd6KBT0 +Pn5qUtJtBIHKEBpUfyNlJhH9uA/Wift2N7D0TYDVuiAyzT58BK8pS6F55z8ENvvEux/s56 +qZj81FUuGwC9L6xTxya88TOGnEZnVnMFTK8MgbcY1cBZqiKLIgBuusMd5lRCbWaBc5AAAA +wQCmKQGwQLaffZg2g9fJMyIA7MGDbqMy1Y5DmiGjATGh+oigMY4+ytgvCy8PrBL9NEZCcN +FMwTUHUgjUP7611DygK+lDHly60bvmhP4JEkI5adxkd13jCXq2dNiazLV7mD2ZghkOkGcl +hMhd7dmYLL1mzlayEvRKLzvexjmcLS4qFkvDl7mCrUBwsAnr6VbZTIyisz1f3H/u+cbWF5 +iSaguuuNL5o1hGCeoCdbAu40e1XOTUQU6kD7GnrV7tATZ8cDMAAADBAMwMsotmUXQkS6fR +XEiMjse3pVT1hRIcDNNZOeavY/cByFoOv/flo111YL/+aGOT68daLBeMwAYkUtgAFpNZjt +LpRZKK7/sN2pliWgCU2PWb4is4QBvmWVtIIaCDs7YbwnumZJdyQHoG8W4wQG+RqzBu5VsA +ylinLCbTuZ3I9oOZcLxfJESmTwl3a9oUquN8C4TmcoAS8MRXd7SHndVxS4rvLkHHV5t2bG +EazYY/2VOUVv8Z3FJc35ZGhzb5vVjLnQAAAMEAxhDpQsXyhoP9CZnkd21rOinYvMDY9ZjC +AYLJ1k7SWBnUki6ozssnURvPgXUKFdj4xfevNvXQdf6Ctj7b1ghC07gpJ9M4Hq+JJcMPSw +l9JQpOMCI46nzbLNeAkXhvvpsuMWJO9L4+e+6LZZHHH665e47/dNFgiuIpUQqQaWdqHWGe +/4z5XrNjRprno01TsMAln8q3aEx3naONapHr9t7WoqT14cuo4NHZVb1oejssDa3iyAajEo +pLZK1nKRsDPQvrAAAAHGV1Z2VuZUBFdWdlbmVzLU1CUC5mcml0ei5ib3gBAgMEBQY= +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh-keys/id_rsa.pub b/tests/ssh-keys/id_rsa.pub new file mode 100644 index 0000000..7c10e2a --- /dev/null +++ b/tests/ssh-keys/id_rsa.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCd30zPwWdhmb+lcHUn7djHwgOxvy+6CAaYT/Elmb8fS+4IPi8D9oOzeB2pcuNTX+7IPQn8JA8fga82oCQnwCK8zZWGcWwbVAkuSWRcQd4EWVPzWbE7a3QMHhLpBBeBtgLte3JjqBWpFi4xP5ahqfMlxgy/eBIYWv9ifxadViXffOwPxi0uXcq7vX7mTKQUL/z8uXE5RjvdfNM9W4KERO/PsqEquUGIeE5DCPLHYHUl0ZCnmFKsFNKY7f4iYgywKWxDDd8/S9zuy3svTp44E1OuE86JCqPKfkygJ+ghEC7q4TugfKuwt9iJmslbJLP5N1lk150p9DFYNcFwrn7SnxxjJL9T7IYdGtKNIe2X6qpLxBgJN2W15H8QNK487vITre1Gk5GDovmSq4C6mt4fqu2A7QenP/pleP1uwBH4az4nozHOslZ4OIDFQCS+D1qdiCPeyQMUGx/KVV7Z57LV9C3HT+KIo2MXKumoc8BQjIldrlBOmWlk+Jwoylr5ffyrqB8= diff --git a/tests/ssh-keys/wg/client-ed25519 b/tests/ssh-keys/wg/client-ed25519 new file mode 100644 index 0000000..bfc0ed2 --- /dev/null +++ b/tests/ssh-keys/wg/client-ed25519 @@ -0,0 +1,7 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW +QyNTUxOQAAACB8c0a+GzS7QQTMt4qvepvzMRZx9t5OvKj2o2HxmOVMAQAAAKAK02MACtNj +AAAAAAtzc2gtZWQyNTUxOQAAACB8c0a+GzS7QQTMt4qvepvzMRZx9t5OvKj2o2HxmOVMAQ +AAAEDuH0qssNc+ANqiQp0MsgyLECAPmDjkUeNpUbahI+AagnxzRr4bNLtBBMy3iq96m/Mx +FnH23k68qPajYfGY5UwBAAAAHGV1Z2VuZUBFdWdlbmVzLU1CUC5mcml0ei5ib3gB +-----END OPENSSH PRIVATE KEY----- diff --git a/tests/ssh-keys/wg/client-ed25519.pub b/tests/ssh-keys/wg/client-ed25519.pub new file mode 100644 index 0000000..9b92939 --- /dev/null +++ b/tests/ssh-keys/wg/client-ed25519.pub @@ -0,0 +1 @@ +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIHxzRr4bNLtBBMy3iq96m/MxFnH23k68qPajYfGY5UwB eugene@Eugenes-MBP.fritz.box diff --git a/tests/ssh-keys/wg/client-rsa b/tests/ssh-keys/wg/client-rsa new file mode 100644 index 0000000..26a541a --- /dev/null +++ b/tests/ssh-keys/wg/client-rsa @@ -0,0 +1,45 @@ +-----BEGIN PRIVATE KEY----- +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 + +-----END PRIVATE KEY----- diff --git a/tests/ssh-keys/wg/host-ed25519 b/tests/ssh-keys/wg/host-ed25519 new file mode 100644 index 0000000..1302687 --- /dev/null +++ b/tests/ssh-keys/wg/host-ed25519 @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MHMCAQEwBQYDK2VwBEIEQBHw4cXPpGgA+KdvPF5gxrzML+oa3yQk0JzIbWvmqM5H30RyBF8GrOWz +p77UAd3O4PgYzzFcUc79g8yKtbKhzJGhIwMhAN9EcgRfBqzls6e+1AHdzuD4GM8xXFHO/YPMirWy +ocyR + +-----END PRIVATE KEY----- diff --git a/tests/ssh-keys/wg/host-rsa b/tests/ssh-keys/wg/host-rsa new file mode 100644 index 0000000..8cbd954 --- /dev/null +++ b/tests/ssh-keys/wg/host-rsa @@ -0,0 +1,45 @@ +-----BEGIN PRIVATE KEY----- +MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQCxPfQ6f73tfGFRV93Aby+hCQAP +QlkOo/4BhG2VoPq4gjcyl5XnhV/zUQFTvcYX/AGbZBI4Wgq70d3R8zH8Xz11Vpj9K+KKpzS3ortk +SC5ZN7bOK1eSnlaS7AVrDX2802KIZOuIvxNrgLQeFnMapXPgaSaD/4F6+U48DlmMQ0cgV39rqwmy +1yJjVXBOVmNtZ7Fd2E+5ov3mmR4iLzSs6YYjTd0Atp7EhsKfPv99zLDYQIqeQZJt5kxsWd+0p2Ci +G1BfOTUTyD5x4YMN6JAk0GvzH09SF17M+yhascljTlnZuHgIjVIESAEoNFBoGytVhOkFvtc3MSiZ +Fdcb38iAbZkEEjHU1K+KCeAJoGG9rblOKV/xrVMXTLVEKa6gKeetuMJhlqXfbpB6bO6aJuPyLRp7 +8Ky0l3kpZrKv2fgMa0sHD14HcPqsEQf8pPhW8oxepyTArTHlU738/9wZ4K8HWt/fKRaXcY81Dz1M +ywSNKbvoqOJNvZDABkgeABJYsLxVYv4ReXy8plrJkuVNrlX1H0KKmFCt0F51SCGAOHp4wqWn/g0c +vz5J3P6E2Bnw1P67VbFnBvRHlDFxtbKMzob56TNutNkpDqQYwBgPz2HVQde3Og2Kem0RCxENsiU7 +Z78w8NYfGfUiu5qt8jKICiJ48TCciwoSF5/uFwnzM9glEp6BPQIDAQABAoICABnzeUagx4f11dtE +IzrMIiF7oN+bFlEMyox2/a3n3m3qMEzJtxrUA3grxyb3ZVbDq4nl/Wj002zWo0TcooKngJHP9ncz +LWihvMKaeBeMc1oqzIBOsPQjF4f244AzfyfeR3yy/MLj6eMBUDNg6W+LBCFlI/eLD0Pt1s+iRj2W +6DDLFDlegf1b1Il4zAhxoP3hgy27a0jsnYJdrvTQYUUOrXjj1f+xvXixm94XKkTFa1XudUgLT8uk +Pvz/rRUqtf0Q72lR21H52B1yfcQpO1m4jpBlaDFxLIyUxZQp7dO1eCBnCw7YKkcS3TXWxbhzKfAh +QBZ679Cr85wef4VxnvjMPe1an/g26brf9o9ralNawdJyakuaOa57CaoiEwk+ZxcZAjpmvS8c0sI0 +ohsiXxouHbZlv6Hrr/ACnFmRp6y+i9wgkk8850GztILnDs5L3pLSDside3VraefuDJ76Qa8ulXIq +/0c5aoj3JkvJYOSGIDvnQUpE8rcrRs43Az3GRUtYfkCpQlyqORR+aHgMcxZfTLKxhLdbgUE9BJc8 +E33xOW0VIsNX2O7RGpDiaFH3dfL9wMORViFR0B/A8q/TKarU7ni+hVgmTyIg7D0NTnnZNaOSloBr +A50iFJxa5YqmpDduaki5WlLzBFauDH5UaeNeAPrT1fYWksrl5IGdOjDxkEE9AoIBAQDUz4jlGW0s +hAmafSrtwbVBPBFZ122AuyplibLlHgADsD6Uu3tUKgVd22nCV8wzlpxL9GPG80S5aJPPI6gpl2XH +vUV2yWuJLsW5324hdKjxH5mJjWZtA0CpIZ+PdNc3LLolFLgew5HMt1sHZeqFRqxbZLGBmNuXyOme +k1SCTVmMuTK8wzWVzDhc2g+u4NL1eRccmS18bGKGA+oNnY/IRdjdPw5gNeBg/6ZVimXj91d0WiFi +6Pr0Bop0g0bpAoWk6s/XskBVQAYcCmNZMI+ll05RgARKYK+Donn/zu/5jGYXQ+9jAv78q796nT2J +dRIPAh/azTxu+yE1byaC01iLGTVDAoIBAQDVNnfBHRF2mWLwfpPponXISupn2WApgjX7P8HS9WjN +bb/X9gbjNJXIgF+XqpwOxaQFolCHw9uyv7+73PHhDiaSqdw5NkvFtfJkacWlJblybUVVGljaGgpp +KFlRqcX8knFwNpDXwnoMttST0eoWZnj5jeWhQkc/8XceN9NPY4fxAwXceMWBAW03p47vMpxC6FGN +Et7KlBB5coQWkVZyxjq3n69FouHAQZWEjBmgBpGf16HtvSGIO91hr3PXX5oktvRejN25WQiwLG0O +/DHnNvQcovb9QpjZ3fnBfkxQmzqLsdCR/FTsYtvMqnR2OzR/zI3UeRPAphzfWBAG2W6+Esd/AoIB +AQC90GqjJd254f+K22/p52hbSk+TmdIjC05SiNKXB/4tTAtVsC/drylgQO+BF7ycmw7HtLE2aA95 +bKzCCmTYzCBNWyXVQOz4zE4ybvaVQq/Zej0BcqzUOR14ffQLCcVYgj16C5P6ZKfsN/Mqkx3uSE49 +qn+lP4lGRj8SYQj0vDdOjHWT5m4qMaBoOVvZuNCRgLM7n+jxXN8398/Q2yO/F4XKOY8CA6wh+IUN +MUeWYSyRLD8xMOt9s0PVjq418TjxEzvVgTlekJ+ibSWWDPljUqTZjtzE1p5WRBqbL6HeLPt2bvLb +lnWHO02r+QpFS7WSy2tMRtlLiBVjysNH12jXkOFvAoIBAEyGSiETr8rjbrFmnOwEFUYYLV2slWkQ +hRNyZLy0vDLPK0X11a8Clqfp+2VSJMTghuhGw6SW1WmojMZ+nInsLEgDkzktlbCWhzMnC3skuRSq +x3GuDSnqosXvZ296AcePQAvIaeAmuuuJS27qrpvvl4fqN/rS8QOwRNKhssQRsx77uMTSzABrZKnP +B+wuPAt/mpWJqlEHJ4qPYX1AGMkFANobBCt4NJJud52lMyVOdkHqgQH1Ge3tnp2K/YbVl1uKFtdA +s+vsWsPwjgwM1FRqUt9cVk2782Ru2U9rZzSfIjo1Tei3qjtVmBIzM62jvkoIPvd9pWtFs6Mt1kK/ +E5JA5z0CggEBAKgsIb3x7mXJXelmyeD3m3KdzIsIqHZ1yo3YUPs6O/2UKwmPMEAcgfBbiIL/Z0Tw +GqytCKR8ghKxj/CLHqZne9P+hd0vJrJfJGuita6JC9HI8EVd024FV+QAjZjQHdSUe3NY4h+J9U0h +f0r/LIyoWGGKKaPLVxJF5EJa46cixhSp71w4fhlIG/FtTNXVGzkJeh4jrLlKU84RxpprUoS9wwKS +gidavaqws6Ks8oVf0oJaqeMk8RrlEeAR1OyXtaxdAdYnHsMKLLoNrEKUAHdBexpJ9fhqlQhZWkT6 +xS6VZKFUtFtzffTY0HE+R+ESSmiUSl3mB6gsfiXlu0Q9kxjw3wU= + +-----END PRIVATE KEY----- diff --git a/tests/test_http_basic.py b/tests/test_http_basic.py new file mode 100644 index 0000000..6584e0c --- /dev/null +++ b/tests/test_http_basic.py @@ -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' diff --git a/tests/test_http_common.py b/tests/test_http_common.py new file mode 100644 index 0000000..8421552 --- /dev/null +++ b/tests/test_http_common.py @@ -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/') + def r(url): + return redirect(url) + + @app.route('/', defaults={'path': ''}) + @app.route('/') + 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 diff --git a/tests/test_http_conntest.py b/tests/test_http_conntest.py new file mode 100644 index 0000000..4cf95cb --- /dev/null +++ b/tests/test_http_conntest.py @@ -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 diff --git a/tests/test_http_cookies.py b/tests/test_http_cookies.py new file mode 100644 index 0000000..fe7d264 --- /dev/null +++ b/tests/test_http_cookies.py @@ -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' diff --git a/tests/test_http_redirects.py b/tests/test_http_redirects.py new file mode 100644 index 0000000..7d70a5c --- /dev/null +++ b/tests/test_http_redirects.py @@ -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' diff --git a/tests/test_http_user_auth_logout.py b/tests/test_http_user_auth_logout.py new file mode 100644 index 0000000..0914f27 --- /dev/null +++ b/tests/test_http_user_auth_logout.py @@ -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 diff --git a/tests/test_http_user_auth_otp.py b/tests/test_http_user_auth_otp.py new file mode 100644 index 0000000..d24f9a2 --- /dev/null +++ b/tests/test_http_user_auth_otp.py @@ -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 diff --git a/tests/test_http_user_auth_password.py b/tests/test_http_user_auth_password.py new file mode 100644 index 0000000..896803d --- /dev/null +++ b/tests/test_http_user_auth_password.py @@ -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 diff --git a/tests/test_http_user_auth_ticket.py b/tests/test_http_user_auth_ticket.py new file mode 100644 index 0000000..1ba7231 --- /dev/null +++ b/tests/test_http_user_auth_ticket.py @@ -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' diff --git a/tests/test_http_websocket.py b/tests/test_http_websocket.py new file mode 100644 index 0000000..50c0af6 --- /dev/null +++ b/tests/test_http_websocket.py @@ -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() diff --git a/tests/test_mysql_user_auth_password.py b/tests/test_mysql_user_auth_password.py new file mode 100644 index 0000000..027f638 --- /dev/null +++ b/tests/test_mysql_user_auth_password.py @@ -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 diff --git a/tests/test_ssh_conntest.py b/tests/test_ssh_conntest.py new file mode 100644 index 0000000..4c00c80 --- /dev/null +++ b/tests/test_ssh_conntest.py @@ -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 diff --git a/tests/test_ssh_proto.py b/tests/test_ssh_proto.py new file mode 100644 index 0000000..1795623 --- /dev/null +++ b/tests/test_ssh_proto.py @@ -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() diff --git a/tests/test_ssh_target_selection.py b/tests/test_ssh_target_selection.py new file mode 100644 index 0000000..d048ae7 --- /dev/null +++ b/tests/test_ssh_target_selection.py @@ -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 diff --git a/tests/test_ssh_user_auth_otp.py b/tests/test_ssh_user_auth_otp.py new file mode 100644 index 0000000..899e88c --- /dev/null +++ b/tests/test_ssh_user_auth_otp.py @@ -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 diff --git a/tests/test_ssh_user_auth_password.py b/tests/test_ssh_user_auth_password.py new file mode 100644 index 0000000..5aeb3e4 --- /dev/null +++ b/tests/test_ssh_user_auth_password.py @@ -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 diff --git a/tests/test_ssh_user_auth_pubkey.py b/tests/test_ssh_user_auth_pubkey.py new file mode 100644 index 0000000..b9bc4f3 --- /dev/null +++ b/tests/test_ssh_user_auth_pubkey.py @@ -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 diff --git a/tests/test_ssh_user_auth_ticket.py b/tests/test_ssh_user_auth_ticket.py new file mode 100644 index 0000000..411924d --- /dev/null +++ b/tests/test_ssh_user_auth_ticket.py @@ -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 diff --git a/tests/util.py b/tests/util.py new file mode 100644 index 0000000..8bdfa40 --- /dev/null +++ b/tests/util.py @@ -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'] diff --git a/warpgate-protocol-ssh/src/lib.rs b/warpgate-protocol-ssh/src/lib.rs index df13480..cbdb059 100644 --- a/warpgate-protocol-ssh/src/lib.rs +++ b/warpgate-protocol-ssh/src/lib.rs @@ -18,7 +18,7 @@ use russh_keys::PublicKeyBase64; pub use server::run_server; use uuid::Uuid; use warpgate_common::{ - ProtocolName, ProtocolServer, Services, Target, TargetOptions, TargetTestError, + ProtocolName, ProtocolServer, Services, Target, TargetOptions, TargetTestError, SshHostKeyVerificationMode, }; use crate::client::{RCCommand, RemoteClient}; @@ -81,13 +81,24 @@ impl ProtocolServer for SSHProtocolServer { RCEvent::HostKeyUnknown(key, reply) => { println!("\nHost key ({}): {}", key.name(), key.public_key_base64()); println!("There is no trusted {} key for this host.", key.name()); - if dialoguer::Confirm::new() - .with_prompt("Trust this key?") - .interact()? - { - let _ = reply.send(true); - } else { - let _ = reply.send(false); + + match self.services.config.lock().await.store.ssh.host_key_verification { + SshHostKeyVerificationMode::AutoAccept => { + let _ = reply.send(true); + } + SshHostKeyVerificationMode::AutoReject => { + 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 { diff --git a/warpgate-protocol-ssh/src/server/session.rs b/warpgate-protocol-ssh/src/server/session.rs index 804c542..5cf7fbd 100644 --- a/warpgate-protocol-ssh/src/server/session.rs +++ b/warpgate-protocol-ssh/src/server/session.rs @@ -936,9 +936,9 @@ impl ServerSession { let selector: AuthSelector = (&ssh_username).into(); info!( - "Public key auth as {:?} with key FP {}", + "Public key auth as {:?} with key {}", selector, - key.fingerprint() + key.public_key_base64() ); match self @@ -1065,7 +1065,7 @@ impl ServerSession { login_url.set_path("@warpgate"); 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 { name: Cow::Owned(format!( diff --git a/warpgate-sso/src/config.rs b/warpgate-sso/src/config.rs index 7b19701..aa6dd60 100644 --- a/warpgate-sso/src/config.rs +++ b/warpgate-sso/src/config.rs @@ -49,8 +49,6 @@ pub enum SsoInternalProviderConfig { }, #[serde(rename = "custom")] Custom { - name: String, - label: String, client_id: ClientId, client_secret: ClientSecret, issuer_url: IssuerUrl, diff --git a/warpgate-web/package.json b/warpgate-web/package.json index 97927fd..34abfbb 100644 --- a/warpgate-web/package.json +++ b/warpgate-web/package.json @@ -13,8 +13,8 @@ "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: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: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: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 tsconfig.json", "openapi": "yarn run openapi:schema:admin && yarn run openapi:schema:gateway && yarn run openapi:client:admin && yarn run openapi:client:gateway" }, "devDependencies": { diff --git a/warpgate-web/src/gateway/Login.svelte b/warpgate-web/src/gateway/Login.svelte index 4f55c62..2738d64 100644 --- a/warpgate-web/src/gateway/Login.svelte +++ b/warpgate-web/src/gateway/Login.svelte @@ -183,7 +183,7 @@ async function startSSO (provider: SsoProviderDescription) { click={login} > Login - + {#if authState === ApiAuthState.Failed} diff --git a/warpgate/src/commands/run.rs b/warpgate/src/commands/run.rs index d1828ef..212296b 100644 --- a/warpgate/src/commands/run.rs +++ b/warpgate/src/commands/run.rs @@ -4,6 +4,7 @@ use anyhow::Result; use futures::StreamExt; #[cfg(target_os = "linux")] use sd_notify::NotifyState; +use tokio::signal::unix::SignalKind; use tracing::*; use warpgate_common::db::cleanup_db; use warpgate_common::logging::install_database_logger; @@ -117,11 +118,16 @@ pub(crate) async fn command(cli: &crate::Cli) -> Result<()> { services.clone(), )); + let mut sigint = tokio::signal::unix::signal(SignalKind::interrupt())?; + loop { tokio::select! { _ = tokio::signal::ctrl_c() => { std::process::exit(1); } + _ = sigint.recv() => { + break + } result = protocol_futures.next() => { match result { Some(Err(error)) => { diff --git a/warpgate/src/commands/test_target.rs b/warpgate/src/commands/test_target.rs index 02017e1..9342200 100644 --- a/warpgate/src/commands/test_target.rs +++ b/warpgate/src/commands/test_target.rs @@ -53,8 +53,9 @@ pub(crate) async fn command(cli: &crate::Cli, target_name: &String) -> Result<() } Ok(()) => { info!("Connection successful!"); + return Ok(()) } } - Ok(()) + anyhow::bail!("Connection test failed") }