Repository: cmliu/webssh Branch: master Commit: d8a7898a3329 Files: 54 Total size: 167.5 KB Directory structure: gitextract_o4r80yov/ ├── .coveragerc ├── .dockerignore ├── .github/ │ ├── FUNDING.yml │ └── workflows/ │ ├── docker.yml │ └── python.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── MANIFEST.in ├── README.md ├── README.rst ├── docker-compose.yml ├── requirements.txt ├── run.py ├── setup.cfg ├── setup.py ├── tests/ │ ├── __init__.py │ ├── data/ │ │ ├── cert.crt │ │ ├── cert.key │ │ ├── fonts/ │ │ │ ├── .gitignore │ │ │ └── fake-font │ │ ├── known_hosts_example │ │ ├── known_hosts_example2 │ │ ├── known_hosts_example3 │ │ ├── test_ed25519.key │ │ ├── test_ed25519_password.key │ │ ├── test_known_hosts │ │ ├── test_new_dsa.key │ │ ├── test_new_rsa_password.key │ │ ├── test_rsa.key │ │ ├── test_rsa_password.key │ │ └── user_rsa_key │ ├── sshserver.py │ ├── test_app.py │ ├── test_handler.py │ ├── test_main.py │ ├── test_policy.py │ ├── test_settings.py │ ├── test_utils.py │ └── utils.py ├── user.js/ │ └── Build-SSH-Link.user.js └── webssh/ ├── __init__.py ├── _version.py ├── handler.py ├── main.py ├── policy.py ├── settings.py ├── static/ │ ├── css/ │ │ └── fonts/ │ │ └── .gitignore │ ├── js/ │ │ ├── main.js │ │ └── service-worker.js │ └── manifest.json ├── templates/ │ └── index.html ├── utils.py └── worker.py ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveragerc ================================================ [run] branch = true source = webssh [report] exclude_lines = if self.debug: pragma: no cover raise NotImplementedError if __name__ == .__main__.: ignore_errors = True omit = tests/* ================================================ FILE: .dockerignore ================================================ .git ================================================ FILE: .github/FUNDING.yml ================================================ # These are supported funding model platforms # github: huashengdun ko_fi: huashengdun custom: https://bit.ly/2XmXXIP ================================================ FILE: .github/workflows/docker.yml ================================================ name: Docker Build and Push on: workflow_dispatch: jobs: docker: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v3 - name: Login to Docker Hub uses: docker/login-action@v2 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} - name: Set up QEMU # 用于多平台编译 uses: docker/setup-qemu-action@v2 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v2 - name: Build and push multi-arch Docker image uses: docker/build-push-action@v4 with: context: . push: true platforms: linux/amd64,linux/arm64 tags: cmliu/webssh:latest ================================================ FILE: .github/workflows/python.yml ================================================ # https://beta.ruff.rs name: python on: #push: # branches: [master] #pull_request: # branches: [master] workflow_dispatch: jobs: ruff: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - run: pip install --user ruff - run: ruff --format=github --ignore=F401 --target-version=py38 . pytest: strategy: fail-fast: false matrix: python-version: ["3.8", "3.9", "3.10", "3.11"] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 with: python-version: ${{ matrix.python-version }} - run: pip install pytest pytest-cov -r requirements.txt - run: pytest --cov=webssh - run: mkdir -p coverage - uses: tj-actions/coverage-badge-py@v2 with: output: coverage/coverage.svg - uses: JamesIves/github-pages-deploy-action@v4 with: branch: coverage-badge folder: coverage ================================================ FILE: .gitignore ================================================ # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] # C extensions *.so # Distribution / packaging .Python env/ build/ develop-eggs/ dist/ eggs/ lib/ lib64/ parts/ sdist/ var/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .cache .pytest_cache/ nosetests.xml coverage.xml # Translations *.mo *.pot # Django stuff: *.log # Sphinx documentation docs/_build/ # PyBuilder target/ # database file *.sqlite *.sqlite3 *.db # temporary file *.swp # known_hosts file known_hosts ================================================ FILE: Dockerfile ================================================ FROM python:3-alpine LABEL maintainer='' LABEL version='0.0.0-dev.0-build.0' ADD . /code WORKDIR /code RUN \ apk add --no-cache libc-dev libffi-dev gcc && \ pip install -r requirements.txt --no-cache-dir && \ apk del gcc libc-dev libffi-dev && \ addgroup webssh && \ adduser -Ss /bin/false -g webssh webssh && \ chown -R webssh:webssh /code EXPOSE 8888/tcp USER webssh CMD ["python", "run.py", "--delay=10", "--encoding=utf-8", "--fbidhttp=False", "--maxconn=20", "--origin=*", "--policy=warning", "--redirect=False", "--timeout=10", "--debug", "--xsrf=False", "--xheaders", "--wpintvl=1"] ================================================ FILE: LICENSE ================================================ The MIT License (MIT) Copyright (c) 2017 Shengdun Hua Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: MANIFEST.in ================================================ include LICENSE recursive-include tests * prune tests/__pycache__ prune tests/.pytest_cache recursive-include webssh * prune webssh/__pycache__ prune webssh/.pytest_cache global-exclude *.pyc global-exclude *.log global-exclude .coverage ================================================ FILE: README.md ================================================ # WebSSH ![webssh](./Picture1.gif) 为你的SSH连接需求提供安全便捷的管理方案 ## ✨ 项目简介 WebSSH 是一个基于 Web 的轻量级 SSH 管理工具,方便地在浏览器中进行安全的远程服务器管理。 ## 🚀 一键云部署 [![Run on CLAWCLOUD](https://raw.githubusercontent.com/ClawCloud/Run-Template/refs/heads/main/Run-on-ClawCloud.svg)](https://template.run.claw.cloud/?openapp=system-fastdeploy%3FtemplateName%3Dwebssh) [![Deploy to Koyeb](https://www.koyeb.com/static/images/deploy/button.svg)](https://app.koyeb.com/deploy?type=docker&name=webssh&ports=8888;http;/&image=docker.io/cmliu/webssh) ## 🐳 Docker 一键部署 ```shell docker run -d --name webssh --restart always -p 8888:8888 cmliu/webssh:latest ``` ## ⚙️ Docker `compose.yml` 部署 ```yml version: '3' services: webssh: container_name: webssh image: cmliu/webssh:latest ports: - "8888:8888" restart: always network_mode: bridge ``` ## 🏗️ 手动部署 在克隆代码后,通过安装依赖并运行脚本即可快速启动项目: ```shell git clone https://github.com/cmliu/webssh cd webssh pip install -r requirements.txt && python run.py --delay=10 --encoding=utf-8 --fbidhttp=False --maxconn=20 --origin='*' --policy=warning --redirect=False --timeout=10 --port=8888 --debug --xsrf=False --xheaders --wpintvl=1 ``` ## 💡 工作原理 WebSSH 通过 WebSocket 与浏览器进行实时交互,并将请求转发给基于 Tornado 与 Paramiko 的后端,实现对 SSH 服务器的安全连接和交互。流程如下所示: ``` +---------+ http +--------+ ssh +-----------+ | browser | <==========> | webssh | <=======> | ssh server| +---------+ websocket +--------+ ssh +-----------+ ``` 这使得用户无需本地安装 SSH 客户端,即可通过网页方便快速地完成服务器管理操作。 ## 🛠️ 更多资料 - [部署到容器的教程](https://zelikk.blogspot.com/2023/10/huashengdun-webssh-codesandbox.html) - [部署到Hugging Face的教程 / 作者 Xiang xjfkkk](https://linux.do/t/topic/135264) - [部署到 Serv00 教程 / 作者 Xiang xjfkkk](https://linux.do/t/topic/211113) # 🙏 致谢 [huashengdun](https://github.com/huashengdun/webssh)、[crazypeace](https://github.com/crazypeace/huashengdun-webssh)、[Mingyu](https://github.com/ymyuuu)、[ClawCloud](https://console.run.claw.cloud/signin?link=1DFUAGF6JA6R) ================================================ FILE: README.rst ================================================ WebSSH ------ |Build Status| |codecov| |PyPI - Python Version| |PyPI| Introduction ~~~~~~~~~~~~ A simple web application to be used as an ssh client to connect to your ssh servers. It is written in Python, base on tornado, paramiko and xterm.js. Features ~~~~~~~~ - SSH password authentication supported, including empty password. - SSH public-key authentication supported, including DSA RSA ECDSA Ed25519 keys. - Encrypted keys supported. - Two-Factor Authentication (time-based one-time password) supported. - Fullscreen terminal supported. - Terminal window resizable. - Auto detect the ssh server's default encoding. - Modern browsers including Chrome, Firefox, Safari, Edge, Opera supported. Preview ~~~~~~~ |Login| |Terminal| How it works ~~~~~~~~~~~~ :: +---------+ http +--------+ ssh +-----------+ | browser | <==========> | webssh | <=======> | ssh server| +---------+ websocket +--------+ ssh +-----------+ Requirements ~~~~~~~~~~~~ - Python 3.8+ Quickstart ~~~~~~~~~~ 1. Install this app, run command ``pip install webssh`` 2. Start a webserver, run command ``wssh`` 3. Open your browser, navigate to ``127.0.0.1:8888`` 4. Input your data, submit the form. Server options ~~~~~~~~~~~~~~ .. code:: bash # start a http server with specified listen address and listen port wssh --address='2.2.2.2' --port=8000 # start a https server, certfile and keyfile must be passed wssh --certfile='/path/to/cert.crt' --keyfile='/path/to/cert.key' # missing host key policy wssh --policy=reject # logging level wssh --logging=debug # log to file wssh --log-file-prefix=main.log # more options wssh --help Browser console ~~~~~~~~~~~~~~~ .. code:: javascript // connect to your ssh server wssh.connect(hostname, port, username, password, privatekey, passphrase, totp); // pass an object to wssh.connect var opts = { hostname: 'hostname', port: 'port', username: 'username', password: 'password', privatekey: 'the private key text', passphrase: 'passphrase', totp: 'totp' }; wssh.connect(opts); // without an argument, wssh will use the form data to connect wssh.connect(); // set a new encoding for client to use wssh.set_encoding(encoding); // reset encoding to use the default one wssh.reset_encoding(); // send a command to the server wssh.send('ls -l'); Custom Font ~~~~~~~~~~~ To use custom font, put your font file in the directory ``webssh/static/css/fonts/`` and restart the server. URL Arguments ~~~~~~~~~~~~~ Support passing arguments by url (query or fragment) like following examples: Passing form data (password must be encoded in base64, privatekey not supported) .. code:: bash http://localhost:8888/?hostname=xx&username=yy&password=str_base64_encoded Passing a terminal background color .. code:: bash http://localhost:8888/#bgcolor=green Passing a user defined title .. code:: bash http://localhost:8888/?title=my-ssh-server Passing an encoding .. code:: bash http://localhost:8888/#encoding=gbk Passing a command executed right after login .. code:: bash http://localhost:8888/?command=pwd Passing a terminal type .. code:: bash http://localhost:8888/?term=xterm-256color Use Docker ~~~~~~~~~~ Start up the app :: docker-compose up Tear down the app :: docker-compose down Tests ~~~~~ Requirements :: pip install pytest pytest-cov codecov flake8 mock Use unittest to run all tests :: python -m unittest discover tests Use pytest to run all tests :: python -m pytest tests Deployment ~~~~~~~~~~ Running behind an Nginx server .. code:: bash wssh --address='127.0.0.1' --port=8888 --policy=reject .. code:: nginx # Nginx config example location / { proxy_pass http://127.0.0.1:8888; proxy_http_version 1.1; proxy_read_timeout 300; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_set_header Host $http_host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Real-PORT $remote_port; } Running as a standalone server .. code:: bash wssh --port=8080 --sslport=4433 --certfile='cert.crt' --keyfile='cert.key' --xheaders=False --policy=reject Tips ~~~~ - For whatever deployment choice you choose, don't forget to enable SSL. - By default plain http requests from a public network will be either redirected or blocked and being redirected takes precedence over being blocked. - Try to use reject policy as the missing host key policy along with your verified known\_hosts, this will prevent man-in-the-middle attacks. The idea is that it checks the system host keys file("~/.ssh/known\_hosts") and the application host keys file("./known\_hosts") in order, if the ssh server's hostname is not found or the key is not matched, the connection will be aborted. .. |Build Status| image:: https://travis-ci.org/huashengdun/webssh.svg?branch=master :target: https://travis-ci.org/huashengdun/webssh .. |codecov| image:: https://codecov.io/gh/huashengdun/webssh/branch/master/graph/badge.svg :target: https://codecov.io/gh/huashengdun/webssh .. |PyPI - Python Version| image:: https://img.shields.io/pypi/pyversions/webssh.svg .. |PyPI| image:: https://img.shields.io/pypi/v/webssh.svg .. |Login| image:: https://github.com/huashengdun/webssh/raw/master/preview/login.png .. |Terminal| image:: https://github.com/huashengdun/webssh/raw/master/preview/terminal.png ================================================ FILE: docker-compose.yml ================================================ version: '3' services: web: build: . ports: - "8888:8888" ================================================ FILE: requirements.txt ================================================ paramiko==3.0.0 tornado==6.2.0 webssh ================================================ FILE: run.py ================================================ from webssh.main import main if __name__ == '__main__': main() ================================================ FILE: setup.cfg ================================================ [wheel] universal = 1 [metadata] license_file = LICENSE [flake8] exclude = .git,build,dist,tests, __init__.py max-line-length = 79 ================================================ FILE: setup.py ================================================ import codecs from setuptools import setup from webssh._version import __version__ as version with codecs.open('README.rst', encoding='utf-8') as f: long_description = f.read() setup( name='webssh', version=version, description='Web based ssh client', long_description=long_description, author='Shengdun Hua', author_email='webmaster0115@gmail.com', url='https://github.com/huashengdun/webssh', packages=['webssh'], entry_points=''' [console_scripts] wssh = webssh.main:main ''', license='MIT', include_package_data=True, classifiers=[ 'Programming Language :: Python', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', ], install_requires=[ 'tornado>=4.5.0', 'paramiko>=2.3.1', ], ) ================================================ FILE: tests/__init__.py ================================================ ================================================ FILE: tests/data/cert.crt ================================================ -----BEGIN CERTIFICATE----- MIIDYDCCAkigAwIBAgIJAPPORA/o2Zd4MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX aWRnaXRzIFB0eSBMdGQwHhcNMTgxMDE0MDgwNTQzWhcNMjExMDEzMDgwNTQzWjBF MQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50 ZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB CgKCAQEAvSFaffq6ExFCPN4cApRopGEqVIipAYb6Ky3VHVu4pW0tOdrdKafGGYkN GWQdsLV0AAzzxmCAPpXmmAx0m0mgtPaJp3iW8NUibkISxdEO/QJOA7y8O9iWhDdb l9ghjwPI5AwURQkDkXbcBBBzQksYDaYseL2NGDGXkKCUQQoLzV0H+SV3vCPrbOXH t50HKgKzEOGoT8LcI7BRCTXk1xTlK0b/4ylKUwKIsfNPH0a9RkukBjMFkpXG/2CV VWb89+TkMzQwhcpIVn6rUCJQW5pHVRYLACP32Zki7xPUJb9OfF7XDK54v6Cwo3Fi aZWxN6rYhnn8wRTufY3PYzv5f3XiZwIDAQABo1MwUTAdBgNVHQ4EFgQUq0kfpU/m WQwNk3ymwm7fuVwYhJ0wHwYDVR0jBBgwFoAUq0kfpU/mWQwNk3ymwm7fuVwYhJ0w DwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAf2xudhAeOTUpNpw+ XZWLBXBKZXINd7PrUDgEG4bB0/0kYZN+T7bMJEtmv6+9t57y6jSni9sQzpbvT2tJ TrbZgwhDvyTm3mw5n5RpAB9ZK+lnMcasa5N4qSd6wmpXjkC+kcEs7oQ8PwgIf3xT /aGdoswNTWCz0W8vs8yRynLB4MKx1d20IMlDkfGu5n7wXhNK0ymcT8pa6iqEYl6X bhPVTlELl8bM/OKktFc42VXoRghLRnfl8yM/9t7HVHKfHXZrLpIdtEOvnKwtzX5r fBMs4IPa0OIPHGCcbLGT4rIbSvSaI8yOPA93G1XXbMF1VKdKyzdGjMS6aFKfbrhV lnaUOA== -----END CERTIFICATE----- ================================================ FILE: tests/data/cert.key ================================================ -----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9IVp9+roTEUI8 3hwClGikYSpUiKkBhvorLdUdW7ilbS052t0pp8YZiQ0ZZB2wtXQADPPGYIA+leaY DHSbSaC09omneJbw1SJuQhLF0Q79Ak4DvLw72JaEN1uX2CGPA8jkDBRFCQORdtwE EHNCSxgNpix4vY0YMZeQoJRBCgvNXQf5JXe8I+ts5ce3nQcqArMQ4ahPwtwjsFEJ NeTXFOUrRv/jKUpTAoix808fRr1GS6QGMwWSlcb/YJVVZvz35OQzNDCFykhWfqtQ IlBbmkdVFgsAI/fZmSLvE9Qlv058XtcMrni/oLCjcWJplbE3qtiGefzBFO59jc9j O/l/deJnAgMBAAECggEAZSwcblvbgiuvVUQzk6W0PIrFzCa20dxUoxiHcocIRWYb 1WEhAhF/xVUtLrIBt++5N/W1yh8BO3mQuzGehxth3qwrguzdQcOiAX1S8YMeE3ZS KWmjABiim+PJGXdCrHCH3IYhqbRitkPw+jOalJH7MgH8tDIh8hlFTNa5t/kZyybW uGFbqF6OFmyHSDIPvjPALzSlmd5po+EywnA5oa3sObj4n5xuaFB2l/IaF3ix38vT geo517L15cCuAa7x42i1cAGn5H/hdeO/Dw+MGk+0sXRRPooCMBzKztxpsB+7kNhk jbsVHmTkE5UG/T7Uc0PsthZNjFwouPOrQQVUFYTnwQKBgQDwBvpmc9vX4gnADa7p L2lgMVo6KccPFeFr4DIAYmwS0Vl0sB2j6nPVEBg3PatGLKGNMCIlcj+A3z6KQ+4o n7pnekRwX+2+m3OPX4Rbw8c/+E0CiRPtmYp9BISKNgPoSRGsI6s/L3wzagsDsQ3v xhKCohvfyY8JwUEPX6Hosmu/UQKBgQDJt0/ihWn0g/2uOKnXlXthxvkXFoR45sO7 lY/yoyJB+Z4yGAjJlbyra+5xnReqYyBnf34/2AoddjT45dPCaFucMInQFINdMGF1 NeVNzC6xa/7jjbgwf4kGqHsLC85Mrq3wyK5hwhMmfEPmRs6w+CRzM/Q78Bsr5P/T zEa13jFINwKBgQC50L0ieUjVDKD9s9oXnWOXWz19T4BRtl+nco1i7M67lqQJCJo5 njQD2ozUnwIrtjtuoLeeg56Ttr+krEf/3P+iQe4fjLPxXkiM0qYVoC9s311GvDXY N4gVllzA3mYR+hcbSxW0OZ+N8ecK+ZNPbug/hx3LFi+MnrYuH5upGA7/sQKBgCRk nlUQHP2wkqRMNNhgb9JEQ8yWk2/8snO1mDL+m7+reY8wJuW3zkJfRrXY0dw75izG I9EA+VI3cXc2f+4jReP4HeUczlaR1AOBpc1TeVkpUuNbPlABsocw/oIPrzjGiztV +aBJk4ruAJIbVE85ddoTFY161Gwm9MERqfBGFj4hAoGAN/ry0KC9/QkLkuPjs3uL AU3xjBJt1SMB7KZq1yt8mBo8M4q/E3ulynBK7G3f+hS2aj7OAhU4IcPRPGqjsLO1 dZTIOMeVyOAr0TAaioCCIyvf8hEjA7cXddnWBJYi3WiUpOc6J0uINoSlrAX2UXtw /Aq5PmJKn4D4a75f+ue2Sw8= -----END PRIVATE KEY----- ================================================ FILE: tests/data/fonts/.gitignore ================================================ ================================================ FILE: tests/data/fonts/fake-font ================================================ ================================================ FILE: tests/data/known_hosts_example ================================================ 192.168.1.199 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINwZGQmNFADnAAlm5uFLQTrdxqpNxHdgg4JPbB3sR2kr ================================================ FILE: tests/data/known_hosts_example2 ================================================ 192.168.1.196 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINwZGQmNFADnAAlm5uFLQTrdxqpNxHdgg4JPbB3sR2kr ================================================ FILE: tests/data/known_hosts_example3 ================================================ 192.168.1.196 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINwZGQmNFADnAAlm5uFLQTrdxqpNxHdgg4JPbB3sR2jr ================================================ FILE: tests/data/test_ed25519.key ================================================ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW QyNTUxOQAAACB69SvZKJh/9VgSL0G27b5xVYa8nethH3IERbi0YqJDXwAAAKhjwAdrY8AH awAAAAtzc2gtZWQyNTUxOQAAACB69SvZKJh/9VgSL0G27b5xVYa8nethH3IERbi0YqJDXw AAAEA9tGQi2IrprbOSbDCF+RmAHd6meNSXBUQ2ekKXm4/8xnr1K9komH/1WBIvQbbtvnFV hryd62EfcgRFuLRiokNfAAAAI2FsZXhfZ2F5bm9yQEFsZXhzLU1hY0Jvb2stQWlyLmxvY2 FsAQI= -----END OPENSSH PRIVATE KEY----- ================================================ FILE: tests/data/test_ed25519_password.key ================================================ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABDaKD4ac7 kieb+UfXaLaw68AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIOQn7fjND5ozMSV3 CvbEtIdT73hWCMRjzS/lRdUDw50xAAAAsE8kLGyYBnl9ihJNqv378y6mO3SkzrDbWXOnK6 ij0vnuTAvcqvWHAnyu6qBbplu/W2m55ZFeAItgaEcV2/V76sh/sAKlERqrLFyXylN0xoOW NU5+zU08aTlbSKGmeNUU2xE/xfJq12U9XClIRuVUkUpYANxNPbmTRpVrbD3fgXMhK97Jrb DEn8ca1IqMPiYmd/hpe5+tq3OxyRljXjCUFWTnqkp9VvUdzSTdSGZHsW9i -----END OPENSSH PRIVATE KEY----- ================================================ FILE: tests/data/test_known_hosts ================================================ [127.0.0.1]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINwZGQmNFADnAAlm5uFLQTrdxqpNxHdgg4JPbB3sR2kr ================================================ FILE: tests/data/test_new_dsa.key ================================================ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH NzAAAAgQC5Y5rQ1EN+eWQUFv/9K/DLfPgjGC0mwyqvKsKyv6RLpKLc0vi0VDj8lY0WUcuG CzdYnhIOSa9aB0buGe10gIjU2vAxkhqv1yaR+Zuj3dLDHQk6jpAAgNHciKlQSf1zho/seL 7nehYq/waXfU8/iJuXqywQgqpMLfaHOnIl/tPLGQAAABUArINMjWcrsmEgLmzf6k+sroko 5GkAAACAMQsRQjOtQGQA8/XI7vOWnEMCVntwt1Xi4RsLH5+4GpUMUcm4CvqjfFfSF4CufH pjlywFhrAC2/ouQIpGJPGToWotk7dt5zWckGX5DscMiRVON7fxdpUMn16IO6DdUctXlWa9 SY+NdfRESKoUCjgH5nlM8k7N2MwCK5phHHkoPu8AAACADgxrRWeNqX3gmZUM1qhrDO0mOH oHJFrBuvJCdQ6+S1GvjuBI0rNm225+gcaAhia9k/LGk8NwCbWG1FbpesuNaNFt/FxS9LVS qEaZoXtKuY+CUCn1BfBWF97/u0oMPwanXKIJEAhU81f5TXZM8Ui7OEIyTx1t9qgva+5/gF cL48kAAAHoLtDYCy7Q2AsAAAAHc3NoLWRzcwAAAIEAuWOa0NRDfnlkFBb//Svwy3z4Ixgt JsMqryrCsr+kS6Si3NL4tFQ4/JWNFlHLhgs3WJ4SDkmvWgdG7hntdICI1NrwMZIar9cmkf mbo93Swx0JOo6QAIDR3IipUEn9c4aP7Hi+53oWKv8Gl31PP4ibl6ssEIKqTC32hzpyJf7T yxkAAAAVAKyDTI1nK7JhIC5s3+pPrK6JKORpAAAAgDELEUIzrUBkAPP1yO7zlpxDAlZ7cL dV4uEbCx+fuBqVDFHJuAr6o3xX0heArnx6Y5csBYawAtv6LkCKRiTxk6FqLZO3bec1nJBl +Q7HDIkVTje38XaVDJ9eiDug3VHLV5VmvUmPjXX0REiqFAo4B+Z5TPJOzdjMAiuaYRx5KD 7vAAAAgA4Ma0Vnjal94JmVDNaoawztJjh6ByRawbryQnUOvktRr47gSNKzZttufoHGgIYm vZPyxpPDcAm1htRW6XrLjWjRbfxcUvS1UqhGmaF7SrmPglAp9QXwVhfe/7tKDD8Gp1yiCR AIVPNX+U12TPFIuzhCMk8dbfaoL2vuf4BXC+PJAAAAFBVcac1iVzrWVnLglRZRenUhlKLr AAAADHNoZW5nQHNlcnZlcgECAwQFBgc= -----END OPENSSH PRIVATE KEY----- ================================================ FILE: tests/data/test_new_rsa_password.key ================================================ -----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABASFMDZtr vMq0+bs9xBVRMOAAAAEAAAAAEAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQCpYgFiRc6d etTng/gKoHzfZrgsr+0dqsfVkrsTAl/w+2OsZbR6MCbcY94fEcE7WMTWSYUY2qv+35nlQn MT/8Q8Y8TTMbcQLIOaNhLQ2dFH8wn2e7+DbUT8giOOEICBjdUZx3tEH7PcFTzQ9ivHVIkb Rk8UHbj3vznvBvNEgQK+jj0ZI3+deOOFlPbnq9R3dJNgdVXAEnSt0cEfjteJQwT4PcaA2N fQvQAQtspC0EfEixvBH+yJsvjPDZwnYyejVGbGwKMdqAJJVka4QRkCJNoi5eyngDj/pzC7 OhGeqNwlG+D28Zz885HXIZ5eEKYNy9YJlff1WlWH8/+1fb9eVdGEXd2/fpzc/+r2QW88aX L3bg2o46qswi+5F/yYbw8AOPCq1P62ZbsVxxWTYvG947AvxfH9ycZoOItizLofOluBELQV 0P/0ooa0kPJpWQXuTAY7YSzo4vgw1F+O+8b1g33mWftUu6OHp7Rb2N3yRUiGVq9dVYeFhR 8ycyFPWjoNvwMAAAWAfnTLRACzZl9T9m7oZXtRn/OFKsr/Z8mKfkeTb4PQ+cFT/Bi2adNq 2JTsBhfGXAXiKLVVOBgBRmY5c+x0oWyrC1agoOEWkz1LhnKlJ2ETbmJBfDeRsMy5COQDmh Wnfj8noLzv59+MrPcIEfHSdC4Rai2JgFH54m5G5vaGR6SGbQ27E1ZPYnzzG9qrEB2UY30S 1gCs8G4ppX/clIVq0eToKAHseV7UG/FDwuaiPOvk61pyUjefj+bexggZxUOJANdB5pWfl7 BnEM3q9nD4QF74yrWZL38897Izku9l2Iupn64DMVs2+T/9WsfR7kDgJDoL2Noa/57w4ien Wt6WtKBnISmh9Bm5zbRG5fhPEMtCgrV3TAPgzj1VQ8Vy91D16CnWucqBpdDys46gUodiVZ Z6idCV6z24hHIJc7joR2mCNmqitCGcyrf4cO8tzug1DZVMeSkKSqL85oH9u/EOR/uWWNQi GAlehn8gmmlborYsLybau68EfyHSwYJ8XaLrELDfvM9L1CHDDacJ4svFa93r0y380Fek5P CqOLH4IqhpLHWWRoWSr23AjO6p0ZihrHzSveIzmuuTNr6uJmFt76jPKcpmLycCKhD8gKtk ZRjh+y5mEruTg/BJixCWhbl88rPYRSGNGjR9e91esw8Yj8BGYEvbvhkG0pQQpv937dbJuh n+CtnpvGr+8Mhw+mB2OW2c38XaAouwugLSoWV16xcwWx3z0ez0EAyeWjHev2XxjW5bigWg edmDPiYN+1I+OmG7d5NctKqNABb0qpwavL1uRJO96cC1drwucu5aTBrMRv1HlDQpsPHSRf u4FVruLE0wDaL2saowkZDJF5GoxjMdpzOpeVmjREuU3NwCrQr8t/AvDxzXl4x8BZ3jJTwe RA0yTGwSAZDzeN3KV2FLn+0K7xB+XvKqtKR5/IOlGviCt2w73nJpReAuSgMk95M/9imm5J r/AEcmkXKUT8gjPIT6B1xs44nnWvyf+CZreUZthAjYAjXn4ncKT51WX8q1dUuCKt9XQC7b pKH20WrP7BB/AoPPyaKtRbDBIy3Y9YA8KDsYoR9kC+hqIttL5IWxXwc15HzkU4fdKLQ4n1 VTfzaz5Ns2gsfsSAYdyJKZ8JkP/tHR2bFN7m1rWqfzL8hrGv+BF/+rR7/3+BDOD0aZCep6 u6mO4OD9hEuOP2rK5EVjJAoON7nYmjdfDpXRmp/p2f0Y+pA4R7CN+4xnel1gxlE7tBdQ7z Zu2O+NPToHXGLhzwUKUIqVhYb5cwdMIzaFQwyvOTyjNVMH0AqcsF2VuDWkgSqALg1CCSz3 7Vinx6/tyPYZ1kHm+j0dNijSdvHZrwsmvxPfYspzB7K+Vi5cNsOw6pQGIBgBTBIU09FqB+ MRBfNmLfVgVYsiU1jz/s/7H3J8DTNIC1XS4LRUXVlwddGSP/dXLgO6EJX3OvdduBD04HSZ wWggXDgWo1snhB8O2w6YSk6ocd801gPesebXGBWm+54oirWrpDr3E9y2RS7oaDFAMUV6rV IG/gc4rEFUNKX+0RwKJyArmYYJOhYgfoH0fEs01OKs6NzcsknXKVLPAXUaXV77nGlc4xsa G62+K3rLdaMFSWf/TFaIrl2Bma3p4tx993hsjNQewRhnrWdyEqP8CLcKq8Wc/fl4LlytWA PhjtjWxAp0RQKvjEu4Ul0SbFoiC+hbh+pWhVoQjPTXZePBWgI1M8CHX4fvcoRk0Ay1VMwx AZzHoZZl6v4arok4/nqwv5kYo7HhRbJrPBbNAJcGkE0Hnbh/4DxtcOLsSgwACTw03qavji wvu8wv0L5oQ6Q0H6LCUMQl/2eTuUt9uVtFXWRPmYolqmIKR5ZejYACI3XVyfaYJR6SuSx8 PR/8/w== -----END OPENSSH PRIVATE KEY----- ================================================ FILE: tests/data/test_rsa.key ================================================ -----BEGIN RSA PRIVATE KEY----- MIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz oWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/ d8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB gBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0 EbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon soVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H tYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU avNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA 4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g H0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv qfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV HhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc nvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7 -----END RSA PRIVATE KEY----- ================================================ FILE: tests/data/test_rsa_password.key ================================================ -----BEGIN RSA PRIVATE KEY----- Proc-Type: 4,ENCRYPTED DEK-Info: DES-EDE3-CBC,DAA422E8A5A8EFB7 +nssHGmWl91IcmGiE6DdCIqGvAP04tuLh60wLjWBvdjtF9CjztPnF57xe+6pBk7o YgF/Ry3ik9ZV9rHNcRXifDKM9crxtYlpUlkM2C0SP89sXaO0P1Q1yCnrtZUwDIKO BNV8et5X7+AGMFsy/nmv0NFMrbpoG03Dppsloecd29NTRlIXwxHRFyHxy6BdEib/ Dn0mEVbwg3dTvKrd/sODWR9hRwpDGM9nkEbUNJCh7vMwFKkIZZF8yqFvmGckuO5C HZkDJ6RkEDYrSZJAavQaiOPF5bu3cHughRfnrIKVrQuTTDiWjwX9Ny8e4p4k7dy7 rLpbPhtxUOUbpOF7T1QxljDi1Tcq3Ebk3kN/ZLPRFnDrJfyUx+m9BXmAa78Wxs/l KaS8DTkYykd3+EGOeJFjZg2bvgqil4V+5JIt/+MQ5pZ/ui7i4GcH2bvZyGAbrXzP 3LipSAdN5RG+fViLe3HUtfCx4ZAgtU78TWJrLk2FwKQGglFxKLnswp+IKZb09rZV uxmG4pPLUnH+mMYdiy5ugzj+5C8iZ0/IstpHVmO6GWROfedpJ82eMztTOtdhfMep 8Z3HwAwkDtksL7Gq9klb0Wq5+uRlBWetixddAvnmqXNzYhaANWcAF/2a2Hz06Rb0 e6pe/g0Ek5KV+6YI+D+oEblG0Sr+d4NtxtDTmIJKNVkmzlhI2s53bHp6txCb5JWJ S8mKLPBBBzaNXYd3odDvGXguuxUntWSsD11KyR6B9DXMIfWQW5dT7hp5kTMGlXWJ lD2hYab13DCCuAkwVTdpzhHYLZyxLYoSu05W6z8SAOs= -----END RSA PRIVATE KEY----- ================================================ FILE: tests/data/user_rsa_key ================================================ -----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDI7iK3d8eWYZlYloat94c5VjtFY7c/0zuGl8C7uMnZ3t6i2G99 66hEW0nCFSZkOW5F0XKEVj+EUCHvo8koYC6wiohAqWQnEwIoOoh7GSAcB8gP/qaq +adIl/Rvlby/mHakj+y05LBND6nFWHAn1y1gOFFKUXSJNRZPXSFy47gqzwIBIwKB gQCbANjz7q/pCXZLp1Hz6tYHqOvlEmjK1iabB1oqafrMpJ0eibUX/u+FMHq6StR5 M5413BaDWHokPdEJUnabfWXXR3SMlBUKrck0eAer1O8m78yxu3OEdpRk+znVo4DL guMeCdJB/qcF0kEsx+Q8HP42MZU1oCmk3PbfXNFwaHbWuwJBAOQ/ry/hLD7AqB8x DmCM82A9E59ICNNlHOhxpJoh6nrNTPCsBAEu/SmqrL8mS6gmbRKUaya5Lx1pkxj2 s/kWOokCQQDhXCcYXjjWiIfxhl6Rlgkk1vmI0l6785XSJNv4P7pXjGmShXfIzroh S8uWK3tL0GELY7+UAKDTUEVjjQdGxYSXAkEA3bo1JzKCwJ3lJZ1ebGuqmADRO6UP 40xH977aadfN1mEI6cusHmgpISl0nG5YH7BMsvaT+bs1FUH8m+hXDzoqOwJBAK3Z X/za+KV/REya2z0b+GzgWhkXUGUa/owrEBdHGriQ47osclkUgPUdNqcLmaDilAF4 1Z4PHPrI5RJIONAx+JECQQC/fChqjBgFpk6iJ+BOdSexQpgfxH/u/457W10Y43HR soS+8btbHqjQkowQ/2NTlUfWvqIlfxs6ZbFsIp/HrhZL -----END RSA PRIVATE KEY----- ================================================ FILE: tests/sshserver.py ================================================ import base64 import random import socket # import sys import threading # import traceback import paramiko from binascii import hexlify from tests.utils import make_tests_data_path # setup logging paramiko.util.log_to_file(make_tests_data_path('sshserver.log')) host_key = paramiko.RSAKey(filename=make_tests_data_path('test_rsa.key')) # host_key = paramiko.DSSKey(filename='test_dss.key') print('Read key: ' + hexlify(host_key.get_fingerprint()).decode('utf-8')) banner = u'\r\n\u6b22\u8fce\r\n' event_timeout = 5 class Server(paramiko.ServerInterface): # 'data' is the output of base64.b64encode(key) # (using the "user_rsa_key" files) data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp' b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC' b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT' b'UWT10hcuO4Ks8=') good_pub_key = paramiko.RSAKey(data=base64.decodebytes(data)) commands = [ b'$SHELL -ilc "locale charmap"', b'$SHELL -ic "locale charmap"' ] encodings = ['UTF-8', 'GBK', 'UTF-8\r\n', 'GBK\r\n'] def __init__(self, encodings=[]): self.shell_event = threading.Event() self.exec_event = threading.Event() self.cmd_to_enc = self.get_cmd2enc(encodings) self.password_verified = False self.key_verified = False def get_cmd2enc(self, encodings): n = len(self.commands) while len(encodings) < n: encodings.append(random.choice(self.encodings)) return dict(zip(self.commands, encodings[0:n])) def check_channel_request(self, kind, chanid): if kind == 'session': return paramiko.OPEN_SUCCEEDED return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED def check_auth_password(self, username, password): print('Auth attempt with username: {!r} & password: {!r}'.format(username, password)) # noqa if (username in ['robey', 'bar', 'foo']) and (password == 'foo'): return paramiko.AUTH_SUCCESSFUL return paramiko.AUTH_FAILED def check_auth_publickey(self, username, key): print('Auth attempt with username: {!r} & key: {!r}'.format(username, hexlify(key.get_fingerprint()).decode('utf-8'))) # noqa if (username in ['robey', 'keyonly']) and (key == self.good_pub_key): return paramiko.AUTH_SUCCESSFUL if username == 'pkey2fa' and key == self.good_pub_key: self.key_verified = True return paramiko.AUTH_PARTIALLY_SUCCESSFUL return paramiko.AUTH_FAILED def check_auth_interactive(self, username, submethods): if username in ['pass2fa', 'pkey2fa']: self.username = username prompt = 'Verification code: ' if self.password_verified else 'Password: ' # noqa print(username, prompt) return paramiko.InteractiveQuery('', '', prompt) return paramiko.AUTH_FAILED def check_auth_interactive_response(self, responses): if self.username in ['pass2fa', 'pkey2fa']: if not self.password_verified: if responses[0] == 'password': print('password verified') self.password_verified = True if self.username == 'pkey2fa': return self.check_auth_interactive(self.username, '') else: print('wrong password: {}'.format(responses[0])) return paramiko.AUTH_FAILED else: if responses[0] == 'passcode': print('totp verified') return paramiko.AUTH_SUCCESSFUL else: print('wrong totp: {}'.format(responses[0])) return paramiko.AUTH_FAILED else: return paramiko.AUTH_FAILED def get_allowed_auths(self, username): if username == 'keyonly': return 'publickey' if username == 'pass2fa': return 'keyboard-interactive' if username == 'pkey2fa': if not self.key_verified: return 'publickey' else: return 'keyboard-interactive' return 'password,publickey' def check_channel_exec_request(self, channel, command): if command not in self.commands: ret = False else: ret = True self.encoding = self.cmd_to_enc[command] channel.send(self.encoding) channel.shutdown(1) self.exec_event.set() return ret def check_channel_shell_request(self, channel): self.shell_event.set() return True def check_channel_pty_request(self, channel, term, width, height, pixelwidth, pixelheight, modes): return True def check_channel_window_change_request(self, channel, width, height, pixelwidth, pixelheight): channel.send('resized') return True def run_ssh_server(port=2200, running=True, encodings=[]): # now connect sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', port)) sock.listen(100) while running: client, addr = sock.accept() print('Got a connection!') t = paramiko.Transport(client) t.load_server_moduli() t.add_server_key(host_key) server = Server(encodings) try: t.start_server(server=server) except Exception as e: print(e) continue # wait for auth chan = t.accept(2) if chan is None: print('*** No channel.') continue username = t.get_username() print('{} Authenticated!'.format(username)) server.shell_event.wait(timeout=event_timeout) if not server.shell_event.is_set(): print('*** Client never asked for a shell.') continue server.exec_event.wait(timeout=event_timeout) if not server.exec_event.is_set(): print('*** Client never asked for a command.') continue # chan.send('\r\n\r\nWelcome!\r\n\r\n') print(server.encoding) try: banner_encoded = banner.encode(server.encoding) except (ValueError, LookupError): continue chan.send(banner_encoded) if username == 'bar': msg = chan.recv(1024) chan.send(msg) elif username == 'foo': lst = [] while True: msg = chan.recv(32 * 1024) lst.append(msg) if msg.endswith(b'\r\n\r\n'): break data = b''.join(lst) while data: s = chan.send(data) data = data[s:] else: chan.close() t.close() client.close() try: sock.close() except Exception: pass if __name__ == '__main__': run_ssh_server() ================================================ FILE: tests/test_app.py ================================================ import json import random import threading import tornado.websocket import tornado.gen from tornado.testing import AsyncHTTPTestCase from tornado.httpclient import HTTPError from tornado.options import options from tests.sshserver import run_ssh_server, banner, Server from tests.utils import encode_multipart_formdata, read_file, make_tests_data_path # noqa from webssh import handler from webssh.main import make_app, make_handlers from webssh.settings import ( get_app_settings, get_server_settings, max_body_size ) from webssh.utils import to_str from webssh.worker import clients try: from urllib.parse import urlencode except ImportError: from urllib import urlencode swallow_http_errors = handler.swallow_http_errors server_encodings = {e.strip() for e in Server.encodings} class TestAppBase(AsyncHTTPTestCase): def get_httpserver_options(self): return get_server_settings(options) def assert_response(self, bstr, response): if swallow_http_errors: self.assertEqual(response.code, 200) self.assertIn(bstr, response.body) else: self.assertEqual(response.code, 400) self.assertIn(b'Bad Request', response.body) def assert_status_in(self, status, data): self.assertIsNone(data['encoding']) self.assertIsNone(data['id']) self.assertIn(status, data['status']) def assert_status_equal(self, status, data): self.assertIsNone(data['encoding']) self.assertIsNone(data['id']) self.assertEqual(status, data['status']) def assert_status_none(self, data): self.assertIsNotNone(data['encoding']) self.assertIsNotNone(data['id']) self.assertIsNone(data['status']) def fetch_request(self, url, method='GET', body='', headers={}, sync=True): if not sync and url.startswith('/'): url = self.get_url(url) if isinstance(body, dict): body = urlencode(body) if not headers: headers = self.headers else: headers.update(self.headers) client = self if sync else self.get_http_client() return client.fetch(url, method=method, body=body, headers=headers) def sync_post(self, url, body, headers={}): return self.fetch_request(url, 'POST', body, headers) def async_post(self, url, body, headers={}): return self.fetch_request(url, 'POST', body, headers, sync=False) class TestAppBasic(TestAppBase): running = [True] sshserver_port = 2200 body = 'hostname=127.0.0.1&port={}&_xsrf=yummy&username=robey&password=foo'.format(sshserver_port) # noqa headers = {'Cookie': '_xsrf=yummy'} def get_app(self): self.body_dict = { 'hostname': '127.0.0.1', 'port': str(self.sshserver_port), 'username': 'robey', 'password': '', '_xsrf': 'yummy' } loop = self.io_loop options.debug = False options.policy = random.choice(['warning', 'autoadd']) options.hostfile = '' options.syshostfile = '' options.tdstream = '' options.delay = 0.1 app = make_app(make_handlers(loop, options), get_app_settings(options)) return app @classmethod def setUpClass(cls): print('='*20) t = threading.Thread( target=run_ssh_server, args=(cls.sshserver_port, cls.running) ) t.setDaemon(True) t.start() @classmethod def tearDownClass(cls): cls.running.pop() print('='*20) def test_app_with_invalid_form_for_missing_argument(self): response = self.fetch('/') self.assertEqual(response.code, 200) body = 'port=7000&username=admin&password&_xsrf=yummy' response = self.sync_post('/', body) self.assert_response(b'Missing argument hostname', response) body = 'hostname=127.0.0.1&port=7000&password&_xsrf=yummy' response = self.sync_post('/', body) self.assert_response(b'Missing argument username', response) body = 'hostname=&port=&username=&password&_xsrf=yummy' response = self.sync_post('/', body) self.assert_response(b'Missing value hostname', response) body = 'hostname=127.0.0.1&port=7000&username=&password&_xsrf=yummy' response = self.sync_post('/', body) self.assert_response(b'Missing value username', response) def test_app_with_invalid_form_for_invalid_value(self): body = 'hostname=127.0.0&port=22&username=&password&_xsrf=yummy' response = self.sync_post('/', body) self.assert_response(b'Invalid hostname', response) body = 'hostname=http://www.googe.com&port=22&username=&password&_xsrf=yummy' # noqa response = self.sync_post('/', body) self.assert_response(b'Invalid hostname', response) body = 'hostname=127.0.0.1&port=port&username=&password&_xsrf=yummy' response = self.sync_post('/', body) self.assert_response(b'Invalid port', response) body = 'hostname=127.0.0.1&port=70000&username=&password&_xsrf=yummy' response = self.sync_post('/', body) self.assert_response(b'Invalid port', response) def test_app_with_wrong_hostname_ip(self): body = 'hostname=127.0.0.2&port=2200&username=admin&_xsrf=yummy' response = self.sync_post('/', body) self.assertEqual(response.code, 200) self.assertIn(b'Unable to connect to', response.body) def test_app_with_wrong_hostname_domain(self): body = 'hostname=xxxxxxxxxxxx&port=2200&username=admin&_xsrf=yummy' response = self.sync_post('/', body) self.assertEqual(response.code, 200) self.assertIn(b'Unable to connect to', response.body) def test_app_with_wrong_port(self): body = 'hostname=127.0.0.1&port=7000&username=admin&_xsrf=yummy' response = self.sync_post('/', body) self.assertEqual(response.code, 200) self.assertIn(b'Unable to connect to', response.body) def test_app_with_wrong_credentials(self): response = self.sync_post('/', self.body + 's') self.assert_status_in('Authentication failed.', json.loads(to_str(response.body))) # noqa def test_app_with_correct_credentials(self): response = self.sync_post('/', self.body) self.assert_status_none(json.loads(to_str(response.body))) def test_app_with_correct_credentials_but_with_no_port(self): default_port = handler.DEFAULT_PORT handler.DEFAULT_PORT = self.sshserver_port # with no port value body = self.body.replace(str(self.sshserver_port), '') response = self.sync_post('/', body) self.assert_status_none(json.loads(to_str(response.body))) # with no port argument body = body.replace('port=&', '') response = self.sync_post('/', body) self.assert_status_none(json.loads(to_str(response.body))) handler.DEFAULT_PORT = default_port @tornado.testing.gen_test def test_app_with_correct_credentials_timeout(self): url = self.get_url('/') response = yield self.async_post(url, self.body) data = json.loads(to_str(response.body)) self.assert_status_none(data) url = url.replace('http', 'ws') ws_url = url + 'ws?id=' + data['id'] yield tornado.gen.sleep(options.delay + 0.1) ws = yield tornado.websocket.websocket_connect(ws_url) msg = yield ws.read_message() self.assertIsNone(msg) self.assertEqual(ws.close_reason, 'Websocket authentication failed.') @tornado.testing.gen_test def test_app_with_correct_credentials_but_ip_not_matched(self): url = self.get_url('/') response = yield self.async_post(url, self.body) data = json.loads(to_str(response.body)) self.assert_status_none(data) clients = handler.clients handler.clients = {} url = url.replace('http', 'ws') ws_url = url + 'ws?id=' + data['id'] ws = yield tornado.websocket.websocket_connect(ws_url) msg = yield ws.read_message() self.assertIsNone(msg) self.assertEqual(ws.close_reason, 'Websocket authentication failed.') handler.clients = clients @tornado.testing.gen_test def test_app_with_correct_credentials_user_robey(self): url = self.get_url('/') response = yield self.async_post(url, self.body) data = json.loads(to_str(response.body)) self.assert_status_none(data) url = url.replace('http', 'ws') ws_url = url + 'ws?id=' + data['id'] ws = yield tornado.websocket.websocket_connect(ws_url) msg = yield ws.read_message() self.assertEqual(to_str(msg, data['encoding']), banner) ws.close() @tornado.testing.gen_test def test_app_with_correct_credentials_but_without_id_argument(self): url = self.get_url('/') response = yield self.async_post(url, self.body) data = json.loads(to_str(response.body)) self.assert_status_none(data) url = url.replace('http', 'ws') ws_url = url + 'ws' ws = yield tornado.websocket.websocket_connect(ws_url) msg = yield ws.read_message() self.assertIsNone(msg) self.assertIn('Missing argument id', ws.close_reason) @tornado.testing.gen_test def test_app_with_correct_credentials_but_empty_id(self): url = self.get_url('/') response = yield self.async_post(url, self.body) data = json.loads(to_str(response.body)) self.assert_status_none(data) url = url.replace('http', 'ws') ws_url = url + 'ws?id=' ws = yield tornado.websocket.websocket_connect(ws_url) msg = yield ws.read_message() self.assertIsNone(msg) self.assertIn('Missing value id', ws.close_reason) @tornado.testing.gen_test def test_app_with_correct_credentials_but_wrong_id(self): url = self.get_url('/') response = yield self.async_post(url, self.body) data = json.loads(to_str(response.body)) self.assert_status_none(data) url = url.replace('http', 'ws') ws_url = url + 'ws?id=1' + data['id'] ws = yield tornado.websocket.websocket_connect(ws_url) msg = yield ws.read_message() self.assertIsNone(msg) self.assertIn('Websocket authentication failed', ws.close_reason) @tornado.testing.gen_test def test_app_with_correct_credentials_user_bar(self): body = self.body.replace('robey', 'bar') url = self.get_url('/') response = yield self.async_post(url, body) data = json.loads(to_str(response.body)) self.assert_status_none(data) url = url.replace('http', 'ws') ws_url = url + 'ws?id=' + data['id'] ws = yield tornado.websocket.websocket_connect(ws_url) msg = yield ws.read_message() self.assertEqual(to_str(msg, data['encoding']), banner) # messages below will be ignored silently yield ws.write_message('hello') yield ws.write_message('"hello"') yield ws.write_message('[hello]') yield ws.write_message(json.dumps({'resize': []})) yield ws.write_message(json.dumps({'resize': {}})) yield ws.write_message(json.dumps({'resize': 'ab'})) yield ws.write_message(json.dumps({'resize': ['a', 'b']})) yield ws.write_message(json.dumps({'resize': {'a': 1, 'b': 2}})) yield ws.write_message(json.dumps({'resize': [100]})) yield ws.write_message(json.dumps({'resize': [100]*10})) yield ws.write_message(json.dumps({'resize': [-1, -1]})) yield ws.write_message(json.dumps({'data': [1]})) yield ws.write_message(json.dumps({'data': (1,)})) yield ws.write_message(json.dumps({'data': {'a': 2}})) yield ws.write_message(json.dumps({'data': 1})) yield ws.write_message(json.dumps({'data': 2.1})) yield ws.write_message(json.dumps({'key-non-existed': 'hello'})) # end - those just for testing webssh websocket stablity yield ws.write_message(json.dumps({'resize': [79, 23]})) msg = yield ws.read_message() self.assertEqual(b'resized', msg) yield ws.write_message(json.dumps({'data': 'bye'})) msg = yield ws.read_message() self.assertEqual(b'bye', msg) ws.close() @tornado.testing.gen_test def test_app_auth_with_valid_pubkey_by_urlencoded_form(self): url = self.get_url('/') privatekey = read_file(make_tests_data_path('user_rsa_key')) self.body_dict.update(privatekey=privatekey) response = yield self.async_post(url, self.body_dict) data = json.loads(to_str(response.body)) self.assert_status_none(data) url = url.replace('http', 'ws') ws_url = url + 'ws?id=' + data['id'] ws = yield tornado.websocket.websocket_connect(ws_url) msg = yield ws.read_message() self.assertEqual(to_str(msg, data['encoding']), banner) ws.close() @tornado.testing.gen_test def test_app_auth_with_valid_pubkey_by_multipart_form(self): url = self.get_url('/') privatekey = read_file(make_tests_data_path('user_rsa_key')) files = [('privatekey', 'user_rsa_key', privatekey)] content_type, body = encode_multipart_formdata(self.body_dict.items(), files) headers = { 'Content-Type': content_type, 'content-length': str(len(body)) } response = yield self.async_post(url, body, headers=headers) data = json.loads(to_str(response.body)) self.assert_status_none(data) url = url.replace('http', 'ws') ws_url = url + 'ws?id=' + data['id'] ws = yield tornado.websocket.websocket_connect(ws_url) msg = yield ws.read_message() self.assertEqual(to_str(msg, data['encoding']), banner) ws.close() @tornado.testing.gen_test def test_app_auth_with_invalid_pubkey_for_user_robey(self): url = self.get_url('/') privatekey = 'h' * 1024 files = [('privatekey', 'user_rsa_key', privatekey)] content_type, body = encode_multipart_formdata(self.body_dict.items(), files) headers = { 'Content-Type': content_type, 'content-length': str(len(body)) } if swallow_http_errors: response = yield self.async_post(url, body, headers=headers) self.assertIn(b'Invalid key', response.body) else: with self.assertRaises(HTTPError) as ctx: yield self.async_post(url, body, headers=headers) self.assertIn('Bad Request', ctx.exception.message) @tornado.testing.gen_test def test_app_auth_with_pubkey_exceeds_key_max_size(self): url = self.get_url('/') privatekey = 'h' * (handler.PrivateKey.max_length + 1) files = [('privatekey', 'user_rsa_key', privatekey)] content_type, body = encode_multipart_formdata(self.body_dict.items(), files) headers = { 'Content-Type': content_type, 'content-length': str(len(body)) } if swallow_http_errors: response = yield self.async_post(url, body, headers=headers) self.assertIn(b'Invalid key', response.body) else: with self.assertRaises(HTTPError) as ctx: yield self.async_post(url, body, headers=headers) self.assertIn('Bad Request', ctx.exception.message) @tornado.testing.gen_test def test_app_auth_with_pubkey_cannot_be_decoded_by_multipart_form(self): url = self.get_url('/') privatekey = 'h' * 1024 files = [('privatekey', 'user_rsa_key', privatekey)] content_type, body = encode_multipart_formdata(self.body_dict.items(), files) body = body.encode('utf-8') # added some gbk bytes to the privatekey, make it cannot be decoded body = body[:-100] + b'\xb4\xed\xce\xf3' + body[-100:] headers = { 'Content-Type': content_type, 'content-length': str(len(body)) } if swallow_http_errors: response = yield self.async_post(url, body, headers=headers) self.assertIn(b'Invalid unicode', response.body) else: with self.assertRaises(HTTPError) as ctx: yield self.async_post(url, body, headers=headers) self.assertIn('Bad Request', ctx.exception.message) def test_app_post_form_with_large_body_size_by_multipart_form(self): privatekey = 'h' * (2 * max_body_size) files = [('privatekey', 'user_rsa_key', privatekey)] content_type, body = encode_multipart_formdata(self.body_dict.items(), files) headers = { 'Content-Type': content_type, 'content-length': str(len(body)) } response = self.sync_post('/', body, headers=headers) self.assertIn(response.code, [400, 599]) def test_app_post_form_with_large_body_size_by_urlencoded_form(self): privatekey = 'h' * (2 * max_body_size) body = self.body + '&privatekey=' + privatekey response = self.sync_post('/', body) self.assertIn(response.code, [400, 599]) @tornado.testing.gen_test def test_app_with_user_keyonly_for_bad_authentication_type(self): self.body_dict.update(username='keyonly', password='foo') response = yield self.async_post('/', self.body_dict) self.assertEqual(response.code, 200) self.assert_status_in('Bad authentication type', json.loads(to_str(response.body))) # noqa @tornado.testing.gen_test def test_app_with_user_pass2fa_with_correct_passwords(self): self.body_dict.update(username='pass2fa', password='password', totp='passcode') response = yield self.async_post('/', self.body_dict) self.assertEqual(response.code, 200) data = json.loads(to_str(response.body)) self.assert_status_none(data) @tornado.testing.gen_test def test_app_with_user_pass2fa_with_wrong_pkey_correct_passwords(self): url = self.get_url('/') privatekey = read_file(make_tests_data_path('user_rsa_key')) self.body_dict.update(username='pass2fa', password='password', privatekey=privatekey, totp='passcode') response = yield self.async_post(url, self.body_dict) data = json.loads(to_str(response.body)) self.assert_status_none(data) @tornado.testing.gen_test def test_app_with_user_pkey2fa_with_correct_passwords(self): url = self.get_url('/') privatekey = read_file(make_tests_data_path('user_rsa_key')) self.body_dict.update(username='pkey2fa', password='password', privatekey=privatekey, totp='passcode') response = yield self.async_post(url, self.body_dict) data = json.loads(to_str(response.body)) self.assert_status_none(data) @tornado.testing.gen_test def test_app_with_user_pkey2fa_with_wrong_password(self): url = self.get_url('/') privatekey = read_file(make_tests_data_path('user_rsa_key')) self.body_dict.update(username='pkey2fa', password='wrongpassword', privatekey=privatekey, totp='passcode') response = yield self.async_post(url, self.body_dict) data = json.loads(to_str(response.body)) self.assert_status_in('Authentication failed', data) @tornado.testing.gen_test def test_app_with_user_pkey2fa_with_wrong_passcode(self): url = self.get_url('/') privatekey = read_file(make_tests_data_path('user_rsa_key')) self.body_dict.update(username='pkey2fa', password='password', privatekey=privatekey, totp='wrongpasscode') response = yield self.async_post(url, self.body_dict) data = json.loads(to_str(response.body)) self.assert_status_in('Authentication failed', data) @tornado.testing.gen_test def test_app_with_user_pkey2fa_with_empty_passcode(self): url = self.get_url('/') privatekey = read_file(make_tests_data_path('user_rsa_key')) self.body_dict.update(username='pkey2fa', password='password', privatekey=privatekey, totp='') response = yield self.async_post(url, self.body_dict) data = json.loads(to_str(response.body)) self.assert_status_in('Need a verification code', data) class OtherTestBase(TestAppBase): sshserver_port = 3300 headers = {'Cookie': '_xsrf=yummy'} debug = False policy = None xsrf = True hostfile = '' syshostfile = '' tdstream = '' maxconn = 20 origin = 'same' encodings = [] body = { 'hostname': '127.0.0.1', 'port': '', 'username': 'robey', 'password': 'foo', '_xsrf': 'yummy' } def get_app(self): self.body.update(port=str(self.sshserver_port)) loop = self.io_loop options.debug = self.debug options.xsrf = self.xsrf options.policy = self.policy if self.policy else random.choice(['warning', 'autoadd']) # noqa options.hostfile = self.hostfile options.syshostfile = self.syshostfile options.tdstream = self.tdstream options.maxconn = self.maxconn options.origin = self.origin app = make_app(make_handlers(loop, options), get_app_settings(options)) return app def setUp(self): print('='*20) self.running = True OtherTestBase.sshserver_port += 1 t = threading.Thread( target=run_ssh_server, args=(self.sshserver_port, self.running, self.encodings) ) t.setDaemon(True) t.start() super(OtherTestBase, self).setUp() def tearDown(self): self.running = False print('='*20) super(OtherTestBase, self).tearDown() class TestAppInDebugMode(OtherTestBase): debug = True def assert_response(self, bstr, response): if swallow_http_errors: self.assertEqual(response.code, 200) self.assertIn(bstr, response.body) else: self.assertEqual(response.code, 500) self.assertIn(b'Uncaught exception', response.body) def test_server_error_for_post_method(self): body = dict(self.body, error='raise') response = self.sync_post('/', body) self.assert_response(b'"status": "Internal Server Error"', response) def test_html(self): response = self.fetch('/', method='GET') self.assertIn(b'novalidate>', response.body) class TestAppWithLargeBuffer(OtherTestBase): @tornado.testing.gen_test def test_app_for_sending_message_with_large_size(self): url = self.get_url('/') response = yield self.async_post(url, dict(self.body, username='foo')) data = json.loads(to_str(response.body)) self.assert_status_none(data) url = url.replace('http', 'ws') ws_url = url + 'ws?id=' + data['id'] ws = yield tornado.websocket.websocket_connect(ws_url) msg = yield ws.read_message() self.assertEqual(to_str(msg, data['encoding']), banner) send = 'h' * (64 * 1024) + '\r\n\r\n' yield ws.write_message(json.dumps({'data': send})) lst = [] while True: msg = yield ws.read_message() lst.append(msg) if msg.endswith(b'\r\n\r\n'): break recv = b''.join(lst).decode(data['encoding']) self.assertEqual(send, recv) ws.close() class TestAppWithRejectPolicy(OtherTestBase): policy = 'reject' hostfile = make_tests_data_path('known_hosts_example') @tornado.testing.gen_test def test_app_with_hostname_not_in_hostkeys(self): response = yield self.async_post('/', self.body) data = json.loads(to_str(response.body)) message = 'Connection to {}:{} is not allowed.'.format(self.body['hostname'], self.sshserver_port) # noqa self.assertEqual(message, data['status']) class TestAppWithBadHostKey(OtherTestBase): policy = random.choice(['warning', 'autoadd', 'reject']) hostfile = make_tests_data_path('test_known_hosts') def setUp(self): self.sshserver_port = 2222 super(TestAppWithBadHostKey, self).setUp() @tornado.testing.gen_test def test_app_with_bad_host_key(self): response = yield self.async_post('/', self.body) data = json.loads(to_str(response.body)) self.assertEqual('Bad host key.', data['status']) class TestAppWithTrustedStream(OtherTestBase): tdstream = '127.0.0.2' def test_with_forbidden_get_request(self): response = self.fetch('/', method='GET') self.assertEqual(response.code, 403) self.assertIn('Forbidden', response.error.message) def test_with_forbidden_post_request(self): response = self.sync_post('/', self.body) self.assertEqual(response.code, 403) self.assertIn('Forbidden', response.error.message) def test_with_forbidden_put_request(self): response = self.fetch_request('/', method='PUT', body=self.body) self.assertEqual(response.code, 403) self.assertIn('Forbidden', response.error.message) class TestAppNotFoundHandler(OtherTestBase): custom_headers = handler.MixinHandler.custom_headers def test_with_not_found_get_request(self): response = self.fetch('/pathnotfound', method='GET') self.assertEqual(response.code, 404) self.assertEqual( response.headers['Server'], self.custom_headers['Server'] ) self.assertIn(b'404: Not Found', response.body) def test_with_not_found_post_request(self): response = self.sync_post('/pathnotfound', self.body) self.assertEqual(response.code, 404) self.assertEqual( response.headers['Server'], self.custom_headers['Server'] ) self.assertIn(b'404: Not Found', response.body) def test_with_not_found_put_request(self): response = self.fetch_request('/pathnotfound', method='PUT', body=self.body) self.assertEqual(response.code, 404) self.assertEqual( response.headers['Server'], self.custom_headers['Server'] ) self.assertIn(b'404: Not Found', response.body) class TestAppWithHeadRequest(OtherTestBase): def test_with_index_path(self): response = self.fetch('/', method='HEAD') self.assertEqual(response.code, 200) def test_with_ws_path(self): response = self.fetch('/ws', method='HEAD') self.assertEqual(response.code, 405) def test_with_not_found_path(self): response = self.fetch('/notfound', method='HEAD') self.assertEqual(response.code, 404) class TestAppWithPutRequest(OtherTestBase): xsrf = False @tornado.testing.gen_test def test_app_with_method_not_supported(self): with self.assertRaises(HTTPError) as ctx: yield self.fetch_request('/', 'PUT', self.body, sync=False) self.assertIn('Method Not Allowed', ctx.exception.message) class TestAppWithTooManyConnections(OtherTestBase): maxconn = 1 def setUp(self): clients.clear() super(TestAppWithTooManyConnections, self).setUp() @tornado.testing.gen_test def test_app_with_too_many_connections(self): clients['127.0.0.1'] = {'fake_worker_id': None} url = self.get_url('/') response = yield self.async_post(url, self.body) data = json.loads(to_str(response.body)) self.assertEqual('Too many live connections.', data['status']) clients['127.0.0.1'].clear() response = yield self.async_post(url, self.body) self.assert_status_none(json.loads(to_str(response.body))) class TestAppWithCrossOriginOperation(OtherTestBase): origin = 'http://www.example.com' @tornado.testing.gen_test def test_app_with_wrong_event_origin(self): body = dict(self.body, _origin='localhost') response = yield self.async_post('/', body) self.assert_status_equal('Cross origin operation is not allowed.', json.loads(to_str(response.body))) # noqa @tornado.testing.gen_test def test_app_with_wrong_header_origin(self): headers = dict(Origin='localhost') response = yield self.async_post('/', self.body, headers=headers) self.assert_status_equal('Cross origin operation is not allowed.', json.loads(to_str(response.body)), ) # noqa @tornado.testing.gen_test def test_app_with_correct_event_origin(self): body = dict(self.body, _origin=self.origin) response = yield self.async_post('/', body) self.assert_status_none(json.loads(to_str(response.body))) self.assertIsNone(response.headers.get('Access-Control-Allow-Origin')) @tornado.testing.gen_test def test_app_with_correct_header_origin(self): headers = dict(Origin=self.origin) response = yield self.async_post('/', self.body, headers=headers) self.assert_status_none(json.loads(to_str(response.body))) self.assertEqual( response.headers.get('Access-Control-Allow-Origin'), self.origin ) class TestAppWithBadEncoding(OtherTestBase): encodings = [u'\u7f16\u7801'] @tornado.testing.gen_test def test_app_with_a_bad_encoding(self): response = yield self.async_post('/', self.body) dic = json.loads(to_str(response.body)) self.assert_status_none(dic) self.assertIn(dic['encoding'], server_encodings) class TestAppWithUnknownEncoding(OtherTestBase): encodings = [u'\u7f16\u7801', u'UnknownEncoding'] @tornado.testing.gen_test def test_app_with_a_unknown_encoding(self): response = yield self.async_post('/', self.body) self.assert_status_none(json.loads(to_str(response.body))) dic = json.loads(to_str(response.body)) self.assert_status_none(dic) self.assertEqual(dic['encoding'], 'utf-8') ================================================ FILE: tests/test_handler.py ================================================ import io import unittest import paramiko from tornado.httputil import HTTPServerRequest from tornado.options import options from tests.utils import read_file, make_tests_data_path from webssh import handler from webssh import worker from webssh.handler import ( IndexHandler, MixinHandler, WsockHandler, PrivateKey, InvalidValueError, SSHClient ) try: from unittest.mock import Mock except ImportError: from mock import Mock class TestMixinHandler(unittest.TestCase): def test_is_forbidden(self): mhandler = MixinHandler() handler.redirecting = True options.fbidhttp = True context = Mock( address=('8.8.8.8', 8888), trusted_downstream=['127.0.0.1'], _orig_protocol='http' ) hostname = '4.4.4.4' self.assertTrue(mhandler.is_forbidden(context, hostname)) context = Mock( address=('8.8.8.8', 8888), trusted_downstream=[], _orig_protocol='http' ) hostname = 'www.google.com' self.assertEqual(mhandler.is_forbidden(context, hostname), False) context = Mock( address=('8.8.8.8', 8888), trusted_downstream=[], _orig_protocol='http' ) hostname = '4.4.4.4' self.assertTrue(mhandler.is_forbidden(context, hostname)) context = Mock( address=('192.168.1.1', 8888), trusted_downstream=[], _orig_protocol='http' ) hostname = 'www.google.com' self.assertIsNone(mhandler.is_forbidden(context, hostname)) options.fbidhttp = False self.assertIsNone(mhandler.is_forbidden(context, hostname)) hostname = '4.4.4.4' self.assertIsNone(mhandler.is_forbidden(context, hostname)) handler.redirecting = False self.assertIsNone(mhandler.is_forbidden(context, hostname)) context._orig_protocol = 'https' self.assertIsNone(mhandler.is_forbidden(context, hostname)) def test_get_redirect_url(self): mhandler = MixinHandler() hostname = 'www.example.com' uri = '/' port = 443 self.assertEqual( mhandler.get_redirect_url(hostname, port, uri=uri), 'https://www.example.com/' ) port = 4433 self.assertEqual( mhandler.get_redirect_url(hostname, port, uri), 'https://www.example.com:4433/' ) def test_get_client_addr(self): mhandler = MixinHandler() client_addr = ('8.8.8.8', 8888) context_addr = ('127.0.0.1', 1234) options.xheaders = True mhandler.context = Mock(address=context_addr) mhandler.get_real_client_addr = lambda: None self.assertEqual(mhandler.get_client_addr(), context_addr) mhandler.context = Mock(address=context_addr) mhandler.get_real_client_addr = lambda: client_addr self.assertEqual(mhandler.get_client_addr(), client_addr) options.xheaders = False mhandler.context = Mock(address=context_addr) mhandler.get_real_client_addr = lambda: client_addr self.assertEqual(mhandler.get_client_addr(), context_addr) def test_get_real_client_addr(self): x_forwarded_for = '1.1.1.1' x_forwarded_port = 1111 x_real_ip = '2.2.2.2' x_real_port = 2222 fake_port = 65535 mhandler = MixinHandler() mhandler.request = HTTPServerRequest(uri='/') mhandler.request.remote_ip = x_forwarded_for self.assertIsNone(mhandler.get_real_client_addr()) mhandler.request.headers.add('X-Forwarded-For', x_forwarded_for) self.assertEqual(mhandler.get_real_client_addr(), (x_forwarded_for, fake_port)) mhandler.request.headers.add('X-Forwarded-Port', fake_port + 1) self.assertEqual(mhandler.get_real_client_addr(), (x_forwarded_for, fake_port)) mhandler.request.headers['X-Forwarded-Port'] = x_forwarded_port self.assertEqual(mhandler.get_real_client_addr(), (x_forwarded_for, x_forwarded_port)) mhandler.request.remote_ip = x_real_ip mhandler.request.headers.add('X-Real-Ip', x_real_ip) self.assertEqual(mhandler.get_real_client_addr(), (x_real_ip, fake_port)) mhandler.request.headers.add('X-Real-Port', fake_port + 1) self.assertEqual(mhandler.get_real_client_addr(), (x_real_ip, fake_port)) mhandler.request.headers['X-Real-Port'] = x_real_port self.assertEqual(mhandler.get_real_client_addr(), (x_real_ip, x_real_port)) class TestPrivateKey(unittest.TestCase): def get_pk_obj(self, fname, password=None): key = read_file(make_tests_data_path(fname)) return PrivateKey(key, password=password, filename=fname) def _test_with_encrypted_key(self, fname, password, klass): pk = self.get_pk_obj(fname, password='') with self.assertRaises(InvalidValueError) as ctx: pk.get_pkey_obj() self.assertIn('Need a passphrase', str(ctx.exception)) pk = self.get_pk_obj(fname, password='wrongpass') with self.assertRaises(InvalidValueError) as ctx: pk.get_pkey_obj() self.assertIn('wrong passphrase', str(ctx.exception)) pk = self.get_pk_obj(fname, password=password) self.assertIsInstance(pk.get_pkey_obj(), klass) def test_class_with_invalid_key_length(self): key = u'a' * (PrivateKey.max_length + 1) with self.assertRaises(InvalidValueError) as ctx: PrivateKey(key) self.assertIn('Invalid key length', str(ctx.exception)) def test_get_pkey_obj_with_invalid_key(self): key = u'a b c' fname = 'abc' pk = PrivateKey(key, filename=fname) with self.assertRaises(InvalidValueError) as ctx: pk.get_pkey_obj() self.assertIn('Invalid key {}'.format(fname), str(ctx.exception)) def test_get_pkey_obj_with_plain_rsa_key(self): pk = self.get_pk_obj('test_rsa.key') self.assertIsInstance(pk.get_pkey_obj(), paramiko.RSAKey) def test_get_pkey_obj_with_plain_ed25519_key(self): pk = self.get_pk_obj('test_ed25519.key') self.assertIsInstance(pk.get_pkey_obj(), paramiko.Ed25519Key) def test_get_pkey_obj_with_encrypted_rsa_key(self): fname = 'test_rsa_password.key' password = 'television' self._test_with_encrypted_key(fname, password, paramiko.RSAKey) def test_get_pkey_obj_with_encrypted_ed25519_key(self): fname = 'test_ed25519_password.key' password = 'abc123' self._test_with_encrypted_key(fname, password, paramiko.Ed25519Key) def test_get_pkey_obj_with_encrypted_new_rsa_key(self): fname = 'test_new_rsa_password.key' password = '123456' self._test_with_encrypted_key(fname, password, paramiko.RSAKey) def test_get_pkey_obj_with_plain_new_dsa_key(self): pk = self.get_pk_obj('test_new_dsa.key') self.assertIsInstance(pk.get_pkey_obj(), paramiko.DSSKey) def test_parse_name(self): key = u'-----BEGIN PRIVATE KEY-----' pk = PrivateKey(key) name, _ = pk.parse_name(pk.iostr, pk.tag_to_name) self.assertIsNone(name) key = u'-----BEGIN xxx PRIVATE KEY-----' pk = PrivateKey(key) name, _ = pk.parse_name(pk.iostr, pk.tag_to_name) self.assertIsNone(name) key = u'-----BEGIN RSA PRIVATE KEY-----' pk = PrivateKey(key) name, _ = pk.parse_name(pk.iostr, pk.tag_to_name) self.assertIsNone(name) key = u'-----BEGIN RSA PRIVATE KEY-----' pk = PrivateKey(key) name, _ = pk.parse_name(pk.iostr, pk.tag_to_name) self.assertIsNone(name) key = u'-----BEGIN RSA PRIVATE KEY-----' pk = PrivateKey(key) name, _ = pk.parse_name(pk.iostr, pk.tag_to_name) self.assertIsNone(name) for tag, to_name in PrivateKey.tag_to_name.items(): key = u'-----BEGIN {} PRIVATE KEY----- \r\n'.format(tag) pk = PrivateKey(key) name, length = pk.parse_name(pk.iostr, pk.tag_to_name) self.assertEqual(name, to_name) self.assertEqual(length, len(key)) class TestWsockHandler(unittest.TestCase): def test_check_origin(self): request = HTTPServerRequest(uri='/') obj = Mock(spec=WsockHandler, request=request) obj.origin_policy = 'same' request.headers['Host'] = 'www.example.com:4433' origin = 'https://www.example.com:4433' self.assertTrue(WsockHandler.check_origin(obj, origin)) origin = 'https://www.example.com' self.assertFalse(WsockHandler.check_origin(obj, origin)) obj.origin_policy = 'primary' self.assertTrue(WsockHandler.check_origin(obj, origin)) origin = 'https://blog.example.com' self.assertTrue(WsockHandler.check_origin(obj, origin)) origin = 'https://blog.example.org' self.assertFalse(WsockHandler.check_origin(obj, origin)) origin = 'https://blog.example.org' obj.origin_policy = {'https://blog.example.org'} self.assertTrue(WsockHandler.check_origin(obj, origin)) origin = 'http://blog.example.org' obj.origin_policy = {'http://blog.example.org'} self.assertTrue(WsockHandler.check_origin(obj, origin)) origin = 'http://blog.example.org' obj.origin_policy = {'https://blog.example.org'} self.assertFalse(WsockHandler.check_origin(obj, origin)) obj.origin_policy = '*' origin = 'https://blog.example.org' self.assertTrue(WsockHandler.check_origin(obj, origin)) def test_failed_weak_ref(self): request = HTTPServerRequest(uri='/') obj = Mock(spec=WsockHandler, request=request) obj.src_addr = ("127.0.0.1", 8888) class FakeWeakRef: def __init__(self): self.count = 0 def __call__(self): self.count += 1 return None ref = FakeWeakRef() obj.worker_ref = ref WsockHandler.on_message(obj, b'{"data": "somestuff"}') self.assertGreaterEqual(ref.count, 1) obj.close.assert_called_with(reason='No worker found') def test_worker_closed(self): request = HTTPServerRequest(uri='/') obj = Mock(spec=WsockHandler, request=request) obj.src_addr = ("127.0.0.1", 8888) class Worker: def __init__(self): self.closed = True class FakeWeakRef: def __call__(self): return Worker() ref = FakeWeakRef() obj.worker_ref = ref WsockHandler.on_message(obj, b'{"data": "somestuff"}') obj.close.assert_called_with(reason='Worker closed') class TestIndexHandler(unittest.TestCase): def test_null_in_encoding(self): handler = Mock(spec=IndexHandler) # This is a little nasty, but the index handler has a lot of # dependencies to mock. Mocking out everything but the bits # we want to test lets us test this case without needing to # refactor the relevant code out of IndexHandler def parse_encoding(data): return IndexHandler.parse_encoding(handler, data) handler.parse_encoding = parse_encoding ssh = Mock(spec=SSHClient) stdin = io.BytesIO() stdout = io.BytesIO(initial_bytes=b"UTF-8\0") stderr = io.BytesIO() ssh.exec_command.return_value = (stdin, stdout, stderr) encoding = IndexHandler.get_default_encoding(handler, ssh) self.assertEquals("utf-8", encoding) ================================================ FILE: tests/test_main.py ================================================ import unittest from tornado.web import Application from webssh import handler from webssh.main import app_listen class TestMain(unittest.TestCase): def test_app_listen(self): app = Application() app.listen = lambda x, y, **kwargs: 1 handler.redirecting = None server_settings = dict() app_listen(app, 80, '127.0.0.1', server_settings) self.assertFalse(handler.redirecting) handler.redirecting = None server_settings = dict(ssl_options='enabled') app_listen(app, 80, '127.0.0.1', server_settings) self.assertTrue(handler.redirecting) ================================================ FILE: tests/test_policy.py ================================================ import os import unittest import paramiko from shutil import copyfile from paramiko.client import RejectPolicy, WarningPolicy from tests.utils import make_tests_data_path from webssh.policy import ( AutoAddPolicy, get_policy_dictionary, load_host_keys, get_policy_class, check_policy_setting ) class TestPolicy(unittest.TestCase): def test_get_policy_dictionary(self): classes = [AutoAddPolicy, RejectPolicy, WarningPolicy] dic = get_policy_dictionary() for cls in classes: val = dic[cls.__name__.lower()] self.assertIs(cls, val) def test_load_host_keys(self): path = '/path-not-exists' host_keys = load_host_keys(path) self.assertFalse(host_keys) path = '/tmp' host_keys = load_host_keys(path) self.assertFalse(host_keys) path = make_tests_data_path('known_hosts_example') host_keys = load_host_keys(path) self.assertEqual(host_keys, paramiko.hostkeys.HostKeys(path)) def test_get_policy_class(self): keys = ['autoadd', 'reject', 'warning'] vals = [AutoAddPolicy, RejectPolicy, WarningPolicy] for key, val in zip(keys, vals): cls = get_policy_class(key) self.assertIs(cls, val) key = 'non-exists' with self.assertRaises(ValueError): get_policy_class(key) def test_check_policy_setting(self): host_keys_filename = make_tests_data_path('host_keys_test.db') host_keys_settings = dict( host_keys=paramiko.hostkeys.HostKeys(), system_host_keys=paramiko.hostkeys.HostKeys(), host_keys_filename=host_keys_filename ) with self.assertRaises(ValueError): check_policy_setting(RejectPolicy, host_keys_settings) try: os.unlink(host_keys_filename) except OSError: pass check_policy_setting(AutoAddPolicy, host_keys_settings) self.assertEqual(os.path.exists(host_keys_filename), True) def test_is_missing_host_key(self): client = paramiko.SSHClient() file1 = make_tests_data_path('known_hosts_example') file2 = make_tests_data_path('known_hosts_example2') client.load_host_keys(file1) client.load_system_host_keys(file2) autoadd = AutoAddPolicy() for f in [file1, file2]: entry = paramiko.hostkeys.HostKeys(f)._entries[0] hostname = entry.hostnames[0] key = entry.key self.assertIsNone( autoadd.is_missing_host_key(client, hostname, key) ) for f in [file1, file2]: entry = paramiko.hostkeys.HostKeys(f)._entries[0] hostname = entry.hostnames[0] key = entry.key key.get_name = lambda: 'unknown' self.assertTrue( autoadd.is_missing_host_key(client, hostname, key) ) del key.get_name for f in [file1, file2]: entry = paramiko.hostkeys.HostKeys(f)._entries[0] hostname = entry.hostnames[0][1:] key = entry.key self.assertTrue( autoadd.is_missing_host_key(client, hostname, key) ) file3 = make_tests_data_path('known_hosts_example3') entry = paramiko.hostkeys.HostKeys(file3)._entries[0] hostname = entry.hostnames[0] key = entry.key with self.assertRaises(paramiko.BadHostKeyException): autoadd.is_missing_host_key(client, hostname, key) def test_missing_host_key(self): client = paramiko.SSHClient() file1 = make_tests_data_path('known_hosts_example') file2 = make_tests_data_path('known_hosts_example2') filename = make_tests_data_path('known_hosts') copyfile(file1, filename) client.load_host_keys(filename) n1 = len(client._host_keys) autoadd = AutoAddPolicy() entry = paramiko.hostkeys.HostKeys(file2)._entries[0] hostname = entry.hostnames[0] key = entry.key autoadd.missing_host_key(client, hostname, key) self.assertEqual(len(client._host_keys), n1 + 1) self.assertEqual(paramiko.hostkeys.HostKeys(filename), client._host_keys) os.unlink(filename) ================================================ FILE: tests/test_settings.py ================================================ import io import random import ssl import sys import os.path import unittest import paramiko import tornado.options as options from tests.utils import make_tests_data_path from webssh.policy import load_host_keys from webssh.settings import ( get_host_keys_settings, get_policy_setting, base_dir, get_font_filename, get_ssl_context, get_trusted_downstream, get_origin_setting, print_version, check_encoding_setting ) from webssh.utils import UnicodeType from webssh._version import __version__ class TestSettings(unittest.TestCase): def test_print_version(self): sys_stdout = sys.stdout sys.stdout = io.StringIO() if UnicodeType == str else io.BytesIO() self.assertEqual(print_version(False), None) self.assertEqual(sys.stdout.getvalue(), '') with self.assertRaises(SystemExit): self.assertEqual(print_version(True), None) self.assertEqual(sys.stdout.getvalue(), __version__ + '\n') sys.stdout = sys_stdout def test_get_host_keys_settings(self): options.hostfile = '' options.syshostfile = '' dic = get_host_keys_settings(options) filename = os.path.join(base_dir, 'known_hosts') self.assertEqual(dic['host_keys'], load_host_keys(filename)) self.assertEqual(dic['host_keys_filename'], filename) self.assertEqual( dic['system_host_keys'], load_host_keys(os.path.expanduser('~/.ssh/known_hosts')) ) options.hostfile = make_tests_data_path('known_hosts_example') options.syshostfile = make_tests_data_path('known_hosts_example2') dic2 = get_host_keys_settings(options) self.assertEqual(dic2['host_keys'], load_host_keys(options.hostfile)) self.assertEqual(dic2['host_keys_filename'], options.hostfile) self.assertEqual(dic2['system_host_keys'], load_host_keys(options.syshostfile)) def test_get_policy_setting(self): options.policy = 'warning' options.hostfile = '' options.syshostfile = '' settings = get_host_keys_settings(options) instance = get_policy_setting(options, settings) self.assertIsInstance(instance, paramiko.client.WarningPolicy) options.policy = 'autoadd' options.hostfile = '' options.syshostfile = '' settings = get_host_keys_settings(options) instance = get_policy_setting(options, settings) self.assertIsInstance(instance, paramiko.client.AutoAddPolicy) os.unlink(settings['host_keys_filename']) options.policy = 'reject' options.hostfile = '' options.syshostfile = '' settings = get_host_keys_settings(options) try: instance = get_policy_setting(options, settings) except ValueError: self.assertFalse( settings['host_keys'] and settings['system_host_keys'] ) else: self.assertIsInstance(instance, paramiko.client.RejectPolicy) def test_get_ssl_context(self): options.certfile = '' options.keyfile = '' ssl_ctx = get_ssl_context(options) self.assertIsNone(ssl_ctx) options.certfile = 'provided' options.keyfile = '' with self.assertRaises(ValueError) as ctx: ssl_ctx = get_ssl_context(options) self.assertEqual('keyfile is not provided', str(ctx.exception)) options.certfile = '' options.keyfile = 'provided' with self.assertRaises(ValueError) as ctx: ssl_ctx = get_ssl_context(options) self.assertEqual('certfile is not provided', str(ctx.exception)) options.certfile = 'FileDoesNotExist' options.keyfile = make_tests_data_path('cert.key') with self.assertRaises(ValueError) as ctx: ssl_ctx = get_ssl_context(options) self.assertIn('does not exist', str(ctx.exception)) options.certfile = make_tests_data_path('cert.key') options.keyfile = 'FileDoesNotExist' with self.assertRaises(ValueError) as ctx: ssl_ctx = get_ssl_context(options) self.assertIn('does not exist', str(ctx.exception)) options.certfile = make_tests_data_path('cert.key') options.keyfile = make_tests_data_path('cert.key') with self.assertRaises(ssl.SSLError) as ctx: ssl_ctx = get_ssl_context(options) options.certfile = make_tests_data_path('cert.crt') options.keyfile = make_tests_data_path('cert.key') ssl_ctx = get_ssl_context(options) self.assertIsNotNone(ssl_ctx) def test_get_trusted_downstream(self): tdstream = '' result = set() self.assertEqual(get_trusted_downstream(tdstream), result) tdstream = '1.1.1.1, 2.2.2.2' result = set(['1.1.1.1', '2.2.2.2']) self.assertEqual(get_trusted_downstream(tdstream), result) tdstream = '1.1.1.1, 2.2.2.2, 2.2.2.2' result = set(['1.1.1.1', '2.2.2.2']) self.assertEqual(get_trusted_downstream(tdstream), result) tdstream = '1.1.1.1, 2.2.2.' with self.assertRaises(ValueError): get_trusted_downstream(tdstream) def test_get_origin_setting(self): options.debug = False options.origin = '*' with self.assertRaises(ValueError): get_origin_setting(options) options.debug = True self.assertEqual(get_origin_setting(options), '*') options.origin = random.choice(['Same', 'Primary']) self.assertEqual(get_origin_setting(options), options.origin.lower()) options.origin = '' with self.assertRaises(ValueError): get_origin_setting(options) options.origin = ',' with self.assertRaises(ValueError): get_origin_setting(options) options.origin = 'www.example.com, https://www.example.org' result = {'http://www.example.com', 'https://www.example.org'} self.assertEqual(get_origin_setting(options), result) options.origin = 'www.example.com:80, www.example.org:443' result = {'http://www.example.com', 'https://www.example.org'} self.assertEqual(get_origin_setting(options), result) def test_get_font_setting(self): font_dir = os.path.join(base_dir, 'tests', 'data', 'fonts') font = '' self.assertEqual(get_font_filename(font, font_dir), 'fake-font') font = 'fake-font' self.assertEqual(get_font_filename(font, font_dir), 'fake-font') font = 'wrong-name' with self.assertRaises(ValueError): get_font_filename(font, font_dir) def test_check_encoding_setting(self): self.assertIsNone(check_encoding_setting('')) self.assertIsNone(check_encoding_setting('utf-8')) with self.assertRaises(ValueError): check_encoding_setting('unknown-encoding') ================================================ FILE: tests/test_utils.py ================================================ import unittest from webssh.utils import ( is_valid_ip_address, is_valid_port, is_valid_hostname, to_str, to_bytes, to_int, is_ip_hostname, is_same_primary_domain, parse_origin_from_url ) class TestUitls(unittest.TestCase): def test_to_str(self): b = b'hello' u = u'hello' self.assertEqual(to_str(b), u) self.assertEqual(to_str(u), u) def test_to_bytes(self): b = b'hello' u = u'hello' self.assertEqual(to_bytes(b), b) self.assertEqual(to_bytes(u), b) def test_to_int(self): self.assertEqual(to_int(''), None) self.assertEqual(to_int(None), None) self.assertEqual(to_int('22'), 22) self.assertEqual(to_int(' 22 '), 22) def test_is_valid_ip_address(self): self.assertFalse(is_valid_ip_address('127.0.0')) self.assertFalse(is_valid_ip_address(b'127.0.0')) self.assertTrue(is_valid_ip_address('127.0.0.1')) self.assertTrue(is_valid_ip_address(b'127.0.0.1')) self.assertFalse(is_valid_ip_address('abc')) self.assertFalse(is_valid_ip_address(b'abc')) self.assertTrue(is_valid_ip_address('::1')) self.assertTrue(is_valid_ip_address(b'::1')) self.assertTrue(is_valid_ip_address('fe80::1111:2222:3333:4444')) self.assertTrue(is_valid_ip_address(b'fe80::1111:2222:3333:4444')) self.assertTrue(is_valid_ip_address('fe80::1111:2222:3333:4444%eth0')) self.assertTrue(is_valid_ip_address(b'fe80::1111:2222:3333:4444%eth0')) def test_is_valid_port(self): self.assertTrue(is_valid_port(80)) self.assertFalse(is_valid_port(0)) self.assertFalse(is_valid_port(65536)) def test_is_valid_hostname(self): self.assertTrue(is_valid_hostname('google.com')) self.assertTrue(is_valid_hostname('google.com.')) self.assertTrue(is_valid_hostname('www.google.com')) self.assertTrue(is_valid_hostname('www.google.com.')) self.assertFalse(is_valid_hostname('.www.google.com')) self.assertFalse(is_valid_hostname('http://www.google.com')) self.assertFalse(is_valid_hostname('https://www.google.com')) self.assertFalse(is_valid_hostname('127.0.0.1')) self.assertFalse(is_valid_hostname('::1')) def test_is_ip_hostname(self): self.assertTrue(is_ip_hostname('[::1]')) self.assertTrue(is_ip_hostname('127.0.0.1')) self.assertFalse(is_ip_hostname('localhost')) self.assertFalse(is_ip_hostname('www.google.com')) def test_is_same_primary_domain(self): domain1 = 'localhost' domain2 = 'localhost' self.assertTrue(is_same_primary_domain(domain1, domain2)) domain1 = 'localhost' domain2 = 'test' self.assertFalse(is_same_primary_domain(domain1, domain2)) domain1 = 'com' domain2 = 'example.com' self.assertFalse(is_same_primary_domain(domain1, domain2)) domain1 = 'example.com' domain2 = 'example.com' self.assertTrue(is_same_primary_domain(domain1, domain2)) domain1 = 'www.example.com' domain2 = 'example.com' self.assertTrue(is_same_primary_domain(domain1, domain2)) domain1 = 'wwwexample.com' domain2 = 'example.com' self.assertFalse(is_same_primary_domain(domain1, domain2)) domain1 = 'www.example.com' domain2 = 'www2.example.com' self.assertTrue(is_same_primary_domain(domain1, domain2)) domain1 = 'xxx.www.example.com' domain2 = 'xxx.www2.example.com' self.assertTrue(is_same_primary_domain(domain1, domain2)) def test_parse_origin_from_url(self): url = '' self.assertIsNone(parse_origin_from_url(url)) url = 'www.example.com' self.assertEqual(parse_origin_from_url(url), 'http://www.example.com') url = 'http://www.example.com' self.assertEqual(parse_origin_from_url(url), 'http://www.example.com') url = 'www.example.com:80' self.assertEqual(parse_origin_from_url(url), 'http://www.example.com') url = 'http://www.example.com:80' self.assertEqual(parse_origin_from_url(url), 'http://www.example.com') url = 'www.example.com:443' self.assertEqual(parse_origin_from_url(url), 'https://www.example.com') url = 'https://www.example.com' self.assertEqual(parse_origin_from_url(url), 'https://www.example.com') url = 'https://www.example.com:443' self.assertEqual(parse_origin_from_url(url), 'https://www.example.com') url = 'https://www.example.com:80' self.assertEqual(parse_origin_from_url(url), url) url = 'http://www.example.com:443' self.assertEqual(parse_origin_from_url(url), url) ================================================ FILE: tests/utils.py ================================================ import mimetypes import os.path from uuid import uuid4 from webssh.settings import base_dir def encode_multipart_formdata(fields, files): """ fields is a sequence of (name, value) elements for regular form fields. files is a sequence of (name, filename, value) elements for data to be uploaded as files. Return (content_type, body) ready for httplib.HTTP instance """ boundary = uuid4().hex CRLF = '\r\n' L = [] for (key, value) in fields: L.append('--' + boundary) L.append('Content-Disposition: form-data; name="%s"' % key) L.append('') L.append(value) for (key, filename, value) in files: L.append('--' + boundary) L.append( 'Content-Disposition: form-data; name="%s"; filename="%s"' % ( key, filename ) ) L.append('Content-Type: %s' % get_content_type(filename)) L.append('') L.append(value) L.append('--' + boundary + '--') L.append('') body = CRLF.join(L) content_type = 'multipart/form-data; boundary=%s' % boundary return content_type, body def get_content_type(filename): return mimetypes.guess_type(filename)[0] or 'application/octet-stream' def read_file(path, encoding='utf-8'): with open(path, 'rb') as f: data = f.read() if encoding is None: return data return data.decode(encoding) def make_tests_data_path(filename): return os.path.join(base_dir, 'tests', 'data', filename) ================================================ FILE: user.js/Build-SSH-Link.user.js ================================================ // ==UserScript== // @name Build SSH Link // @namespace http://tampermonkey.net/ // @version 0.1 // @description Build SSH link for huashengdun-webssh // @author ǝɔ∀ǝdʎz∀ɹɔ 👽 // @match https://ssh.vps.vc/* // @match https://ssh.hax.co.id/* // @match https://ssh-crazypeace.koyeb.app/* // @icon https://www.google.com/s2/favicons?sz=64&domain=koyeb.app // @grant none // ==/UserScript== (function() { 'use strict'; // Your code here... // 获取 form 元素 var form = document.getElementById("connect"); ///////////////////// // 创建 `
================================================ FILE: webssh/utils.py ================================================ import ipaddress import re try: from types import UnicodeType except ImportError: UnicodeType = str try: from urllib.parse import urlparse except ImportError: from urlparse import urlparse numeric = re.compile(r'[0-9]+$') allowed = re.compile(r'(?!-)[a-z0-9-]{1,63}(? 253: return False labels = hostname.split('.') # the TLD must be not all-numeric if numeric.match(labels[-1]): return False return all(allowed.match(label) for label in labels) def is_same_primary_domain(domain1, domain2): i = -1 dots = 0 l1 = len(domain1) l2 = len(domain2) m = min(l1, l2) while i >= -m: c1 = domain1[i] c2 = domain2[i] if c1 == c2: if c1 == '.': dots += 1 if dots == 2: return True else: return False i -= 1 if l1 == l2: return True if dots == 0: return False c = domain1[i] if l1 > m else domain2[i] return c == '.' def parse_origin_from_url(url): url = url.strip() if not url: return if not (url.startswith('http://') or url.startswith('https://') or url.startswith('//')): url = '//' + url parsed = urlparse(url) port = parsed.port scheme = parsed.scheme if scheme == '': scheme = 'https' if port == 443 else 'http' if port == 443 and scheme == 'https': netloc = parsed.netloc.replace(':443', '') elif port == 80 and scheme == 'http': netloc = parsed.netloc.replace(':80', '') else: netloc = parsed.netloc return '{}://{}'.format(scheme, netloc) ================================================ FILE: webssh/worker.py ================================================ import logging try: import secrets except ImportError: secrets = None import tornado.websocket from uuid import uuid4 from tornado.ioloop import IOLoop from tornado.iostream import _ERRNO_CONNRESET from tornado.util import errno_from_exception BUF_SIZE = 32 * 1024 clients = {} # {ip: {id: worker}} def clear_worker(worker, clients): ip = worker.src_addr[0] workers = clients.get(ip) assert worker.id in workers workers.pop(worker.id) if not workers: clients.pop(ip) if not clients: clients.clear() def recycle_worker(worker): if worker.handler: return logging.warning('Recycling worker {}'.format(worker.id)) worker.close(reason='worker recycled') class Worker(object): def __init__(self, loop, ssh, chan, dst_addr): self.loop = loop self.ssh = ssh self.chan = chan self.dst_addr = dst_addr self.fd = chan.fileno() self.id = self.gen_id() self.data_to_dst = [] self.handler = None self.mode = IOLoop.READ self.closed = False def __call__(self, fd, events): if events & IOLoop.READ: self.on_read() if events & IOLoop.WRITE: self.on_write() if events & IOLoop.ERROR: self.close(reason='error event occurred') @classmethod def gen_id(cls): return secrets.token_urlsafe(nbytes=32) if secrets else uuid4().hex def set_handler(self, handler): if not self.handler: self.handler = handler def update_handler(self, mode): if self.mode != mode: self.loop.update_handler(self.fd, mode) self.mode = mode if mode == IOLoop.WRITE: self.loop.call_later(0.1, self, self.fd, IOLoop.WRITE) def on_read(self): logging.debug('worker {} on read'.format(self.id)) try: data = self.chan.recv(BUF_SIZE) except (OSError, IOError) as e: logging.error(e) if self.chan.closed or errno_from_exception(e) in _ERRNO_CONNRESET: self.close(reason='chan error on reading') else: logging.debug('{!r} from {}:{}'.format(data, *self.dst_addr)) if not data: self.close(reason='chan closed') return logging.debug('{!r} to {}:{}'.format(data, *self.handler.src_addr)) try: self.handler.write_message(data, binary=True) except tornado.websocket.WebSocketClosedError: self.close(reason='websocket closed') def on_write(self): logging.debug('worker {} on write'.format(self.id)) if not self.data_to_dst: return data = ''.join(self.data_to_dst) logging.debug('{!r} to {}:{}'.format(data, *self.dst_addr)) try: sent = self.chan.send(data) except (OSError, IOError) as e: logging.error(e) if self.chan.closed or errno_from_exception(e) in _ERRNO_CONNRESET: self.close(reason='chan error on writing') else: self.update_handler(IOLoop.WRITE) else: self.data_to_dst = [] data = data[sent:] if data: self.data_to_dst.append(data) self.update_handler(IOLoop.WRITE) else: self.update_handler(IOLoop.READ) def close(self, reason=None): if self.closed: return self.closed = True logging.info( 'Closing worker {} with reason: {}'.format(self.id, reason) ) if self.handler: self.loop.remove_handler(self.fd) self.handler.close(reason=reason) self.chan.close() self.ssh.close() logging.info('Connection to {}:{} lost'.format(*self.dst_addr)) clear_worker(self, clients) logging.debug(clients)