[
  {
    "path": ".coveragerc",
    "content": "[run]\nbranch = true\nsource = webssh\n\n[report]\nexclude_lines =\n    if self.debug:\n    pragma: no cover\n    raise NotImplementedError\n    if __name__ == .__main__.:\nignore_errors = True\nomit =\n    tests/*\n"
  },
  {
    "path": ".dockerignore",
    "content": ".git\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "# These are supported funding model platforms\n\n# github: huashengdun\nko_fi: huashengdun\ncustom: https://bit.ly/2XmXXIP\n"
  },
  {
    "path": ".github/workflows/docker.yml",
    "content": "name: Docker Build and Push\r\n\r\non:\r\n  workflow_dispatch:\r\n\r\njobs:\r\n  docker:\r\n    runs-on: ubuntu-latest\r\n    steps:\r\n      - name: Checkout repository\r\n        uses: actions/checkout@v3\r\n      \r\n      - name: Login to Docker Hub\r\n        uses: docker/login-action@v2\r\n        with:\r\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\r\n          password: ${{ secrets.DOCKERHUB_PASSWORD }}\r\n          \r\n      - name: Set up QEMU  # 用于多平台编译\r\n        uses: docker/setup-qemu-action@v2\r\n      \r\n      - name: Set up Docker Buildx\r\n        uses: docker/setup-buildx-action@v2\r\n\r\n      - name: Build and push multi-arch Docker image\r\n        uses: docker/build-push-action@v4\r\n        with:\r\n          context: .\r\n          push: true\r\n          platforms: linux/amd64,linux/arm64\r\n          tags: cmliu/webssh:latest"
  },
  {
    "path": ".github/workflows/python.yml",
    "content": "# https://beta.ruff.rs\nname: python\non:\n  #push:\n  #  branches: [master]\n  #pull_request:\n  #  branches: [master]\n  workflow_dispatch:\njobs:\n  ruff:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - run: pip install --user ruff\n      - run: ruff --format=github --ignore=F401 --target-version=py38 .\n  pytest:\n    strategy:\n      fail-fast: false\n      matrix:\n        python-version: [\"3.8\", \"3.9\", \"3.10\", \"3.11\"]\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n      - uses: actions/setup-python@v4\n        with:\n          python-version: ${{ matrix.python-version }}\n      - run: pip install pytest pytest-cov -r requirements.txt\n      - run: pytest --cov=webssh\n      - run: mkdir -p coverage\n      - uses: tj-actions/coverage-badge-py@v2\n        with:\n          output: coverage/coverage.svg\n      - uses: JamesIves/github-pages-deploy-action@v4\n        with:\n          branch: coverage-badge\n          folder: coverage\n"
  },
  {
    "path": ".gitignore",
    "content": "# Byte-compiled / optimized / DLL files\n__pycache__/\n*.py[cod]\n\n# C extensions\n*.so\n\n# Distribution / packaging\n.Python\nenv/\nbuild/\ndevelop-eggs/\ndist/\neggs/\nlib/\nlib64/\nparts/\nsdist/\nvar/\n*.egg-info/\n.installed.cfg\n*.egg\n\n# PyInstaller\n#  Usually these files are written by a python script from a template \n#  before PyInstaller builds the exe, so as to inject date/other infos into it.\n*.manifest\n*.spec\n\n# Installer logs\npip-log.txt\npip-delete-this-directory.txt\n\n# Unit test / coverage reports\nhtmlcov/\n.tox/\n.coverage\n.cache\n.pytest_cache/\nnosetests.xml\ncoverage.xml\n\n# Translations\n*.mo\n*.pot\n\n# Django stuff:\n*.log\n\n# Sphinx documentation\ndocs/_build/\n\n# PyBuilder\ntarget/\n\n# database file\n*.sqlite\n*.sqlite3\n*.db\n\n# temporary file\n*.swp\n\n# known_hosts file\nknown_hosts\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM python:3-alpine\n\nLABEL maintainer='<author>'\nLABEL version='0.0.0-dev.0-build.0'\n\nADD . /code\nWORKDIR /code\nRUN \\\n  apk add --no-cache libc-dev libffi-dev gcc && \\\n  pip install -r requirements.txt --no-cache-dir && \\\n  apk del gcc libc-dev libffi-dev && \\\n  addgroup webssh && \\\n  adduser -Ss /bin/false -g webssh webssh && \\\n  chown -R webssh:webssh /code\n\nEXPOSE 8888/tcp\nUSER webssh\nCMD [\"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\"]"
  },
  {
    "path": "LICENSE",
    "content": "The MIT License (MIT)\n\nCopyright (c) 2017 Shengdun Hua\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "MANIFEST.in",
    "content": "include LICENSE\n\nrecursive-include tests *\nprune tests/__pycache__\nprune tests/.pytest_cache\n\nrecursive-include webssh *\nprune webssh/__pycache__\nprune webssh/.pytest_cache\n\nglobal-exclude *.pyc\nglobal-exclude *.log\nglobal-exclude .coverage\n"
  },
  {
    "path": "README.md",
    "content": "# WebSSH\n![webssh](./Picture1.gif)\n\n为你的SSH连接需求提供安全便捷的管理方案\n\n## ✨ 项目简介\nWebSSH 是一个基于 Web 的轻量级 SSH 管理工具，方便地在浏览器中进行安全的远程服务器管理。\n\n## 🚀 一键云部署\n[![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)\n[![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)\n## 🐳 Docker 一键部署\n```shell\ndocker run -d --name webssh --restart always -p 8888:8888 cmliu/webssh:latest\n```\n\n## ⚙️ Docker `compose.yml` 部署\n```yml\nversion: '3'\nservices:\n  webssh:\n    container_name: webssh\n    image: cmliu/webssh:latest\n    ports:\n    - \"8888:8888\"\n    restart: always\n    network_mode: bridge\n```\n\n## 🏗️ 手动部署\n在克隆代码后，通过安装依赖并运行脚本即可快速启动项目：\n\n```shell\ngit clone https://github.com/cmliu/webssh\ncd webssh\npip 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\n```\n\n## 💡 工作原理\nWebSSH 通过 WebSocket 与浏览器进行实时交互，并将请求转发给基于 Tornado 与 Paramiko 的后端，实现对 SSH 服务器的安全连接和交互。流程如下所示：\n```\n+---------+     http     +--------+    ssh    +-----------+\n| browser | <==========> | webssh | <=======> | ssh server|\n+---------+   websocket  +--------+    ssh    +-----------+\n```\n这使得用户无需本地安装 SSH 客户端，即可通过网页方便快速地完成服务器管理操作。\n\n## 🛠️ 更多资料\n- [部署到容器的教程](https://zelikk.blogspot.com/2023/10/huashengdun-webssh-codesandbox.html)\n- [部署到Hugging Face的教程 / 作者 Xiang xjfkkk](https://linux.do/t/topic/135264)\n- [部署到 Serv00 教程 / 作者 Xiang xjfkkk](https://linux.do/t/topic/211113)\n\n# 🙏 致谢\n[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)"
  },
  {
    "path": "README.rst",
    "content": "WebSSH\n------\n\n|Build Status| |codecov| |PyPI - Python Version| |PyPI|\n\nIntroduction\n~~~~~~~~~~~~\n\nA simple web application to be used as an ssh client to connect to your\nssh servers. It is written in Python, base on tornado, paramiko and\nxterm.js.\n\nFeatures\n~~~~~~~~\n\n-  SSH password authentication supported, including empty password.\n-  SSH public-key authentication supported, including DSA RSA ECDSA\n   Ed25519 keys.\n-  Encrypted keys supported.\n-  Two-Factor Authentication (time-based one-time password) supported.\n-  Fullscreen terminal supported.\n-  Terminal window resizable.\n-  Auto detect the ssh server's default encoding.\n-  Modern browsers including Chrome, Firefox, Safari, Edge, Opera\n   supported.\n\nPreview\n~~~~~~~\n\n|Login| |Terminal|\n\nHow it works\n~~~~~~~~~~~~\n\n::\n\n    +---------+     http     +--------+    ssh    +-----------+\n    | browser | <==========> | webssh | <=======> | ssh server|\n    +---------+   websocket  +--------+    ssh    +-----------+\n\nRequirements\n~~~~~~~~~~~~\n\n-  Python 3.8+\n\nQuickstart\n~~~~~~~~~~\n\n1. Install this app, run command ``pip install webssh``\n2. Start a webserver, run command ``wssh``\n3. Open your browser, navigate to ``127.0.0.1:8888``\n4. Input your data, submit the form.\n\nServer options\n~~~~~~~~~~~~~~\n\n.. code:: bash\n\n    # start a http server with specified listen address and listen port\n    wssh --address='2.2.2.2' --port=8000\n\n    # start a https server, certfile and keyfile must be passed\n    wssh --certfile='/path/to/cert.crt' --keyfile='/path/to/cert.key'\n\n    # missing host key policy\n    wssh --policy=reject\n\n    # logging level\n    wssh --logging=debug\n\n    # log to file\n    wssh --log-file-prefix=main.log\n\n    # more options\n    wssh --help\n\nBrowser console\n~~~~~~~~~~~~~~~\n\n.. code:: javascript\n\n    // connect to your ssh server\n    wssh.connect(hostname, port, username, password, privatekey, passphrase, totp);\n\n    // pass an object to wssh.connect\n    var opts = {\n      hostname: 'hostname',\n      port: 'port',\n      username: 'username',\n      password: 'password',\n      privatekey: 'the private key text',\n      passphrase: 'passphrase',\n      totp: 'totp'\n    };\n    wssh.connect(opts);\n\n    // without an argument, wssh will use the form data to connect\n    wssh.connect();\n\n    // set a new encoding for client to use\n    wssh.set_encoding(encoding);\n\n    // reset encoding to use the default one\n    wssh.reset_encoding();\n\n    // send a command to the server\n    wssh.send('ls -l');\n\nCustom Font\n~~~~~~~~~~~\n\nTo use custom font, put your font file in the directory\n``webssh/static/css/fonts/`` and restart the server.\n\nURL Arguments\n~~~~~~~~~~~~~\n\nSupport passing arguments by url (query or fragment) like following\nexamples:\n\nPassing form data (password must be encoded in base64, privatekey not\nsupported)\n\n.. code:: bash\n\n    http://localhost:8888/?hostname=xx&username=yy&password=str_base64_encoded\n\nPassing a terminal background color\n\n.. code:: bash\n\n    http://localhost:8888/#bgcolor=green\n\nPassing a user defined title\n\n.. code:: bash\n\n    http://localhost:8888/?title=my-ssh-server\n\nPassing an encoding\n\n.. code:: bash\n\n    http://localhost:8888/#encoding=gbk\n\nPassing a command executed right after login\n\n.. code:: bash\n\n    http://localhost:8888/?command=pwd\n\nPassing a terminal type\n\n.. code:: bash\n\n    http://localhost:8888/?term=xterm-256color\n\nUse Docker\n~~~~~~~~~~\n\nStart up the app\n\n::\n\n    docker-compose up\n\nTear down the app\n\n::\n\n    docker-compose down\n\nTests\n~~~~~\n\nRequirements\n\n::\n\n    pip install pytest pytest-cov codecov flake8 mock\n\nUse unittest to run all tests\n\n::\n\n    python -m unittest discover tests\n\nUse pytest to run all tests\n\n::\n\n    python -m pytest tests\n\nDeployment\n~~~~~~~~~~\n\nRunning behind an Nginx server\n\n.. code:: bash\n\n    wssh --address='127.0.0.1' --port=8888 --policy=reject\n\n.. code:: nginx\n\n    # Nginx config example\n    location / {\n        proxy_pass http://127.0.0.1:8888;\n        proxy_http_version 1.1;\n        proxy_read_timeout 300;\n        proxy_set_header Upgrade $http_upgrade;\n        proxy_set_header Connection \"upgrade\";\n        proxy_set_header Host $http_host;\n        proxy_set_header X-Real-IP $remote_addr;\n        proxy_set_header X-Real-PORT $remote_port;\n    }\n\nRunning as a standalone server\n\n.. code:: bash\n\n    wssh --port=8080 --sslport=4433 --certfile='cert.crt' --keyfile='cert.key' --xheaders=False --policy=reject\n\nTips\n~~~~\n\n-  For whatever deployment choice you choose, don't forget to enable\n   SSL.\n-  By default plain http requests from a public network will be either\n   redirected or blocked and being redirected takes precedence over\n   being blocked.\n-  Try to use reject policy as the missing host key policy along with\n   your verified known\\_hosts, this will prevent man-in-the-middle\n   attacks. The idea is that it checks the system host keys\n   file(\"~/.ssh/known\\_hosts\") and the application host keys\n   file(\"./known\\_hosts\") in order, if the ssh server's hostname is not\n   found or the key is not matched, the connection will be aborted.\n\n.. |Build Status| image:: https://travis-ci.org/huashengdun/webssh.svg?branch=master\n   :target: https://travis-ci.org/huashengdun/webssh\n.. |codecov| image:: https://codecov.io/gh/huashengdun/webssh/branch/master/graph/badge.svg\n   :target: https://codecov.io/gh/huashengdun/webssh\n.. |PyPI - Python Version| image:: https://img.shields.io/pypi/pyversions/webssh.svg\n.. |PyPI| image:: https://img.shields.io/pypi/v/webssh.svg\n.. |Login| image:: https://github.com/huashengdun/webssh/raw/master/preview/login.png\n.. |Terminal| image:: https://github.com/huashengdun/webssh/raw/master/preview/terminal.png\n\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\nservices:\n  web:\n    build: .\n    ports:\n    - \"8888:8888\"\n"
  },
  {
    "path": "requirements.txt",
    "content": "paramiko==3.0.0\ntornado==6.2.0\nwebssh\n"
  },
  {
    "path": "run.py",
    "content": "from webssh.main import main\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "setup.cfg",
    "content": "[wheel]\nuniversal = 1\n\n[metadata]\nlicense_file = LICENSE\n\n[flake8]\nexclude = .git,build,dist,tests, __init__.py\nmax-line-length = 79\n"
  },
  {
    "path": "setup.py",
    "content": "import codecs\nfrom setuptools import setup\nfrom webssh._version import __version__ as version\n\n\nwith codecs.open('README.rst', encoding='utf-8') as f:\n    long_description = f.read()\n\n\nsetup(\n    name='webssh',\n    version=version,\n    description='Web based ssh client',\n    long_description=long_description,\n    author='Shengdun Hua',\n    author_email='webmaster0115@gmail.com',\n    url='https://github.com/huashengdun/webssh',\n    packages=['webssh'],\n    entry_points='''\n    [console_scripts]\n    wssh = webssh.main:main\n    ''',\n    license='MIT',\n    include_package_data=True,\n    classifiers=[\n        'Programming Language :: Python',\n        'Programming Language :: Python :: 3',\n        'Programming Language :: Python :: 3.8',\n        'Programming Language :: Python :: 3.9',\n        'Programming Language :: Python :: 3.10',\n        'Programming Language :: Python :: 3.11',\n    ],\n    install_requires=[\n        'tornado>=4.5.0',\n        'paramiko>=2.3.1',\n    ],\n)\n"
  },
  {
    "path": "tests/__init__.py",
    "content": ""
  },
  {
    "path": "tests/data/cert.crt",
    "content": "-----BEGIN CERTIFICATE-----\nMIIDYDCCAkigAwIBAgIJAPPORA/o2Zd4MA0GCSqGSIb3DQEBCwUAMEUxCzAJBgNV\nBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX\naWRnaXRzIFB0eSBMdGQwHhcNMTgxMDE0MDgwNTQzWhcNMjExMDEzMDgwNTQzWjBF\nMQswCQYDVQQGEwJBVTETMBEGA1UECAwKU29tZS1TdGF0ZTEhMB8GA1UECgwYSW50\nZXJuZXQgV2lkZ2l0cyBQdHkgTHRkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB\nCgKCAQEAvSFaffq6ExFCPN4cApRopGEqVIipAYb6Ky3VHVu4pW0tOdrdKafGGYkN\nGWQdsLV0AAzzxmCAPpXmmAx0m0mgtPaJp3iW8NUibkISxdEO/QJOA7y8O9iWhDdb\nl9ghjwPI5AwURQkDkXbcBBBzQksYDaYseL2NGDGXkKCUQQoLzV0H+SV3vCPrbOXH\nt50HKgKzEOGoT8LcI7BRCTXk1xTlK0b/4ylKUwKIsfNPH0a9RkukBjMFkpXG/2CV\nVWb89+TkMzQwhcpIVn6rUCJQW5pHVRYLACP32Zki7xPUJb9OfF7XDK54v6Cwo3Fi\naZWxN6rYhnn8wRTufY3PYzv5f3XiZwIDAQABo1MwUTAdBgNVHQ4EFgQUq0kfpU/m\nWQwNk3ymwm7fuVwYhJ0wHwYDVR0jBBgwFoAUq0kfpU/mWQwNk3ymwm7fuVwYhJ0w\nDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0BAQsFAAOCAQEAf2xudhAeOTUpNpw+\nXZWLBXBKZXINd7PrUDgEG4bB0/0kYZN+T7bMJEtmv6+9t57y6jSni9sQzpbvT2tJ\nTrbZgwhDvyTm3mw5n5RpAB9ZK+lnMcasa5N4qSd6wmpXjkC+kcEs7oQ8PwgIf3xT\n/aGdoswNTWCz0W8vs8yRynLB4MKx1d20IMlDkfGu5n7wXhNK0ymcT8pa6iqEYl6X\nbhPVTlELl8bM/OKktFc42VXoRghLRnfl8yM/9t7HVHKfHXZrLpIdtEOvnKwtzX5r\nfBMs4IPa0OIPHGCcbLGT4rIbSvSaI8yOPA93G1XXbMF1VKdKyzdGjMS6aFKfbrhV\nlnaUOA==\n-----END CERTIFICATE-----\n"
  },
  {
    "path": "tests/data/cert.key",
    "content": "-----BEGIN PRIVATE KEY-----\nMIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC9IVp9+roTEUI8\n3hwClGikYSpUiKkBhvorLdUdW7ilbS052t0pp8YZiQ0ZZB2wtXQADPPGYIA+leaY\nDHSbSaC09omneJbw1SJuQhLF0Q79Ak4DvLw72JaEN1uX2CGPA8jkDBRFCQORdtwE\nEHNCSxgNpix4vY0YMZeQoJRBCgvNXQf5JXe8I+ts5ce3nQcqArMQ4ahPwtwjsFEJ\nNeTXFOUrRv/jKUpTAoix808fRr1GS6QGMwWSlcb/YJVVZvz35OQzNDCFykhWfqtQ\nIlBbmkdVFgsAI/fZmSLvE9Qlv058XtcMrni/oLCjcWJplbE3qtiGefzBFO59jc9j\nO/l/deJnAgMBAAECggEAZSwcblvbgiuvVUQzk6W0PIrFzCa20dxUoxiHcocIRWYb\n1WEhAhF/xVUtLrIBt++5N/W1yh8BO3mQuzGehxth3qwrguzdQcOiAX1S8YMeE3ZS\nKWmjABiim+PJGXdCrHCH3IYhqbRitkPw+jOalJH7MgH8tDIh8hlFTNa5t/kZyybW\nuGFbqF6OFmyHSDIPvjPALzSlmd5po+EywnA5oa3sObj4n5xuaFB2l/IaF3ix38vT\ngeo517L15cCuAa7x42i1cAGn5H/hdeO/Dw+MGk+0sXRRPooCMBzKztxpsB+7kNhk\njbsVHmTkE5UG/T7Uc0PsthZNjFwouPOrQQVUFYTnwQKBgQDwBvpmc9vX4gnADa7p\nL2lgMVo6KccPFeFr4DIAYmwS0Vl0sB2j6nPVEBg3PatGLKGNMCIlcj+A3z6KQ+4o\nn7pnekRwX+2+m3OPX4Rbw8c/+E0CiRPtmYp9BISKNgPoSRGsI6s/L3wzagsDsQ3v\nxhKCohvfyY8JwUEPX6Hosmu/UQKBgQDJt0/ihWn0g/2uOKnXlXthxvkXFoR45sO7\nlY/yoyJB+Z4yGAjJlbyra+5xnReqYyBnf34/2AoddjT45dPCaFucMInQFINdMGF1\nNeVNzC6xa/7jjbgwf4kGqHsLC85Mrq3wyK5hwhMmfEPmRs6w+CRzM/Q78Bsr5P/T\nzEa13jFINwKBgQC50L0ieUjVDKD9s9oXnWOXWz19T4BRtl+nco1i7M67lqQJCJo5\nnjQD2ozUnwIrtjtuoLeeg56Ttr+krEf/3P+iQe4fjLPxXkiM0qYVoC9s311GvDXY\nN4gVllzA3mYR+hcbSxW0OZ+N8ecK+ZNPbug/hx3LFi+MnrYuH5upGA7/sQKBgCRk\nnlUQHP2wkqRMNNhgb9JEQ8yWk2/8snO1mDL+m7+reY8wJuW3zkJfRrXY0dw75izG\nI9EA+VI3cXc2f+4jReP4HeUczlaR1AOBpc1TeVkpUuNbPlABsocw/oIPrzjGiztV\n+aBJk4ruAJIbVE85ddoTFY161Gwm9MERqfBGFj4hAoGAN/ry0KC9/QkLkuPjs3uL\nAU3xjBJt1SMB7KZq1yt8mBo8M4q/E3ulynBK7G3f+hS2aj7OAhU4IcPRPGqjsLO1\ndZTIOMeVyOAr0TAaioCCIyvf8hEjA7cXddnWBJYi3WiUpOc6J0uINoSlrAX2UXtw\n/Aq5PmJKn4D4a75f+ue2Sw8=\n-----END PRIVATE KEY-----\n"
  },
  {
    "path": "tests/data/fonts/.gitignore",
    "content": ""
  },
  {
    "path": "tests/data/fonts/fake-font",
    "content": ""
  },
  {
    "path": "tests/data/known_hosts_example",
    "content": "192.168.1.199 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINwZGQmNFADnAAlm5uFLQTrdxqpNxHdgg4JPbB3sR2kr\n"
  },
  {
    "path": "tests/data/known_hosts_example2",
    "content": "192.168.1.196 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINwZGQmNFADnAAlm5uFLQTrdxqpNxHdgg4JPbB3sR2kr\n"
  },
  {
    "path": "tests/data/known_hosts_example3",
    "content": "192.168.1.196 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINwZGQmNFADnAAlm5uFLQTrdxqpNxHdgg4JPbB3sR2jr\n"
  },
  {
    "path": "tests/data/test_ed25519.key",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACB69SvZKJh/9VgSL0G27b5xVYa8nethH3IERbi0YqJDXwAAAKhjwAdrY8AH\nawAAAAtzc2gtZWQyNTUxOQAAACB69SvZKJh/9VgSL0G27b5xVYa8nethH3IERbi0YqJDXw\nAAAEA9tGQi2IrprbOSbDCF+RmAHd6meNSXBUQ2ekKXm4/8xnr1K9komH/1WBIvQbbtvnFV\nhryd62EfcgRFuLRiokNfAAAAI2FsZXhfZ2F5bm9yQEFsZXhzLU1hY0Jvb2stQWlyLmxvY2\nFsAQI=\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "tests/data/test_ed25519_password.key",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jYmMAAAAGYmNyeXB0AAAAGAAAABDaKD4ac7\nkieb+UfXaLaw68AAAAEAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIOQn7fjND5ozMSV3\nCvbEtIdT73hWCMRjzS/lRdUDw50xAAAAsE8kLGyYBnl9ihJNqv378y6mO3SkzrDbWXOnK6\nij0vnuTAvcqvWHAnyu6qBbplu/W2m55ZFeAItgaEcV2/V76sh/sAKlERqrLFyXylN0xoOW\nNU5+zU08aTlbSKGmeNUU2xE/xfJq12U9XClIRuVUkUpYANxNPbmTRpVrbD3fgXMhK97Jrb\nDEn8ca1IqMPiYmd/hpe5+tq3OxyRljXjCUFWTnqkp9VvUdzSTdSGZHsW9i\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "tests/data/test_known_hosts",
    "content": "[127.0.0.1]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAINwZGQmNFADnAAlm5uFLQTrdxqpNxHdgg4JPbB3sR2kr\n"
  },
  {
    "path": "tests/data/test_new_dsa.key",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAABsQAAAAdzc2gtZH\nNzAAAAgQC5Y5rQ1EN+eWQUFv/9K/DLfPgjGC0mwyqvKsKyv6RLpKLc0vi0VDj8lY0WUcuG\nCzdYnhIOSa9aB0buGe10gIjU2vAxkhqv1yaR+Zuj3dLDHQk6jpAAgNHciKlQSf1zho/seL\n7nehYq/waXfU8/iJuXqywQgqpMLfaHOnIl/tPLGQAAABUArINMjWcrsmEgLmzf6k+sroko\n5GkAAACAMQsRQjOtQGQA8/XI7vOWnEMCVntwt1Xi4RsLH5+4GpUMUcm4CvqjfFfSF4CufH\npjlywFhrAC2/ouQIpGJPGToWotk7dt5zWckGX5DscMiRVON7fxdpUMn16IO6DdUctXlWa9\nSY+NdfRESKoUCjgH5nlM8k7N2MwCK5phHHkoPu8AAACADgxrRWeNqX3gmZUM1qhrDO0mOH\noHJFrBuvJCdQ6+S1GvjuBI0rNm225+gcaAhia9k/LGk8NwCbWG1FbpesuNaNFt/FxS9LVS\nqEaZoXtKuY+CUCn1BfBWF97/u0oMPwanXKIJEAhU81f5TXZM8Ui7OEIyTx1t9qgva+5/gF\ncL48kAAAHoLtDYCy7Q2AsAAAAHc3NoLWRzcwAAAIEAuWOa0NRDfnlkFBb//Svwy3z4Ixgt\nJsMqryrCsr+kS6Si3NL4tFQ4/JWNFlHLhgs3WJ4SDkmvWgdG7hntdICI1NrwMZIar9cmkf\nmbo93Swx0JOo6QAIDR3IipUEn9c4aP7Hi+53oWKv8Gl31PP4ibl6ssEIKqTC32hzpyJf7T\nyxkAAAAVAKyDTI1nK7JhIC5s3+pPrK6JKORpAAAAgDELEUIzrUBkAPP1yO7zlpxDAlZ7cL\ndV4uEbCx+fuBqVDFHJuAr6o3xX0heArnx6Y5csBYawAtv6LkCKRiTxk6FqLZO3bec1nJBl\n+Q7HDIkVTje38XaVDJ9eiDug3VHLV5VmvUmPjXX0REiqFAo4B+Z5TPJOzdjMAiuaYRx5KD\n7vAAAAgA4Ma0Vnjal94JmVDNaoawztJjh6ByRawbryQnUOvktRr47gSNKzZttufoHGgIYm\nvZPyxpPDcAm1htRW6XrLjWjRbfxcUvS1UqhGmaF7SrmPglAp9QXwVhfe/7tKDD8Gp1yiCR\nAIVPNX+U12TPFIuzhCMk8dbfaoL2vuf4BXC+PJAAAAFBVcac1iVzrWVnLglRZRenUhlKLr\nAAAADHNoZW5nQHNlcnZlcgECAwQFBgc=\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "tests/data/test_new_rsa_password.key",
    "content": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABASFMDZtr\nvMq0+bs9xBVRMOAAAAEAAAAAEAAAGXAAAAB3NzaC1yc2EAAAADAQABAAABgQCpYgFiRc6d\netTng/gKoHzfZrgsr+0dqsfVkrsTAl/w+2OsZbR6MCbcY94fEcE7WMTWSYUY2qv+35nlQn\nMT/8Q8Y8TTMbcQLIOaNhLQ2dFH8wn2e7+DbUT8giOOEICBjdUZx3tEH7PcFTzQ9ivHVIkb\nRk8UHbj3vznvBvNEgQK+jj0ZI3+deOOFlPbnq9R3dJNgdVXAEnSt0cEfjteJQwT4PcaA2N\nfQvQAQtspC0EfEixvBH+yJsvjPDZwnYyejVGbGwKMdqAJJVka4QRkCJNoi5eyngDj/pzC7\nOhGeqNwlG+D28Zz885HXIZ5eEKYNy9YJlff1WlWH8/+1fb9eVdGEXd2/fpzc/+r2QW88aX\nL3bg2o46qswi+5F/yYbw8AOPCq1P62ZbsVxxWTYvG947AvxfH9ycZoOItizLofOluBELQV\n0P/0ooa0kPJpWQXuTAY7YSzo4vgw1F+O+8b1g33mWftUu6OHp7Rb2N3yRUiGVq9dVYeFhR\n8ycyFPWjoNvwMAAAWAfnTLRACzZl9T9m7oZXtRn/OFKsr/Z8mKfkeTb4PQ+cFT/Bi2adNq\n2JTsBhfGXAXiKLVVOBgBRmY5c+x0oWyrC1agoOEWkz1LhnKlJ2ETbmJBfDeRsMy5COQDmh\nWnfj8noLzv59+MrPcIEfHSdC4Rai2JgFH54m5G5vaGR6SGbQ27E1ZPYnzzG9qrEB2UY30S\n1gCs8G4ppX/clIVq0eToKAHseV7UG/FDwuaiPOvk61pyUjefj+bexggZxUOJANdB5pWfl7\nBnEM3q9nD4QF74yrWZL38897Izku9l2Iupn64DMVs2+T/9WsfR7kDgJDoL2Noa/57w4ien\nWt6WtKBnISmh9Bm5zbRG5fhPEMtCgrV3TAPgzj1VQ8Vy91D16CnWucqBpdDys46gUodiVZ\nZ6idCV6z24hHIJc7joR2mCNmqitCGcyrf4cO8tzug1DZVMeSkKSqL85oH9u/EOR/uWWNQi\nGAlehn8gmmlborYsLybau68EfyHSwYJ8XaLrELDfvM9L1CHDDacJ4svFa93r0y380Fek5P\nCqOLH4IqhpLHWWRoWSr23AjO6p0ZihrHzSveIzmuuTNr6uJmFt76jPKcpmLycCKhD8gKtk\nZRjh+y5mEruTg/BJixCWhbl88rPYRSGNGjR9e91esw8Yj8BGYEvbvhkG0pQQpv937dbJuh\nn+CtnpvGr+8Mhw+mB2OW2c38XaAouwugLSoWV16xcwWx3z0ez0EAyeWjHev2XxjW5bigWg\nedmDPiYN+1I+OmG7d5NctKqNABb0qpwavL1uRJO96cC1drwucu5aTBrMRv1HlDQpsPHSRf\nu4FVruLE0wDaL2saowkZDJF5GoxjMdpzOpeVmjREuU3NwCrQr8t/AvDxzXl4x8BZ3jJTwe\nRA0yTGwSAZDzeN3KV2FLn+0K7xB+XvKqtKR5/IOlGviCt2w73nJpReAuSgMk95M/9imm5J\nr/AEcmkXKUT8gjPIT6B1xs44nnWvyf+CZreUZthAjYAjXn4ncKT51WX8q1dUuCKt9XQC7b\npKH20WrP7BB/AoPPyaKtRbDBIy3Y9YA8KDsYoR9kC+hqIttL5IWxXwc15HzkU4fdKLQ4n1\nVTfzaz5Ns2gsfsSAYdyJKZ8JkP/tHR2bFN7m1rWqfzL8hrGv+BF/+rR7/3+BDOD0aZCep6\nu6mO4OD9hEuOP2rK5EVjJAoON7nYmjdfDpXRmp/p2f0Y+pA4R7CN+4xnel1gxlE7tBdQ7z\nZu2O+NPToHXGLhzwUKUIqVhYb5cwdMIzaFQwyvOTyjNVMH0AqcsF2VuDWkgSqALg1CCSz3\n7Vinx6/tyPYZ1kHm+j0dNijSdvHZrwsmvxPfYspzB7K+Vi5cNsOw6pQGIBgBTBIU09FqB+\nMRBfNmLfVgVYsiU1jz/s/7H3J8DTNIC1XS4LRUXVlwddGSP/dXLgO6EJX3OvdduBD04HSZ\nwWggXDgWo1snhB8O2w6YSk6ocd801gPesebXGBWm+54oirWrpDr3E9y2RS7oaDFAMUV6rV\nIG/gc4rEFUNKX+0RwKJyArmYYJOhYgfoH0fEs01OKs6NzcsknXKVLPAXUaXV77nGlc4xsa\nG62+K3rLdaMFSWf/TFaIrl2Bma3p4tx993hsjNQewRhnrWdyEqP8CLcKq8Wc/fl4LlytWA\nPhjtjWxAp0RQKvjEu4Ul0SbFoiC+hbh+pWhVoQjPTXZePBWgI1M8CHX4fvcoRk0Ay1VMwx\nAZzHoZZl6v4arok4/nqwv5kYo7HhRbJrPBbNAJcGkE0Hnbh/4DxtcOLsSgwACTw03qavji\nwvu8wv0L5oQ6Q0H6LCUMQl/2eTuUt9uVtFXWRPmYolqmIKR5ZejYACI3XVyfaYJR6SuSx8\nPR/8/w==\n-----END OPENSSH PRIVATE KEY-----\n"
  },
  {
    "path": "tests/data/test_rsa.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIICWgIBAAKBgQDTj1bqB4WmayWNPB+8jVSYpZYk80Ujvj680pOTh2bORBjbIAyz\noWGW+GUjzKxTiiPvVmxFgx5wdsFvF03v34lEVVhMpouqPAYQ15N37K/ir5XY+9m/\nd8ufMCkjeXsQkKqFbAlQcnWMCRnOoPHS3I4vi6hmnDDeeYTSRvfLbW0fhwIBIwKB\ngBIiOqZYaoqbeD9OS9z2K9KR2atlTxGxOJPXiP4ESqP3NVScWNwyZ3NXHpyrJLa0\nEbVtzsQhLn6rF+TzXnOlcipFvjsem3iYzCpuChfGQ6SovTcOjHV9z+hnpXvQ/fon\nsoVRZY65wKnF7IAoUwTmJS9opqgrN6kRgCd3DASAMd1bAkEA96SBVWFt/fJBNJ9H\ntYnBKZGw0VeHOYmVYbvMSstssn8un+pQpUm9vlG/bp7Oxd/m+b9KWEh2xPfv6zqU\navNwHwJBANqzGZa/EpzF4J8pGti7oIAPUIDGMtfIcmqNXVMckrmzQ2vTfqtkEZsA\n4rE1IERRyiJQx6EJsz21wJmGV9WJQ5kCQQDwkS0uXqVdFzgHO6S++tjmjYcxwr3g\nH0CoFYSgbddOT6miqRskOQF3DZVkJT3kyuBgU2zKygz52ukQZMqxCb1fAkASvuTv\nqfpH87Qq5kQhNKdbbwbmd2NxlNabazPijWuphGTdW0VfJdWfklyS2Kr+iqrs/5wV\nHhathJt636Eg7oIjAkA8ht3MQ+XSl9yIJIS8gVpbPxSw5OMfw0PjVE7tBdQruiSc\nnvuQES5C9BMHjF39LZiGH1iLQy7FgdHyoP+eodI7\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "tests/data/test_rsa_password.key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nDEK-Info: DES-EDE3-CBC,DAA422E8A5A8EFB7\n\n+nssHGmWl91IcmGiE6DdCIqGvAP04tuLh60wLjWBvdjtF9CjztPnF57xe+6pBk7o\nYgF/Ry3ik9ZV9rHNcRXifDKM9crxtYlpUlkM2C0SP89sXaO0P1Q1yCnrtZUwDIKO\nBNV8et5X7+AGMFsy/nmv0NFMrbpoG03Dppsloecd29NTRlIXwxHRFyHxy6BdEib/\nDn0mEVbwg3dTvKrd/sODWR9hRwpDGM9nkEbUNJCh7vMwFKkIZZF8yqFvmGckuO5C\nHZkDJ6RkEDYrSZJAavQaiOPF5bu3cHughRfnrIKVrQuTTDiWjwX9Ny8e4p4k7dy7\nrLpbPhtxUOUbpOF7T1QxljDi1Tcq3Ebk3kN/ZLPRFnDrJfyUx+m9BXmAa78Wxs/l\nKaS8DTkYykd3+EGOeJFjZg2bvgqil4V+5JIt/+MQ5pZ/ui7i4GcH2bvZyGAbrXzP\n3LipSAdN5RG+fViLe3HUtfCx4ZAgtU78TWJrLk2FwKQGglFxKLnswp+IKZb09rZV\nuxmG4pPLUnH+mMYdiy5ugzj+5C8iZ0/IstpHVmO6GWROfedpJ82eMztTOtdhfMep\n8Z3HwAwkDtksL7Gq9klb0Wq5+uRlBWetixddAvnmqXNzYhaANWcAF/2a2Hz06Rb0\ne6pe/g0Ek5KV+6YI+D+oEblG0Sr+d4NtxtDTmIJKNVkmzlhI2s53bHp6txCb5JWJ\nS8mKLPBBBzaNXYd3odDvGXguuxUntWSsD11KyR6B9DXMIfWQW5dT7hp5kTMGlXWJ\nlD2hYab13DCCuAkwVTdpzhHYLZyxLYoSu05W6z8SAOs=\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "tests/data/user_rsa_key",
    "content": "-----BEGIN RSA PRIVATE KEY-----\nMIICXQIBAAKBgQDI7iK3d8eWYZlYloat94c5VjtFY7c/0zuGl8C7uMnZ3t6i2G99\n66hEW0nCFSZkOW5F0XKEVj+EUCHvo8koYC6wiohAqWQnEwIoOoh7GSAcB8gP/qaq\n+adIl/Rvlby/mHakj+y05LBND6nFWHAn1y1gOFFKUXSJNRZPXSFy47gqzwIBIwKB\ngQCbANjz7q/pCXZLp1Hz6tYHqOvlEmjK1iabB1oqafrMpJ0eibUX/u+FMHq6StR5\nM5413BaDWHokPdEJUnabfWXXR3SMlBUKrck0eAer1O8m78yxu3OEdpRk+znVo4DL\nguMeCdJB/qcF0kEsx+Q8HP42MZU1oCmk3PbfXNFwaHbWuwJBAOQ/ry/hLD7AqB8x\nDmCM82A9E59ICNNlHOhxpJoh6nrNTPCsBAEu/SmqrL8mS6gmbRKUaya5Lx1pkxj2\ns/kWOokCQQDhXCcYXjjWiIfxhl6Rlgkk1vmI0l6785XSJNv4P7pXjGmShXfIzroh\nS8uWK3tL0GELY7+UAKDTUEVjjQdGxYSXAkEA3bo1JzKCwJ3lJZ1ebGuqmADRO6UP\n40xH977aadfN1mEI6cusHmgpISl0nG5YH7BMsvaT+bs1FUH8m+hXDzoqOwJBAK3Z\nX/za+KV/REya2z0b+GzgWhkXUGUa/owrEBdHGriQ47osclkUgPUdNqcLmaDilAF4\n1Z4PHPrI5RJIONAx+JECQQC/fChqjBgFpk6iJ+BOdSexQpgfxH/u/457W10Y43HR\nsoS+8btbHqjQkowQ/2NTlUfWvqIlfxs6ZbFsIp/HrhZL\n-----END RSA PRIVATE KEY-----\n"
  },
  {
    "path": "tests/sshserver.py",
    "content": "import base64\nimport random\nimport socket\n# import sys\nimport threading\n# import traceback\nimport paramiko\n\nfrom binascii import hexlify\nfrom tests.utils import make_tests_data_path\n\n\n# setup logging\nparamiko.util.log_to_file(make_tests_data_path('sshserver.log'))\n\nhost_key = paramiko.RSAKey(filename=make_tests_data_path('test_rsa.key'))\n# host_key = paramiko.DSSKey(filename='test_dss.key')\n\nprint('Read key: ' + hexlify(host_key.get_fingerprint()).decode('utf-8'))\n\nbanner = u'\\r\\n\\u6b22\\u8fce\\r\\n'\nevent_timeout = 5\n\n\nclass Server(paramiko.ServerInterface):\n    # 'data' is the output of base64.b64encode(key)\n    # (using the \"user_rsa_key\" files)\n    data = (b'AAAAB3NzaC1yc2EAAAABIwAAAIEAyO4it3fHlmGZWJaGrfeHOVY7RWO3P9M7hp'\n            b'fAu7jJ2d7eothvfeuoRFtJwhUmZDluRdFyhFY/hFAh76PJKGAusIqIQKlkJxMC'\n            b'KDqIexkgHAfID/6mqvmnSJf0b5W8v5h2pI/stOSwTQ+pxVhwJ9ctYDhRSlF0iT'\n            b'UWT10hcuO4Ks8=')\n    good_pub_key = paramiko.RSAKey(data=base64.decodebytes(data))\n\n    commands = [\n        b'$SHELL -ilc \"locale charmap\"',\n        b'$SHELL -ic \"locale charmap\"'\n    ]\n    encodings = ['UTF-8', 'GBK', 'UTF-8\\r\\n', 'GBK\\r\\n']\n\n    def __init__(self, encodings=[]):\n        self.shell_event = threading.Event()\n        self.exec_event = threading.Event()\n        self.cmd_to_enc = self.get_cmd2enc(encodings)\n        self.password_verified = False\n        self.key_verified = False\n\n    def get_cmd2enc(self, encodings):\n        n = len(self.commands)\n        while len(encodings) < n:\n            encodings.append(random.choice(self.encodings))\n        return dict(zip(self.commands, encodings[0:n]))\n\n    def check_channel_request(self, kind, chanid):\n        if kind == 'session':\n            return paramiko.OPEN_SUCCEEDED\n        return paramiko.OPEN_FAILED_ADMINISTRATIVELY_PROHIBITED\n\n    def check_auth_password(self, username, password):\n        print('Auth attempt with username: {!r} & password: {!r}'.format(username, password)) # noqa\n        if (username in ['robey', 'bar', 'foo']) and (password == 'foo'):\n            return paramiko.AUTH_SUCCESSFUL\n        return paramiko.AUTH_FAILED\n\n    def check_auth_publickey(self, username, key):\n        print('Auth attempt with username: {!r} & key: {!r}'.format(username, hexlify(key.get_fingerprint()).decode('utf-8'))) # noqa\n        if (username in ['robey', 'keyonly']) and (key == self.good_pub_key):\n            return paramiko.AUTH_SUCCESSFUL\n        if username == 'pkey2fa' and key == self.good_pub_key:\n            self.key_verified = True\n            return paramiko.AUTH_PARTIALLY_SUCCESSFUL\n        return paramiko.AUTH_FAILED\n\n    def check_auth_interactive(self, username, submethods):\n        if username in ['pass2fa', 'pkey2fa']:\n            self.username = username\n            prompt = 'Verification code: ' if self.password_verified else 'Password: '  # noqa\n            print(username, prompt)\n            return paramiko.InteractiveQuery('', '', prompt)\n        return paramiko.AUTH_FAILED\n\n    def check_auth_interactive_response(self, responses):\n        if self.username in ['pass2fa', 'pkey2fa']:\n            if not self.password_verified:\n                if responses[0] == 'password':\n                    print('password verified')\n                    self.password_verified = True\n                    if self.username == 'pkey2fa':\n                        return self.check_auth_interactive(self.username, '')\n                else:\n                    print('wrong password: {}'.format(responses[0]))\n                    return paramiko.AUTH_FAILED\n            else:\n                if responses[0] == 'passcode':\n                    print('totp verified')\n                    return paramiko.AUTH_SUCCESSFUL\n                else:\n                    print('wrong totp: {}'.format(responses[0]))\n                    return paramiko.AUTH_FAILED\n        else:\n            return paramiko.AUTH_FAILED\n\n    def get_allowed_auths(self, username):\n        if username == 'keyonly':\n            return 'publickey'\n        if username == 'pass2fa':\n            return 'keyboard-interactive'\n        if username == 'pkey2fa':\n            if not self.key_verified:\n                return 'publickey'\n            else:\n                return 'keyboard-interactive'\n        return 'password,publickey'\n\n    def check_channel_exec_request(self, channel, command):\n        if command not in self.commands:\n            ret = False\n        else:\n            ret = True\n            self.encoding = self.cmd_to_enc[command]\n            channel.send(self.encoding)\n            channel.shutdown(1)\n        self.exec_event.set()\n        return ret\n\n    def check_channel_shell_request(self, channel):\n        self.shell_event.set()\n        return True\n\n    def check_channel_pty_request(self, channel, term, width, height,\n                                  pixelwidth, pixelheight, modes):\n        return True\n\n    def check_channel_window_change_request(self, channel, width, height,\n                                            pixelwidth, pixelheight):\n        channel.send('resized')\n        return True\n\n\ndef run_ssh_server(port=2200, running=True, encodings=[]):\n    # now connect\n    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)\n    sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)\n    sock.bind(('127.0.0.1', port))\n    sock.listen(100)\n\n    while running:\n        client, addr = sock.accept()\n        print('Got a connection!')\n\n        t = paramiko.Transport(client)\n        t.load_server_moduli()\n        t.add_server_key(host_key)\n        server = Server(encodings)\n        try:\n            t.start_server(server=server)\n        except Exception as e:\n            print(e)\n            continue\n\n        # wait for auth\n        chan = t.accept(2)\n        if chan is None:\n            print('*** No channel.')\n            continue\n\n        username = t.get_username()\n        print('{} Authenticated!'.format(username))\n\n        server.shell_event.wait(timeout=event_timeout)\n        if not server.shell_event.is_set():\n            print('*** Client never asked for a shell.')\n            continue\n\n        server.exec_event.wait(timeout=event_timeout)\n        if not server.exec_event.is_set():\n            print('*** Client never asked for a command.')\n            continue\n\n        # chan.send('\\r\\n\\r\\nWelcome!\\r\\n\\r\\n')\n        print(server.encoding)\n        try:\n            banner_encoded = banner.encode(server.encoding)\n        except (ValueError, LookupError):\n            continue\n\n        chan.send(banner_encoded)\n        if username == 'bar':\n            msg = chan.recv(1024)\n            chan.send(msg)\n        elif username == 'foo':\n            lst = []\n            while True:\n                msg = chan.recv(32 * 1024)\n                lst.append(msg)\n                if msg.endswith(b'\\r\\n\\r\\n'):\n                    break\n            data = b''.join(lst)\n            while data:\n                s = chan.send(data)\n                data = data[s:]\n        else:\n            chan.close()\n            t.close()\n            client.close()\n\n    try:\n        sock.close()\n    except Exception:\n        pass\n\n\nif __name__ == '__main__':\n    run_ssh_server()\n"
  },
  {
    "path": "tests/test_app.py",
    "content": "import json\nimport random\nimport threading\nimport tornado.websocket\nimport tornado.gen\n\nfrom tornado.testing import AsyncHTTPTestCase\nfrom tornado.httpclient import HTTPError\nfrom tornado.options import options\nfrom tests.sshserver import run_ssh_server, banner, Server\nfrom tests.utils import encode_multipart_formdata, read_file, make_tests_data_path  # noqa\nfrom webssh import handler\nfrom webssh.main import make_app, make_handlers\nfrom webssh.settings import (\n    get_app_settings, get_server_settings, max_body_size\n)\nfrom webssh.utils import to_str\nfrom webssh.worker import clients\n\ntry:\n    from urllib.parse import urlencode\nexcept ImportError:\n    from urllib import urlencode\n\n\nswallow_http_errors = handler.swallow_http_errors\nserver_encodings = {e.strip() for e in Server.encodings}\n\n\nclass TestAppBase(AsyncHTTPTestCase):\n\n    def get_httpserver_options(self):\n        return get_server_settings(options)\n\n    def assert_response(self, bstr, response):\n        if swallow_http_errors:\n            self.assertEqual(response.code, 200)\n            self.assertIn(bstr, response.body)\n        else:\n            self.assertEqual(response.code, 400)\n            self.assertIn(b'Bad Request', response.body)\n\n    def assert_status_in(self, status, data):\n        self.assertIsNone(data['encoding'])\n        self.assertIsNone(data['id'])\n        self.assertIn(status, data['status'])\n\n    def assert_status_equal(self, status, data):\n        self.assertIsNone(data['encoding'])\n        self.assertIsNone(data['id'])\n        self.assertEqual(status, data['status'])\n\n    def assert_status_none(self, data):\n        self.assertIsNotNone(data['encoding'])\n        self.assertIsNotNone(data['id'])\n        self.assertIsNone(data['status'])\n\n    def fetch_request(self, url, method='GET', body='', headers={}, sync=True):\n        if not sync and url.startswith('/'):\n            url = self.get_url(url)\n\n        if isinstance(body, dict):\n            body = urlencode(body)\n\n        if not headers:\n            headers = self.headers\n        else:\n            headers.update(self.headers)\n\n        client = self if sync else self.get_http_client()\n        return client.fetch(url, method=method, body=body, headers=headers)\n\n    def sync_post(self, url, body, headers={}):\n        return self.fetch_request(url, 'POST', body, headers)\n\n    def async_post(self, url, body, headers={}):\n        return self.fetch_request(url, 'POST', body, headers, sync=False)\n\n\nclass TestAppBasic(TestAppBase):\n\n    running = [True]\n    sshserver_port = 2200\n    body = 'hostname=127.0.0.1&port={}&_xsrf=yummy&username=robey&password=foo'.format(sshserver_port) # noqa\n    headers = {'Cookie': '_xsrf=yummy'}\n\n    def get_app(self):\n        self.body_dict = {\n            'hostname': '127.0.0.1',\n            'port': str(self.sshserver_port),\n            'username': 'robey',\n            'password': '',\n            '_xsrf': 'yummy'\n        }\n        loop = self.io_loop\n        options.debug = False\n        options.policy = random.choice(['warning', 'autoadd'])\n        options.hostfile = ''\n        options.syshostfile = ''\n        options.tdstream = ''\n        options.delay = 0.1\n        app = make_app(make_handlers(loop, options), get_app_settings(options))\n        return app\n\n    @classmethod\n    def setUpClass(cls):\n        print('='*20)\n        t = threading.Thread(\n            target=run_ssh_server, args=(cls.sshserver_port, cls.running)\n        )\n        t.setDaemon(True)\n        t.start()\n\n    @classmethod\n    def tearDownClass(cls):\n        cls.running.pop()\n        print('='*20)\n\n    def test_app_with_invalid_form_for_missing_argument(self):\n        response = self.fetch('/')\n        self.assertEqual(response.code, 200)\n\n        body = 'port=7000&username=admin&password&_xsrf=yummy'\n        response = self.sync_post('/', body)\n        self.assert_response(b'Missing argument hostname', response)\n\n        body = 'hostname=127.0.0.1&port=7000&password&_xsrf=yummy'\n        response = self.sync_post('/', body)\n        self.assert_response(b'Missing argument username', response)\n\n        body = 'hostname=&port=&username=&password&_xsrf=yummy'\n        response = self.sync_post('/', body)\n        self.assert_response(b'Missing value hostname', response)\n\n        body = 'hostname=127.0.0.1&port=7000&username=&password&_xsrf=yummy'\n        response = self.sync_post('/', body)\n        self.assert_response(b'Missing value username', response)\n\n    def test_app_with_invalid_form_for_invalid_value(self):\n        body = 'hostname=127.0.0&port=22&username=&password&_xsrf=yummy'\n        response = self.sync_post('/', body)\n        self.assert_response(b'Invalid hostname', response)\n\n        body = 'hostname=http://www.googe.com&port=22&username=&password&_xsrf=yummy'  # noqa\n        response = self.sync_post('/', body)\n        self.assert_response(b'Invalid hostname', response)\n\n        body = 'hostname=127.0.0.1&port=port&username=&password&_xsrf=yummy'\n        response = self.sync_post('/', body)\n        self.assert_response(b'Invalid port', response)\n\n        body = 'hostname=127.0.0.1&port=70000&username=&password&_xsrf=yummy'\n        response = self.sync_post('/', body)\n        self.assert_response(b'Invalid port', response)\n\n    def test_app_with_wrong_hostname_ip(self):\n        body = 'hostname=127.0.0.2&port=2200&username=admin&_xsrf=yummy'\n        response = self.sync_post('/', body)\n        self.assertEqual(response.code, 200)\n        self.assertIn(b'Unable to connect to', response.body)\n\n    def test_app_with_wrong_hostname_domain(self):\n        body = 'hostname=xxxxxxxxxxxx&port=2200&username=admin&_xsrf=yummy'\n        response = self.sync_post('/', body)\n        self.assertEqual(response.code, 200)\n        self.assertIn(b'Unable to connect to', response.body)\n\n    def test_app_with_wrong_port(self):\n        body = 'hostname=127.0.0.1&port=7000&username=admin&_xsrf=yummy'\n        response = self.sync_post('/', body)\n        self.assertEqual(response.code, 200)\n        self.assertIn(b'Unable to connect to', response.body)\n\n    def test_app_with_wrong_credentials(self):\n        response = self.sync_post('/', self.body + 's')\n        self.assert_status_in('Authentication failed.', json.loads(to_str(response.body))) # noqa\n\n    def test_app_with_correct_credentials(self):\n        response = self.sync_post('/', self.body)\n        self.assert_status_none(json.loads(to_str(response.body)))\n\n    def test_app_with_correct_credentials_but_with_no_port(self):\n        default_port = handler.DEFAULT_PORT\n        handler.DEFAULT_PORT = self.sshserver_port\n\n        # with no port value\n        body = self.body.replace(str(self.sshserver_port), '')\n        response = self.sync_post('/', body)\n        self.assert_status_none(json.loads(to_str(response.body)))\n\n        # with no port argument\n        body = body.replace('port=&', '')\n        response = self.sync_post('/', body)\n        self.assert_status_none(json.loads(to_str(response.body)))\n\n        handler.DEFAULT_PORT = default_port\n\n    @tornado.testing.gen_test\n    def test_app_with_correct_credentials_timeout(self):\n        url = self.get_url('/')\n        response = yield self.async_post(url, self.body)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n        url = url.replace('http', 'ws')\n        ws_url = url + 'ws?id=' + data['id']\n        yield tornado.gen.sleep(options.delay + 0.1)\n        ws = yield tornado.websocket.websocket_connect(ws_url)\n        msg = yield ws.read_message()\n        self.assertIsNone(msg)\n        self.assertEqual(ws.close_reason, 'Websocket authentication failed.')\n\n    @tornado.testing.gen_test\n    def test_app_with_correct_credentials_but_ip_not_matched(self):\n        url = self.get_url('/')\n        response = yield self.async_post(url, self.body)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n        clients = handler.clients\n        handler.clients = {}\n        url = url.replace('http', 'ws')\n        ws_url = url + 'ws?id=' + data['id']\n        ws = yield tornado.websocket.websocket_connect(ws_url)\n        msg = yield ws.read_message()\n        self.assertIsNone(msg)\n        self.assertEqual(ws.close_reason, 'Websocket authentication failed.')\n        handler.clients = clients\n\n    @tornado.testing.gen_test\n    def test_app_with_correct_credentials_user_robey(self):\n        url = self.get_url('/')\n        response = yield self.async_post(url, self.body)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n        url = url.replace('http', 'ws')\n        ws_url = url + 'ws?id=' + data['id']\n        ws = yield tornado.websocket.websocket_connect(ws_url)\n        msg = yield ws.read_message()\n        self.assertEqual(to_str(msg, data['encoding']), banner)\n        ws.close()\n\n    @tornado.testing.gen_test\n    def test_app_with_correct_credentials_but_without_id_argument(self):\n        url = self.get_url('/')\n        response = yield self.async_post(url, self.body)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n        url = url.replace('http', 'ws')\n        ws_url = url + 'ws'\n        ws = yield tornado.websocket.websocket_connect(ws_url)\n        msg = yield ws.read_message()\n        self.assertIsNone(msg)\n        self.assertIn('Missing argument id', ws.close_reason)\n\n    @tornado.testing.gen_test\n    def test_app_with_correct_credentials_but_empty_id(self):\n        url = self.get_url('/')\n        response = yield self.async_post(url, self.body)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n        url = url.replace('http', 'ws')\n        ws_url = url + 'ws?id='\n        ws = yield tornado.websocket.websocket_connect(ws_url)\n        msg = yield ws.read_message()\n        self.assertIsNone(msg)\n        self.assertIn('Missing value id', ws.close_reason)\n\n    @tornado.testing.gen_test\n    def test_app_with_correct_credentials_but_wrong_id(self):\n        url = self.get_url('/')\n        response = yield self.async_post(url, self.body)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n        url = url.replace('http', 'ws')\n        ws_url = url + 'ws?id=1' + data['id']\n        ws = yield tornado.websocket.websocket_connect(ws_url)\n        msg = yield ws.read_message()\n        self.assertIsNone(msg)\n        self.assertIn('Websocket authentication failed', ws.close_reason)\n\n    @tornado.testing.gen_test\n    def test_app_with_correct_credentials_user_bar(self):\n        body = self.body.replace('robey', 'bar')\n        url = self.get_url('/')\n        response = yield self.async_post(url, body)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n        url = url.replace('http', 'ws')\n        ws_url = url + 'ws?id=' + data['id']\n        ws = yield tornado.websocket.websocket_connect(ws_url)\n        msg = yield ws.read_message()\n        self.assertEqual(to_str(msg, data['encoding']), banner)\n\n        # messages below will be ignored silently\n        yield ws.write_message('hello')\n        yield ws.write_message('\"hello\"')\n        yield ws.write_message('[hello]')\n        yield ws.write_message(json.dumps({'resize': []}))\n        yield ws.write_message(json.dumps({'resize': {}}))\n        yield ws.write_message(json.dumps({'resize': 'ab'}))\n        yield ws.write_message(json.dumps({'resize': ['a', 'b']}))\n        yield ws.write_message(json.dumps({'resize': {'a': 1, 'b': 2}}))\n        yield ws.write_message(json.dumps({'resize': [100]}))\n        yield ws.write_message(json.dumps({'resize': [100]*10}))\n        yield ws.write_message(json.dumps({'resize': [-1, -1]}))\n        yield ws.write_message(json.dumps({'data': [1]}))\n        yield ws.write_message(json.dumps({'data': (1,)}))\n        yield ws.write_message(json.dumps({'data': {'a': 2}}))\n        yield ws.write_message(json.dumps({'data': 1}))\n        yield ws.write_message(json.dumps({'data': 2.1}))\n        yield ws.write_message(json.dumps({'key-non-existed': 'hello'}))\n        # end - those just for testing webssh websocket stablity\n\n        yield ws.write_message(json.dumps({'resize': [79, 23]}))\n        msg = yield ws.read_message()\n        self.assertEqual(b'resized', msg)\n\n        yield ws.write_message(json.dumps({'data': 'bye'}))\n        msg = yield ws.read_message()\n        self.assertEqual(b'bye', msg)\n        ws.close()\n\n    @tornado.testing.gen_test\n    def test_app_auth_with_valid_pubkey_by_urlencoded_form(self):\n        url = self.get_url('/')\n        privatekey = read_file(make_tests_data_path('user_rsa_key'))\n        self.body_dict.update(privatekey=privatekey)\n        response = yield self.async_post(url, self.body_dict)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n        url = url.replace('http', 'ws')\n        ws_url = url + 'ws?id=' + data['id']\n        ws = yield tornado.websocket.websocket_connect(ws_url)\n        msg = yield ws.read_message()\n        self.assertEqual(to_str(msg, data['encoding']), banner)\n        ws.close()\n\n    @tornado.testing.gen_test\n    def test_app_auth_with_valid_pubkey_by_multipart_form(self):\n        url = self.get_url('/')\n        privatekey = read_file(make_tests_data_path('user_rsa_key'))\n        files = [('privatekey', 'user_rsa_key', privatekey)]\n        content_type, body = encode_multipart_formdata(self.body_dict.items(),\n                                                       files)\n        headers = {\n            'Content-Type': content_type, 'content-length': str(len(body))\n        }\n        response = yield self.async_post(url, body, headers=headers)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n        url = url.replace('http', 'ws')\n        ws_url = url + 'ws?id=' + data['id']\n        ws = yield tornado.websocket.websocket_connect(ws_url)\n        msg = yield ws.read_message()\n        self.assertEqual(to_str(msg, data['encoding']), banner)\n        ws.close()\n\n    @tornado.testing.gen_test\n    def test_app_auth_with_invalid_pubkey_for_user_robey(self):\n        url = self.get_url('/')\n        privatekey = 'h' * 1024\n        files = [('privatekey', 'user_rsa_key', privatekey)]\n        content_type, body = encode_multipart_formdata(self.body_dict.items(),\n                                                       files)\n        headers = {\n            'Content-Type': content_type, 'content-length': str(len(body))\n        }\n\n        if swallow_http_errors:\n            response = yield self.async_post(url, body, headers=headers)\n            self.assertIn(b'Invalid key', response.body)\n        else:\n            with self.assertRaises(HTTPError) as ctx:\n                yield self.async_post(url, body, headers=headers)\n            self.assertIn('Bad Request', ctx.exception.message)\n\n    @tornado.testing.gen_test\n    def test_app_auth_with_pubkey_exceeds_key_max_size(self):\n        url = self.get_url('/')\n        privatekey = 'h' * (handler.PrivateKey.max_length + 1)\n        files = [('privatekey', 'user_rsa_key', privatekey)]\n        content_type, body = encode_multipart_formdata(self.body_dict.items(),\n                                                       files)\n        headers = {\n            'Content-Type': content_type, 'content-length': str(len(body))\n        }\n        if swallow_http_errors:\n            response = yield self.async_post(url, body, headers=headers)\n            self.assertIn(b'Invalid key', response.body)\n        else:\n            with self.assertRaises(HTTPError) as ctx:\n                yield self.async_post(url, body, headers=headers)\n            self.assertIn('Bad Request', ctx.exception.message)\n\n    @tornado.testing.gen_test\n    def test_app_auth_with_pubkey_cannot_be_decoded_by_multipart_form(self):\n        url = self.get_url('/')\n        privatekey = 'h' * 1024\n        files = [('privatekey', 'user_rsa_key', privatekey)]\n        content_type, body = encode_multipart_formdata(self.body_dict.items(),\n                                                       files)\n        body = body.encode('utf-8')\n        # added some gbk bytes to the privatekey, make it cannot be decoded\n        body = body[:-100] + b'\\xb4\\xed\\xce\\xf3' + body[-100:]\n        headers = {\n            'Content-Type': content_type, 'content-length': str(len(body))\n        }\n        if swallow_http_errors:\n            response = yield self.async_post(url, body, headers=headers)\n            self.assertIn(b'Invalid unicode', response.body)\n        else:\n            with self.assertRaises(HTTPError) as ctx:\n                yield self.async_post(url, body, headers=headers)\n            self.assertIn('Bad Request', ctx.exception.message)\n\n    def test_app_post_form_with_large_body_size_by_multipart_form(self):\n        privatekey = 'h' * (2 * max_body_size)\n        files = [('privatekey', 'user_rsa_key', privatekey)]\n        content_type, body = encode_multipart_formdata(self.body_dict.items(),\n                                                       files)\n        headers = {\n            'Content-Type': content_type, 'content-length': str(len(body))\n        }\n        response = self.sync_post('/', body, headers=headers)\n        self.assertIn(response.code, [400, 599])\n\n    def test_app_post_form_with_large_body_size_by_urlencoded_form(self):\n        privatekey = 'h' * (2 * max_body_size)\n        body = self.body + '&privatekey=' + privatekey\n        response = self.sync_post('/', body)\n        self.assertIn(response.code, [400, 599])\n\n    @tornado.testing.gen_test\n    def test_app_with_user_keyonly_for_bad_authentication_type(self):\n        self.body_dict.update(username='keyonly', password='foo')\n        response = yield self.async_post('/', self.body_dict)\n        self.assertEqual(response.code, 200)\n        self.assert_status_in('Bad authentication type', json.loads(to_str(response.body))) # noqa\n\n    @tornado.testing.gen_test\n    def test_app_with_user_pass2fa_with_correct_passwords(self):\n        self.body_dict.update(username='pass2fa', password='password',\n                              totp='passcode')\n        response = yield self.async_post('/', self.body_dict)\n        self.assertEqual(response.code, 200)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n    @tornado.testing.gen_test\n    def test_app_with_user_pass2fa_with_wrong_pkey_correct_passwords(self):\n        url = self.get_url('/')\n        privatekey = read_file(make_tests_data_path('user_rsa_key'))\n        self.body_dict.update(username='pass2fa', password='password',\n                              privatekey=privatekey, totp='passcode')\n        response = yield self.async_post(url, self.body_dict)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n    @tornado.testing.gen_test\n    def test_app_with_user_pkey2fa_with_correct_passwords(self):\n        url = self.get_url('/')\n        privatekey = read_file(make_tests_data_path('user_rsa_key'))\n        self.body_dict.update(username='pkey2fa', password='password',\n                              privatekey=privatekey, totp='passcode')\n        response = yield self.async_post(url, self.body_dict)\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n    @tornado.testing.gen_test\n    def test_app_with_user_pkey2fa_with_wrong_password(self):\n        url = self.get_url('/')\n        privatekey = read_file(make_tests_data_path('user_rsa_key'))\n        self.body_dict.update(username='pkey2fa', password='wrongpassword',\n                              privatekey=privatekey, totp='passcode')\n        response = yield self.async_post(url, self.body_dict)\n        data = json.loads(to_str(response.body))\n        self.assert_status_in('Authentication failed', data)\n\n    @tornado.testing.gen_test\n    def test_app_with_user_pkey2fa_with_wrong_passcode(self):\n        url = self.get_url('/')\n        privatekey = read_file(make_tests_data_path('user_rsa_key'))\n        self.body_dict.update(username='pkey2fa', password='password',\n                              privatekey=privatekey, totp='wrongpasscode')\n        response = yield self.async_post(url, self.body_dict)\n        data = json.loads(to_str(response.body))\n        self.assert_status_in('Authentication failed', data)\n\n    @tornado.testing.gen_test\n    def test_app_with_user_pkey2fa_with_empty_passcode(self):\n        url = self.get_url('/')\n        privatekey = read_file(make_tests_data_path('user_rsa_key'))\n        self.body_dict.update(username='pkey2fa', password='password',\n                              privatekey=privatekey, totp='')\n        response = yield self.async_post(url, self.body_dict)\n        data = json.loads(to_str(response.body))\n        self.assert_status_in('Need a verification code', data)\n\n\nclass OtherTestBase(TestAppBase):\n    sshserver_port = 3300\n    headers = {'Cookie': '_xsrf=yummy'}\n    debug = False\n    policy = None\n    xsrf = True\n    hostfile = ''\n    syshostfile = ''\n    tdstream = ''\n    maxconn = 20\n    origin = 'same'\n    encodings = []\n    body = {\n        'hostname': '127.0.0.1',\n        'port': '',\n        'username': 'robey',\n        'password': 'foo',\n        '_xsrf': 'yummy'\n    }\n\n    def get_app(self):\n        self.body.update(port=str(self.sshserver_port))\n        loop = self.io_loop\n        options.debug = self.debug\n        options.xsrf = self.xsrf\n        options.policy = self.policy if self.policy else random.choice(['warning', 'autoadd'])  # noqa\n        options.hostfile = self.hostfile\n        options.syshostfile = self.syshostfile\n        options.tdstream = self.tdstream\n        options.maxconn = self.maxconn\n        options.origin = self.origin\n        app = make_app(make_handlers(loop, options), get_app_settings(options))\n        return app\n\n    def setUp(self):\n        print('='*20)\n        self.running = True\n        OtherTestBase.sshserver_port += 1\n\n        t = threading.Thread(\n            target=run_ssh_server,\n            args=(self.sshserver_port, self.running, self.encodings)\n        )\n        t.setDaemon(True)\n        t.start()\n        super(OtherTestBase, self).setUp()\n\n    def tearDown(self):\n        self.running = False\n        print('='*20)\n        super(OtherTestBase, self).tearDown()\n\n\nclass TestAppInDebugMode(OtherTestBase):\n\n    debug = True\n\n    def assert_response(self, bstr, response):\n        if swallow_http_errors:\n            self.assertEqual(response.code, 200)\n            self.assertIn(bstr, response.body)\n        else:\n            self.assertEqual(response.code, 500)\n            self.assertIn(b'Uncaught exception', response.body)\n\n    def test_server_error_for_post_method(self):\n        body = dict(self.body, error='raise')\n        response = self.sync_post('/', body)\n        self.assert_response(b'\"status\": \"Internal Server Error\"', response)\n\n    def test_html(self):\n        response = self.fetch('/', method='GET')\n        self.assertIn(b'novalidate>', response.body)\n\n\nclass TestAppWithLargeBuffer(OtherTestBase):\n\n    @tornado.testing.gen_test\n    def test_app_for_sending_message_with_large_size(self):\n        url = self.get_url('/')\n        response = yield self.async_post(url, dict(self.body, username='foo'))\n        data = json.loads(to_str(response.body))\n        self.assert_status_none(data)\n\n        url = url.replace('http', 'ws')\n        ws_url = url + 'ws?id=' + data['id']\n        ws = yield tornado.websocket.websocket_connect(ws_url)\n        msg = yield ws.read_message()\n        self.assertEqual(to_str(msg, data['encoding']), banner)\n\n        send = 'h' * (64 * 1024) + '\\r\\n\\r\\n'\n        yield ws.write_message(json.dumps({'data': send}))\n        lst = []\n        while True:\n            msg = yield ws.read_message()\n            lst.append(msg)\n            if msg.endswith(b'\\r\\n\\r\\n'):\n                break\n        recv = b''.join(lst).decode(data['encoding'])\n        self.assertEqual(send, recv)\n        ws.close()\n\n\nclass TestAppWithRejectPolicy(OtherTestBase):\n\n    policy = 'reject'\n    hostfile = make_tests_data_path('known_hosts_example')\n\n    @tornado.testing.gen_test\n    def test_app_with_hostname_not_in_hostkeys(self):\n        response = yield self.async_post('/', self.body)\n        data = json.loads(to_str(response.body))\n        message = 'Connection to {}:{} is not allowed.'.format(self.body['hostname'], self.sshserver_port) # noqa\n        self.assertEqual(message, data['status'])\n\n\nclass TestAppWithBadHostKey(OtherTestBase):\n\n    policy = random.choice(['warning', 'autoadd', 'reject'])\n    hostfile = make_tests_data_path('test_known_hosts')\n\n    def setUp(self):\n        self.sshserver_port = 2222\n        super(TestAppWithBadHostKey, self).setUp()\n\n    @tornado.testing.gen_test\n    def test_app_with_bad_host_key(self):\n        response = yield self.async_post('/', self.body)\n        data = json.loads(to_str(response.body))\n        self.assertEqual('Bad host key.', data['status'])\n\n\nclass TestAppWithTrustedStream(OtherTestBase):\n    tdstream = '127.0.0.2'\n\n    def test_with_forbidden_get_request(self):\n        response = self.fetch('/', method='GET')\n        self.assertEqual(response.code, 403)\n        self.assertIn('Forbidden', response.error.message)\n\n    def test_with_forbidden_post_request(self):\n        response = self.sync_post('/', self.body)\n        self.assertEqual(response.code, 403)\n        self.assertIn('Forbidden', response.error.message)\n\n    def test_with_forbidden_put_request(self):\n        response = self.fetch_request('/', method='PUT', body=self.body)\n        self.assertEqual(response.code, 403)\n        self.assertIn('Forbidden', response.error.message)\n\n\nclass TestAppNotFoundHandler(OtherTestBase):\n\n    custom_headers = handler.MixinHandler.custom_headers\n\n    def test_with_not_found_get_request(self):\n        response = self.fetch('/pathnotfound', method='GET')\n        self.assertEqual(response.code, 404)\n        self.assertEqual(\n            response.headers['Server'], self.custom_headers['Server']\n        )\n        self.assertIn(b'404: Not Found', response.body)\n\n    def test_with_not_found_post_request(self):\n        response = self.sync_post('/pathnotfound', self.body)\n        self.assertEqual(response.code, 404)\n        self.assertEqual(\n            response.headers['Server'], self.custom_headers['Server']\n        )\n        self.assertIn(b'404: Not Found', response.body)\n\n    def test_with_not_found_put_request(self):\n        response = self.fetch_request('/pathnotfound', method='PUT',\n                                      body=self.body)\n        self.assertEqual(response.code, 404)\n        self.assertEqual(\n            response.headers['Server'], self.custom_headers['Server']\n        )\n        self.assertIn(b'404: Not Found', response.body)\n\n\nclass TestAppWithHeadRequest(OtherTestBase):\n\n    def test_with_index_path(self):\n        response = self.fetch('/', method='HEAD')\n        self.assertEqual(response.code, 200)\n\n    def test_with_ws_path(self):\n        response = self.fetch('/ws', method='HEAD')\n        self.assertEqual(response.code, 405)\n\n    def test_with_not_found_path(self):\n        response = self.fetch('/notfound', method='HEAD')\n        self.assertEqual(response.code, 404)\n\n\nclass TestAppWithPutRequest(OtherTestBase):\n\n    xsrf = False\n\n    @tornado.testing.gen_test\n    def test_app_with_method_not_supported(self):\n        with self.assertRaises(HTTPError) as ctx:\n            yield self.fetch_request('/', 'PUT', self.body, sync=False)\n        self.assertIn('Method Not Allowed', ctx.exception.message)\n\n\nclass TestAppWithTooManyConnections(OtherTestBase):\n\n    maxconn = 1\n\n    def setUp(self):\n        clients.clear()\n        super(TestAppWithTooManyConnections, self).setUp()\n\n    @tornado.testing.gen_test\n    def test_app_with_too_many_connections(self):\n        clients['127.0.0.1'] = {'fake_worker_id': None}\n\n        url = self.get_url('/')\n        response = yield self.async_post(url, self.body)\n        data = json.loads(to_str(response.body))\n        self.assertEqual('Too many live connections.', data['status'])\n\n        clients['127.0.0.1'].clear()\n        response = yield self.async_post(url, self.body)\n        self.assert_status_none(json.loads(to_str(response.body)))\n\n\nclass TestAppWithCrossOriginOperation(OtherTestBase):\n\n    origin = 'http://www.example.com'\n\n    @tornado.testing.gen_test\n    def test_app_with_wrong_event_origin(self):\n        body = dict(self.body, _origin='localhost')\n        response = yield self.async_post('/', body)\n        self.assert_status_equal('Cross origin operation is not allowed.', json.loads(to_str(response.body))) # noqa\n\n    @tornado.testing.gen_test\n    def test_app_with_wrong_header_origin(self):\n        headers = dict(Origin='localhost')\n        response = yield self.async_post('/', self.body, headers=headers)\n        self.assert_status_equal('Cross origin operation is not allowed.', json.loads(to_str(response.body)), ) # noqa\n\n    @tornado.testing.gen_test\n    def test_app_with_correct_event_origin(self):\n        body = dict(self.body, _origin=self.origin)\n        response = yield self.async_post('/', body)\n        self.assert_status_none(json.loads(to_str(response.body)))\n        self.assertIsNone(response.headers.get('Access-Control-Allow-Origin'))\n\n    @tornado.testing.gen_test\n    def test_app_with_correct_header_origin(self):\n        headers = dict(Origin=self.origin)\n        response = yield self.async_post('/', self.body, headers=headers)\n        self.assert_status_none(json.loads(to_str(response.body)))\n        self.assertEqual(\n            response.headers.get('Access-Control-Allow-Origin'), self.origin\n        )\n\n\nclass TestAppWithBadEncoding(OtherTestBase):\n\n    encodings = [u'\\u7f16\\u7801']\n\n    @tornado.testing.gen_test\n    def test_app_with_a_bad_encoding(self):\n        response = yield self.async_post('/', self.body)\n        dic = json.loads(to_str(response.body))\n        self.assert_status_none(dic)\n        self.assertIn(dic['encoding'], server_encodings)\n\n\nclass TestAppWithUnknownEncoding(OtherTestBase):\n\n    encodings = [u'\\u7f16\\u7801', u'UnknownEncoding']\n\n    @tornado.testing.gen_test\n    def test_app_with_a_unknown_encoding(self):\n        response = yield self.async_post('/', self.body)\n        self.assert_status_none(json.loads(to_str(response.body)))\n        dic = json.loads(to_str(response.body))\n        self.assert_status_none(dic)\n        self.assertEqual(dic['encoding'], 'utf-8')\n"
  },
  {
    "path": "tests/test_handler.py",
    "content": "import io\nimport unittest\nimport paramiko\n\nfrom tornado.httputil import HTTPServerRequest\nfrom tornado.options import options\nfrom tests.utils import read_file, make_tests_data_path\nfrom webssh import handler\nfrom webssh import worker\nfrom webssh.handler import (\n    IndexHandler, MixinHandler, WsockHandler, PrivateKey, InvalidValueError, SSHClient\n)\n\ntry:\n    from unittest.mock import Mock\nexcept ImportError:\n    from mock import Mock\n\n\nclass TestMixinHandler(unittest.TestCase):\n\n    def test_is_forbidden(self):\n        mhandler = MixinHandler()\n        handler.redirecting = True\n        options.fbidhttp = True\n\n        context = Mock(\n            address=('8.8.8.8', 8888),\n            trusted_downstream=['127.0.0.1'],\n            _orig_protocol='http'\n        )\n        hostname = '4.4.4.4'\n        self.assertTrue(mhandler.is_forbidden(context, hostname))\n\n        context = Mock(\n            address=('8.8.8.8', 8888),\n            trusted_downstream=[],\n            _orig_protocol='http'\n        )\n        hostname = 'www.google.com'\n        self.assertEqual(mhandler.is_forbidden(context, hostname), False)\n\n        context = Mock(\n            address=('8.8.8.8', 8888),\n            trusted_downstream=[],\n            _orig_protocol='http'\n        )\n        hostname = '4.4.4.4'\n        self.assertTrue(mhandler.is_forbidden(context, hostname))\n\n        context = Mock(\n            address=('192.168.1.1', 8888),\n            trusted_downstream=[],\n            _orig_protocol='http'\n        )\n        hostname = 'www.google.com'\n        self.assertIsNone(mhandler.is_forbidden(context, hostname))\n\n        options.fbidhttp = False\n        self.assertIsNone(mhandler.is_forbidden(context, hostname))\n\n        hostname = '4.4.4.4'\n        self.assertIsNone(mhandler.is_forbidden(context, hostname))\n\n        handler.redirecting = False\n        self.assertIsNone(mhandler.is_forbidden(context, hostname))\n\n        context._orig_protocol = 'https'\n        self.assertIsNone(mhandler.is_forbidden(context, hostname))\n\n    def test_get_redirect_url(self):\n        mhandler = MixinHandler()\n        hostname = 'www.example.com'\n        uri = '/'\n        port = 443\n\n        self.assertEqual(\n            mhandler.get_redirect_url(hostname, port, uri=uri),\n            'https://www.example.com/'\n        )\n\n        port = 4433\n        self.assertEqual(\n            mhandler.get_redirect_url(hostname, port, uri),\n            'https://www.example.com:4433/'\n        )\n\n    def test_get_client_addr(self):\n        mhandler = MixinHandler()\n        client_addr = ('8.8.8.8', 8888)\n        context_addr = ('127.0.0.1', 1234)\n        options.xheaders = True\n\n        mhandler.context = Mock(address=context_addr)\n        mhandler.get_real_client_addr = lambda: None\n        self.assertEqual(mhandler.get_client_addr(), context_addr)\n\n        mhandler.context = Mock(address=context_addr)\n        mhandler.get_real_client_addr = lambda: client_addr\n        self.assertEqual(mhandler.get_client_addr(), client_addr)\n\n        options.xheaders = False\n        mhandler.context = Mock(address=context_addr)\n        mhandler.get_real_client_addr = lambda: client_addr\n        self.assertEqual(mhandler.get_client_addr(), context_addr)\n\n    def test_get_real_client_addr(self):\n        x_forwarded_for = '1.1.1.1'\n        x_forwarded_port = 1111\n        x_real_ip = '2.2.2.2'\n        x_real_port = 2222\n        fake_port = 65535\n\n        mhandler = MixinHandler()\n        mhandler.request = HTTPServerRequest(uri='/')\n        mhandler.request.remote_ip = x_forwarded_for\n\n        self.assertIsNone(mhandler.get_real_client_addr())\n\n        mhandler.request.headers.add('X-Forwarded-For', x_forwarded_for)\n        self.assertEqual(mhandler.get_real_client_addr(),\n                         (x_forwarded_for, fake_port))\n\n        mhandler.request.headers.add('X-Forwarded-Port', fake_port + 1)\n        self.assertEqual(mhandler.get_real_client_addr(),\n                         (x_forwarded_for, fake_port))\n\n        mhandler.request.headers['X-Forwarded-Port'] = x_forwarded_port\n        self.assertEqual(mhandler.get_real_client_addr(),\n                         (x_forwarded_for, x_forwarded_port))\n\n        mhandler.request.remote_ip = x_real_ip\n\n        mhandler.request.headers.add('X-Real-Ip', x_real_ip)\n        self.assertEqual(mhandler.get_real_client_addr(),\n                         (x_real_ip, fake_port))\n\n        mhandler.request.headers.add('X-Real-Port', fake_port + 1)\n        self.assertEqual(mhandler.get_real_client_addr(),\n                         (x_real_ip, fake_port))\n\n        mhandler.request.headers['X-Real-Port'] = x_real_port\n        self.assertEqual(mhandler.get_real_client_addr(),\n                         (x_real_ip, x_real_port))\n\n\nclass TestPrivateKey(unittest.TestCase):\n\n    def get_pk_obj(self, fname, password=None):\n        key = read_file(make_tests_data_path(fname))\n        return PrivateKey(key, password=password, filename=fname)\n\n    def _test_with_encrypted_key(self, fname, password, klass):\n        pk = self.get_pk_obj(fname, password='')\n        with self.assertRaises(InvalidValueError) as ctx:\n            pk.get_pkey_obj()\n        self.assertIn('Need a passphrase', str(ctx.exception))\n\n        pk = self.get_pk_obj(fname, password='wrongpass')\n        with self.assertRaises(InvalidValueError) as ctx:\n            pk.get_pkey_obj()\n        self.assertIn('wrong passphrase', str(ctx.exception))\n\n        pk = self.get_pk_obj(fname, password=password)\n        self.assertIsInstance(pk.get_pkey_obj(), klass)\n\n    def test_class_with_invalid_key_length(self):\n        key = u'a' * (PrivateKey.max_length + 1)\n\n        with self.assertRaises(InvalidValueError) as ctx:\n            PrivateKey(key)\n        self.assertIn('Invalid key length', str(ctx.exception))\n\n    def test_get_pkey_obj_with_invalid_key(self):\n        key = u'a b c'\n        fname = 'abc'\n\n        pk = PrivateKey(key, filename=fname)\n        with self.assertRaises(InvalidValueError) as ctx:\n            pk.get_pkey_obj()\n        self.assertIn('Invalid key {}'.format(fname), str(ctx.exception))\n\n    def test_get_pkey_obj_with_plain_rsa_key(self):\n        pk = self.get_pk_obj('test_rsa.key')\n        self.assertIsInstance(pk.get_pkey_obj(), paramiko.RSAKey)\n\n    def test_get_pkey_obj_with_plain_ed25519_key(self):\n        pk = self.get_pk_obj('test_ed25519.key')\n        self.assertIsInstance(pk.get_pkey_obj(), paramiko.Ed25519Key)\n\n    def test_get_pkey_obj_with_encrypted_rsa_key(self):\n        fname = 'test_rsa_password.key'\n        password = 'television'\n        self._test_with_encrypted_key(fname, password, paramiko.RSAKey)\n\n    def test_get_pkey_obj_with_encrypted_ed25519_key(self):\n        fname = 'test_ed25519_password.key'\n        password = 'abc123'\n        self._test_with_encrypted_key(fname, password, paramiko.Ed25519Key)\n\n    def test_get_pkey_obj_with_encrypted_new_rsa_key(self):\n        fname = 'test_new_rsa_password.key'\n        password = '123456'\n        self._test_with_encrypted_key(fname, password, paramiko.RSAKey)\n\n    def test_get_pkey_obj_with_plain_new_dsa_key(self):\n        pk = self.get_pk_obj('test_new_dsa.key')\n        self.assertIsInstance(pk.get_pkey_obj(), paramiko.DSSKey)\n\n    def test_parse_name(self):\n        key = u'-----BEGIN PRIVATE KEY-----'\n        pk = PrivateKey(key)\n        name, _ = pk.parse_name(pk.iostr, pk.tag_to_name)\n        self.assertIsNone(name)\n\n        key = u'-----BEGIN xxx PRIVATE KEY-----'\n        pk = PrivateKey(key)\n        name, _ = pk.parse_name(pk.iostr, pk.tag_to_name)\n        self.assertIsNone(name)\n\n        key = u'-----BEGIN  RSA PRIVATE KEY-----'\n        pk = PrivateKey(key)\n        name, _ = pk.parse_name(pk.iostr, pk.tag_to_name)\n        self.assertIsNone(name)\n\n        key = u'-----BEGIN RSA  PRIVATE KEY-----'\n        pk = PrivateKey(key)\n        name, _ = pk.parse_name(pk.iostr, pk.tag_to_name)\n        self.assertIsNone(name)\n\n        key = u'-----BEGIN RSA PRIVATE  KEY-----'\n        pk = PrivateKey(key)\n        name, _ = pk.parse_name(pk.iostr, pk.tag_to_name)\n        self.assertIsNone(name)\n\n        for tag, to_name in PrivateKey.tag_to_name.items():\n            key = u'-----BEGIN {} PRIVATE KEY----- \\r\\n'.format(tag)\n            pk = PrivateKey(key)\n            name, length = pk.parse_name(pk.iostr, pk.tag_to_name)\n            self.assertEqual(name, to_name)\n            self.assertEqual(length, len(key))\n\n\nclass TestWsockHandler(unittest.TestCase):\n\n    def test_check_origin(self):\n        request = HTTPServerRequest(uri='/')\n        obj = Mock(spec=WsockHandler, request=request)\n\n        obj.origin_policy = 'same'\n        request.headers['Host'] = 'www.example.com:4433'\n        origin = 'https://www.example.com:4433'\n        self.assertTrue(WsockHandler.check_origin(obj, origin))\n\n        origin = 'https://www.example.com'\n        self.assertFalse(WsockHandler.check_origin(obj, origin))\n\n        obj.origin_policy = 'primary'\n        self.assertTrue(WsockHandler.check_origin(obj, origin))\n\n        origin = 'https://blog.example.com'\n        self.assertTrue(WsockHandler.check_origin(obj, origin))\n\n        origin = 'https://blog.example.org'\n        self.assertFalse(WsockHandler.check_origin(obj, origin))\n\n        origin = 'https://blog.example.org'\n        obj.origin_policy = {'https://blog.example.org'}\n        self.assertTrue(WsockHandler.check_origin(obj, origin))\n\n        origin = 'http://blog.example.org'\n        obj.origin_policy = {'http://blog.example.org'}\n        self.assertTrue(WsockHandler.check_origin(obj, origin))\n\n        origin = 'http://blog.example.org'\n        obj.origin_policy = {'https://blog.example.org'}\n        self.assertFalse(WsockHandler.check_origin(obj, origin))\n\n        obj.origin_policy = '*'\n        origin = 'https://blog.example.org'\n        self.assertTrue(WsockHandler.check_origin(obj, origin))\n\n    def test_failed_weak_ref(self):\n        request = HTTPServerRequest(uri='/')\n        obj = Mock(spec=WsockHandler, request=request)\n        obj.src_addr = (\"127.0.0.1\", 8888)\n\n        class FakeWeakRef:\n            def __init__(self):\n                self.count = 0\n\n            def __call__(self):\n                self.count += 1\n                return None\n\n        ref = FakeWeakRef()\n        obj.worker_ref = ref\n        WsockHandler.on_message(obj, b'{\"data\": \"somestuff\"}')\n        self.assertGreaterEqual(ref.count, 1)\n        obj.close.assert_called_with(reason='No worker found')\n\n    def test_worker_closed(self):\n        request = HTTPServerRequest(uri='/')\n        obj = Mock(spec=WsockHandler, request=request)\n        obj.src_addr = (\"127.0.0.1\", 8888)\n\n        class Worker:\n            def __init__(self):\n                self.closed = True\n\n        class FakeWeakRef:\n            def __call__(self):\n                return Worker()\n\n        ref = FakeWeakRef()\n        obj.worker_ref = ref\n        WsockHandler.on_message(obj, b'{\"data\": \"somestuff\"}')\n        obj.close.assert_called_with(reason='Worker closed')\n\nclass TestIndexHandler(unittest.TestCase):\n    def test_null_in_encoding(self):\n        handler = Mock(spec=IndexHandler)\n\n        # This is a little nasty, but the index handler has a lot of\n        # dependencies to mock. Mocking out everything but the bits\n        # we want to test lets us test this case without needing to\n        # refactor the relevant code out of IndexHandler\n        def parse_encoding(data):\n            return IndexHandler.parse_encoding(handler, data)\n        handler.parse_encoding = parse_encoding\n\n        ssh = Mock(spec=SSHClient)\n        stdin = io.BytesIO()\n        stdout = io.BytesIO(initial_bytes=b\"UTF-8\\0\")\n        stderr = io.BytesIO()\n        ssh.exec_command.return_value = (stdin, stdout, stderr)\n\n        encoding = IndexHandler.get_default_encoding(handler, ssh)\n        self.assertEquals(\"utf-8\", encoding)\n\n"
  },
  {
    "path": "tests/test_main.py",
    "content": "import unittest\n\nfrom tornado.web import Application\nfrom webssh import handler\nfrom webssh.main import app_listen\n\n\nclass TestMain(unittest.TestCase):\n\n    def test_app_listen(self):\n        app = Application()\n        app.listen = lambda x, y, **kwargs: 1\n\n        handler.redirecting = None\n        server_settings = dict()\n        app_listen(app, 80, '127.0.0.1', server_settings)\n        self.assertFalse(handler.redirecting)\n\n        handler.redirecting = None\n        server_settings = dict(ssl_options='enabled')\n        app_listen(app, 80, '127.0.0.1', server_settings)\n        self.assertTrue(handler.redirecting)\n"
  },
  {
    "path": "tests/test_policy.py",
    "content": "import os\nimport unittest\nimport paramiko\n\nfrom shutil import copyfile\nfrom paramiko.client import RejectPolicy, WarningPolicy\nfrom tests.utils import make_tests_data_path\nfrom webssh.policy import (\n    AutoAddPolicy, get_policy_dictionary, load_host_keys,\n    get_policy_class, check_policy_setting\n)\n\n\nclass TestPolicy(unittest.TestCase):\n\n    def test_get_policy_dictionary(self):\n        classes = [AutoAddPolicy, RejectPolicy, WarningPolicy]\n        dic = get_policy_dictionary()\n        for cls in classes:\n            val = dic[cls.__name__.lower()]\n            self.assertIs(cls, val)\n\n    def test_load_host_keys(self):\n        path = '/path-not-exists'\n        host_keys = load_host_keys(path)\n        self.assertFalse(host_keys)\n\n        path = '/tmp'\n        host_keys = load_host_keys(path)\n        self.assertFalse(host_keys)\n\n        path = make_tests_data_path('known_hosts_example')\n        host_keys = load_host_keys(path)\n        self.assertEqual(host_keys, paramiko.hostkeys.HostKeys(path))\n\n    def test_get_policy_class(self):\n        keys = ['autoadd', 'reject', 'warning']\n        vals = [AutoAddPolicy, RejectPolicy, WarningPolicy]\n        for key, val in zip(keys, vals):\n            cls = get_policy_class(key)\n            self.assertIs(cls, val)\n\n        key = 'non-exists'\n        with self.assertRaises(ValueError):\n            get_policy_class(key)\n\n    def test_check_policy_setting(self):\n        host_keys_filename = make_tests_data_path('host_keys_test.db')\n        host_keys_settings = dict(\n            host_keys=paramiko.hostkeys.HostKeys(),\n            system_host_keys=paramiko.hostkeys.HostKeys(),\n            host_keys_filename=host_keys_filename\n        )\n\n        with self.assertRaises(ValueError):\n            check_policy_setting(RejectPolicy, host_keys_settings)\n\n        try:\n            os.unlink(host_keys_filename)\n        except OSError:\n            pass\n        check_policy_setting(AutoAddPolicy, host_keys_settings)\n        self.assertEqual(os.path.exists(host_keys_filename), True)\n\n    def test_is_missing_host_key(self):\n        client = paramiko.SSHClient()\n        file1 = make_tests_data_path('known_hosts_example')\n        file2 = make_tests_data_path('known_hosts_example2')\n        client.load_host_keys(file1)\n        client.load_system_host_keys(file2)\n\n        autoadd = AutoAddPolicy()\n        for f in [file1, file2]:\n            entry = paramiko.hostkeys.HostKeys(f)._entries[0]\n            hostname = entry.hostnames[0]\n            key = entry.key\n            self.assertIsNone(\n                autoadd.is_missing_host_key(client, hostname, key)\n            )\n\n        for f in [file1, file2]:\n            entry = paramiko.hostkeys.HostKeys(f)._entries[0]\n            hostname = entry.hostnames[0]\n            key = entry.key\n            key.get_name = lambda: 'unknown'\n            self.assertTrue(\n                autoadd.is_missing_host_key(client, hostname, key)\n            )\n        del key.get_name\n\n        for f in [file1, file2]:\n            entry = paramiko.hostkeys.HostKeys(f)._entries[0]\n            hostname = entry.hostnames[0][1:]\n            key = entry.key\n            self.assertTrue(\n                autoadd.is_missing_host_key(client, hostname, key)\n            )\n\n        file3 = make_tests_data_path('known_hosts_example3')\n        entry = paramiko.hostkeys.HostKeys(file3)._entries[0]\n        hostname = entry.hostnames[0]\n        key = entry.key\n        with self.assertRaises(paramiko.BadHostKeyException):\n            autoadd.is_missing_host_key(client, hostname, key)\n\n    def test_missing_host_key(self):\n        client = paramiko.SSHClient()\n        file1 = make_tests_data_path('known_hosts_example')\n        file2 = make_tests_data_path('known_hosts_example2')\n        filename = make_tests_data_path('known_hosts')\n        copyfile(file1, filename)\n        client.load_host_keys(filename)\n        n1 = len(client._host_keys)\n\n        autoadd = AutoAddPolicy()\n        entry = paramiko.hostkeys.HostKeys(file2)._entries[0]\n        hostname = entry.hostnames[0]\n        key = entry.key\n        autoadd.missing_host_key(client, hostname, key)\n        self.assertEqual(len(client._host_keys),  n1 + 1)\n        self.assertEqual(paramiko.hostkeys.HostKeys(filename),\n                         client._host_keys)\n        os.unlink(filename)\n"
  },
  {
    "path": "tests/test_settings.py",
    "content": "import io\nimport random\nimport ssl\nimport sys\nimport os.path\nimport unittest\nimport paramiko\nimport tornado.options as options\n\nfrom tests.utils import make_tests_data_path\nfrom webssh.policy import load_host_keys\nfrom webssh.settings import (\n    get_host_keys_settings, get_policy_setting, base_dir, get_font_filename,\n    get_ssl_context, get_trusted_downstream, get_origin_setting, print_version,\n    check_encoding_setting\n)\nfrom webssh.utils import UnicodeType\nfrom webssh._version import __version__\n\n\nclass TestSettings(unittest.TestCase):\n\n    def test_print_version(self):\n        sys_stdout = sys.stdout\n        sys.stdout = io.StringIO() if UnicodeType == str else io.BytesIO()\n\n        self.assertEqual(print_version(False), None)\n        self.assertEqual(sys.stdout.getvalue(), '')\n\n        with self.assertRaises(SystemExit):\n            self.assertEqual(print_version(True), None)\n        self.assertEqual(sys.stdout.getvalue(), __version__ + '\\n')\n\n        sys.stdout = sys_stdout\n\n    def test_get_host_keys_settings(self):\n        options.hostfile = ''\n        options.syshostfile = ''\n        dic = get_host_keys_settings(options)\n\n        filename = os.path.join(base_dir, 'known_hosts')\n        self.assertEqual(dic['host_keys'], load_host_keys(filename))\n        self.assertEqual(dic['host_keys_filename'], filename)\n        self.assertEqual(\n            dic['system_host_keys'],\n            load_host_keys(os.path.expanduser('~/.ssh/known_hosts'))\n        )\n\n        options.hostfile = make_tests_data_path('known_hosts_example')\n        options.syshostfile = make_tests_data_path('known_hosts_example2')\n        dic2 = get_host_keys_settings(options)\n        self.assertEqual(dic2['host_keys'], load_host_keys(options.hostfile))\n        self.assertEqual(dic2['host_keys_filename'], options.hostfile)\n        self.assertEqual(dic2['system_host_keys'],\n                         load_host_keys(options.syshostfile))\n\n    def test_get_policy_setting(self):\n        options.policy = 'warning'\n        options.hostfile = ''\n        options.syshostfile = ''\n        settings = get_host_keys_settings(options)\n        instance = get_policy_setting(options, settings)\n        self.assertIsInstance(instance, paramiko.client.WarningPolicy)\n\n        options.policy = 'autoadd'\n        options.hostfile = ''\n        options.syshostfile = ''\n        settings = get_host_keys_settings(options)\n        instance = get_policy_setting(options, settings)\n        self.assertIsInstance(instance, paramiko.client.AutoAddPolicy)\n        os.unlink(settings['host_keys_filename'])\n\n        options.policy = 'reject'\n        options.hostfile = ''\n        options.syshostfile = ''\n        settings = get_host_keys_settings(options)\n        try:\n            instance = get_policy_setting(options, settings)\n        except ValueError:\n            self.assertFalse(\n                settings['host_keys'] and settings['system_host_keys']\n            )\n        else:\n            self.assertIsInstance(instance, paramiko.client.RejectPolicy)\n\n    def test_get_ssl_context(self):\n        options.certfile = ''\n        options.keyfile = ''\n        ssl_ctx = get_ssl_context(options)\n        self.assertIsNone(ssl_ctx)\n\n        options.certfile = 'provided'\n        options.keyfile = ''\n        with self.assertRaises(ValueError) as ctx:\n            ssl_ctx = get_ssl_context(options)\n        self.assertEqual('keyfile is not provided', str(ctx.exception))\n\n        options.certfile = ''\n        options.keyfile = 'provided'\n        with self.assertRaises(ValueError) as ctx:\n            ssl_ctx = get_ssl_context(options)\n        self.assertEqual('certfile is not provided', str(ctx.exception))\n\n        options.certfile = 'FileDoesNotExist'\n        options.keyfile = make_tests_data_path('cert.key')\n        with self.assertRaises(ValueError) as ctx:\n            ssl_ctx = get_ssl_context(options)\n        self.assertIn('does not exist', str(ctx.exception))\n\n        options.certfile = make_tests_data_path('cert.key')\n        options.keyfile = 'FileDoesNotExist'\n        with self.assertRaises(ValueError) as ctx:\n            ssl_ctx = get_ssl_context(options)\n        self.assertIn('does not exist', str(ctx.exception))\n\n        options.certfile = make_tests_data_path('cert.key')\n        options.keyfile = make_tests_data_path('cert.key')\n        with self.assertRaises(ssl.SSLError) as ctx:\n            ssl_ctx = get_ssl_context(options)\n\n        options.certfile = make_tests_data_path('cert.crt')\n        options.keyfile = make_tests_data_path('cert.key')\n        ssl_ctx = get_ssl_context(options)\n        self.assertIsNotNone(ssl_ctx)\n\n    def test_get_trusted_downstream(self):\n        tdstream = ''\n        result = set()\n        self.assertEqual(get_trusted_downstream(tdstream), result)\n\n        tdstream = '1.1.1.1, 2.2.2.2'\n        result = set(['1.1.1.1', '2.2.2.2'])\n        self.assertEqual(get_trusted_downstream(tdstream), result)\n\n        tdstream = '1.1.1.1, 2.2.2.2, 2.2.2.2'\n        result = set(['1.1.1.1', '2.2.2.2'])\n        self.assertEqual(get_trusted_downstream(tdstream), result)\n\n        tdstream = '1.1.1.1, 2.2.2.'\n        with self.assertRaises(ValueError):\n            get_trusted_downstream(tdstream)\n\n    def test_get_origin_setting(self):\n        options.debug = False\n        options.origin = '*'\n        with self.assertRaises(ValueError):\n            get_origin_setting(options)\n\n        options.debug = True\n        self.assertEqual(get_origin_setting(options), '*')\n\n        options.origin = random.choice(['Same', 'Primary'])\n        self.assertEqual(get_origin_setting(options), options.origin.lower())\n\n        options.origin = ''\n        with self.assertRaises(ValueError):\n            get_origin_setting(options)\n\n        options.origin = ','\n        with self.assertRaises(ValueError):\n            get_origin_setting(options)\n\n        options.origin = 'www.example.com,  https://www.example.org'\n        result = {'http://www.example.com', 'https://www.example.org'}\n        self.assertEqual(get_origin_setting(options), result)\n\n        options.origin = 'www.example.com:80,  www.example.org:443'\n        result = {'http://www.example.com', 'https://www.example.org'}\n        self.assertEqual(get_origin_setting(options), result)\n\n    def test_get_font_setting(self):\n        font_dir = os.path.join(base_dir, 'tests', 'data', 'fonts')\n        font = ''\n        self.assertEqual(get_font_filename(font, font_dir), 'fake-font')\n\n        font = 'fake-font'\n        self.assertEqual(get_font_filename(font, font_dir), 'fake-font')\n\n        font = 'wrong-name'\n        with self.assertRaises(ValueError):\n            get_font_filename(font, font_dir)\n\n    def test_check_encoding_setting(self):\n        self.assertIsNone(check_encoding_setting(''))\n        self.assertIsNone(check_encoding_setting('utf-8'))\n        with self.assertRaises(ValueError):\n            check_encoding_setting('unknown-encoding')\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "import unittest\n\nfrom webssh.utils import (\n    is_valid_ip_address, is_valid_port, is_valid_hostname, to_str, to_bytes,\n    to_int, is_ip_hostname, is_same_primary_domain, parse_origin_from_url\n)\n\n\nclass TestUitls(unittest.TestCase):\n\n    def test_to_str(self):\n        b = b'hello'\n        u = u'hello'\n        self.assertEqual(to_str(b), u)\n        self.assertEqual(to_str(u), u)\n\n    def test_to_bytes(self):\n        b = b'hello'\n        u = u'hello'\n        self.assertEqual(to_bytes(b), b)\n        self.assertEqual(to_bytes(u), b)\n\n    def test_to_int(self):\n        self.assertEqual(to_int(''), None)\n        self.assertEqual(to_int(None), None)\n        self.assertEqual(to_int('22'), 22)\n        self.assertEqual(to_int(' 22 '), 22)\n\n    def test_is_valid_ip_address(self):\n        self.assertFalse(is_valid_ip_address('127.0.0'))\n        self.assertFalse(is_valid_ip_address(b'127.0.0'))\n        self.assertTrue(is_valid_ip_address('127.0.0.1'))\n        self.assertTrue(is_valid_ip_address(b'127.0.0.1'))\n        self.assertFalse(is_valid_ip_address('abc'))\n        self.assertFalse(is_valid_ip_address(b'abc'))\n        self.assertTrue(is_valid_ip_address('::1'))\n        self.assertTrue(is_valid_ip_address(b'::1'))\n        self.assertTrue(is_valid_ip_address('fe80::1111:2222:3333:4444'))\n        self.assertTrue(is_valid_ip_address(b'fe80::1111:2222:3333:4444'))\n        self.assertTrue(is_valid_ip_address('fe80::1111:2222:3333:4444%eth0'))\n        self.assertTrue(is_valid_ip_address(b'fe80::1111:2222:3333:4444%eth0'))\n\n    def test_is_valid_port(self):\n        self.assertTrue(is_valid_port(80))\n        self.assertFalse(is_valid_port(0))\n        self.assertFalse(is_valid_port(65536))\n\n    def test_is_valid_hostname(self):\n        self.assertTrue(is_valid_hostname('google.com'))\n        self.assertTrue(is_valid_hostname('google.com.'))\n        self.assertTrue(is_valid_hostname('www.google.com'))\n        self.assertTrue(is_valid_hostname('www.google.com.'))\n        self.assertFalse(is_valid_hostname('.www.google.com'))\n        self.assertFalse(is_valid_hostname('http://www.google.com'))\n        self.assertFalse(is_valid_hostname('https://www.google.com'))\n        self.assertFalse(is_valid_hostname('127.0.0.1'))\n        self.assertFalse(is_valid_hostname('::1'))\n\n    def test_is_ip_hostname(self):\n        self.assertTrue(is_ip_hostname('[::1]'))\n        self.assertTrue(is_ip_hostname('127.0.0.1'))\n        self.assertFalse(is_ip_hostname('localhost'))\n        self.assertFalse(is_ip_hostname('www.google.com'))\n\n    def test_is_same_primary_domain(self):\n        domain1 = 'localhost'\n        domain2 = 'localhost'\n        self.assertTrue(is_same_primary_domain(domain1, domain2))\n\n        domain1 = 'localhost'\n        domain2 = 'test'\n        self.assertFalse(is_same_primary_domain(domain1, domain2))\n\n        domain1 = 'com'\n        domain2 = 'example.com'\n        self.assertFalse(is_same_primary_domain(domain1, domain2))\n\n        domain1 = 'example.com'\n        domain2 = 'example.com'\n        self.assertTrue(is_same_primary_domain(domain1, domain2))\n\n        domain1 = 'www.example.com'\n        domain2 = 'example.com'\n        self.assertTrue(is_same_primary_domain(domain1, domain2))\n\n        domain1 = 'wwwexample.com'\n        domain2 = 'example.com'\n        self.assertFalse(is_same_primary_domain(domain1, domain2))\n\n        domain1 = 'www.example.com'\n        domain2 = 'www2.example.com'\n        self.assertTrue(is_same_primary_domain(domain1, domain2))\n\n        domain1 = 'xxx.www.example.com'\n        domain2 = 'xxx.www2.example.com'\n        self.assertTrue(is_same_primary_domain(domain1, domain2))\n\n    def test_parse_origin_from_url(self):\n        url = ''\n        self.assertIsNone(parse_origin_from_url(url))\n\n        url = 'www.example.com'\n        self.assertEqual(parse_origin_from_url(url), 'http://www.example.com')\n\n        url = 'http://www.example.com'\n        self.assertEqual(parse_origin_from_url(url), 'http://www.example.com')\n\n        url = 'www.example.com:80'\n        self.assertEqual(parse_origin_from_url(url), 'http://www.example.com')\n\n        url = 'http://www.example.com:80'\n        self.assertEqual(parse_origin_from_url(url), 'http://www.example.com')\n\n        url = 'www.example.com:443'\n        self.assertEqual(parse_origin_from_url(url), 'https://www.example.com')\n\n        url = 'https://www.example.com'\n        self.assertEqual(parse_origin_from_url(url), 'https://www.example.com')\n\n        url = 'https://www.example.com:443'\n        self.assertEqual(parse_origin_from_url(url), 'https://www.example.com')\n\n        url = 'https://www.example.com:80'\n        self.assertEqual(parse_origin_from_url(url), url)\n\n        url = 'http://www.example.com:443'\n        self.assertEqual(parse_origin_from_url(url), url)\n"
  },
  {
    "path": "tests/utils.py",
    "content": "import mimetypes\nimport os.path\nfrom uuid import uuid4\nfrom webssh.settings import base_dir\n\n\ndef encode_multipart_formdata(fields, files):\n    \"\"\"\n    fields is a sequence of (name, value) elements for regular form fields.\n    files is a sequence of (name, filename, value) elements for data to be\n    uploaded as files.\n    Return (content_type, body) ready for httplib.HTTP instance\n    \"\"\"\n    boundary = uuid4().hex\n    CRLF = '\\r\\n'\n    L = []\n    for (key, value) in fields:\n        L.append('--' + boundary)\n        L.append('Content-Disposition: form-data; name=\"%s\"' % key)\n        L.append('')\n        L.append(value)\n    for (key, filename, value) in files:\n        L.append('--' + boundary)\n        L.append(\n            'Content-Disposition: form-data; name=\"%s\"; filename=\"%s\"' % (\n                key, filename\n            )\n        )\n        L.append('Content-Type: %s' % get_content_type(filename))\n        L.append('')\n        L.append(value)\n    L.append('--' + boundary + '--')\n    L.append('')\n    body = CRLF.join(L)\n    content_type = 'multipart/form-data; boundary=%s' % boundary\n    return content_type, body\n\n\ndef get_content_type(filename):\n    return mimetypes.guess_type(filename)[0] or 'application/octet-stream'\n\n\ndef read_file(path, encoding='utf-8'):\n    with open(path, 'rb') as f:\n        data = f.read()\n        if encoding is None:\n            return data\n        return data.decode(encoding)\n\n\ndef make_tests_data_path(filename):\n    return os.path.join(base_dir, 'tests', 'data', filename)\n"
  },
  {
    "path": "user.js/Build-SSH-Link.user.js",
    "content": "// ==UserScript==\n// @name         Build SSH Link\n// @namespace    http://tampermonkey.net/\n// @version      0.1\n// @description  Build SSH link for huashengdun-webssh \n// @author       ǝɔ∀ǝdʎz∀ɹɔ 👽\n// @match        https://ssh.vps.vc/*\n// @match        https://ssh.hax.co.id/*\n// @match        https://ssh-crazypeace.koyeb.app/*\n// @icon         https://www.google.com/s2/favicons?sz=64&domain=koyeb.app\n// @grant        none\n// ==/UserScript==\n\n\n(function() {\n    'use strict';\n\n    // Your code here...\n    // 获取 form 元素\n    var form = document.getElementById(\"connect\");\n\n    /////////////////////\n    // 创建 `<button>` 元素\n    var buildLinkBtn = document.createElement(\"button\");\n\n    // 设置 `<button>` 的属性\n    buildLinkBtn.type=\"button\";\n    buildLinkBtn.className=\"btn btn-info\";\n    buildLinkBtn.innerHTML=\"buildSSHLink\";\n    buildLinkBtn.id=\"sshlinkBtnA\";\n\n    // 将 `<button>` 添加到 `<form>` 元素范围内部的尾部\n    form.appendChild(buildLinkBtn);\n\n    ////////////////////\n    // 创建 `<div>` 元素\n    var sshlinkdiv = document.createElement(\"div\");\n\n    // 设置 `<div>` 的属性\n    sshlinkdiv.id = \"sshlinkA\";\n\n    // 将 `<div>` 添加到 `<form>` 元素范围内部的尾部\n    form.appendChild(sshlinkdiv);\n\n    ////////////////////\n    // 让按钮的click事件 调用 updateSSHlinkA 函数\n    document.querySelector('#sshlinkBtnA').addEventListener(\"click\", updateSSHlinkA);\n})();\n\nfunction updateSSHlinkA() {\n    var thisPageProtocol = window.location.protocol;\n    var thisPageUrl = window.location.host;\n\n    var hostnamestr = document.getElementById(\"hostname\").value;\n    var portstr = document.getElementById(\"port\").value;\n    if (portstr == \"\") {\n        portstr = \"22\"\n    }\n    var usrnamestr = document.getElementById(\"username\").value;\n    if (usrnamestr == \"\") {\n        usrnamestr = \"root\"\n    }\n    var passwdstr = document.getElementById(\"password\").value;\n    var passwdstrAfterBase64 = window.btoa(passwdstr);\n\n    var sshlinkstr;\n    sshlinkstr = thisPageProtocol+\"//\"+thisPageUrl+\"/?hostname=\"+hostnamestr+\"&port=\"+portstr+\"&username=\"+usrnamestr+\"&password=\"+passwdstrAfterBase64;\n\n    document.getElementById(\"sshlinkA\").innerHTML = sshlinkstr;\n}\n"
  },
  {
    "path": "webssh/__init__.py",
    "content": "import sys\nfrom webssh._version import __version__, __version_info__\n\n\n__author__ = 'Shengdun Hua <webmaster0115@gmail.com>'\n\nif sys.platform == 'win32' and sys.version_info.major == 3 and \\\n        sys.version_info.minor >= 8:\n    import asyncio\n    asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())\n"
  },
  {
    "path": "webssh/_version.py",
    "content": "__version_info__ = (1, 6, 2)\n__version__ = '.'.join(map(str, __version_info__))\n"
  },
  {
    "path": "webssh/handler.py",
    "content": "import io\nimport json\nimport logging\nimport socket\nimport struct\nimport traceback\nimport weakref\nimport paramiko\nimport tornado.web\n\nfrom concurrent.futures import ThreadPoolExecutor\nfrom tornado.ioloop import IOLoop\nfrom tornado.options import options\nfrom tornado.process import cpu_count\nfrom webssh.utils import (\n    is_valid_ip_address, is_valid_port, is_valid_hostname, to_bytes, to_str,\n    to_int, to_ip_address, UnicodeType, is_ip_hostname, is_same_primary_domain,\n    is_valid_encoding\n)\nfrom webssh.worker import Worker, recycle_worker, clients\n\ntry:\n    from json.decoder import JSONDecodeError\nexcept ImportError:\n    JSONDecodeError = ValueError\n\ntry:\n    from urllib.parse import urlparse\nexcept ImportError:\n    from urlparse import urlparse\n\n\nDEFAULT_PORT = 22\n\nswallow_http_errors = True\nredirecting = None\n\n\nclass InvalidValueError(Exception):\n    pass\n\n\nclass SSHClient(paramiko.SSHClient):\n\n    def handler(self, title, instructions, prompt_list):\n        answers = []\n        for prompt_, _ in prompt_list:\n            prompt = prompt_.strip().lower()\n            if prompt.startswith('password'):\n                answers.append(self.password)\n            elif prompt.startswith('verification'):\n                answers.append(self.totp)\n            else:\n                raise ValueError('Unknown prompt: {}'.format(prompt_))\n        return answers\n\n    def auth_interactive(self, username, handler):\n        if not self.totp:\n            raise ValueError('Need a verification code for 2fa.')\n        self._transport.auth_interactive(username, handler)\n\n    def _auth(self, username, password, pkey, *args):\n        self.password = password\n        saved_exception = None\n        two_factor = False\n        allowed_types = set()\n        two_factor_types = {'keyboard-interactive', 'password'}\n\n        if pkey is not None:\n            logging.info('Trying publickey authentication')\n            try:\n                allowed_types = set(\n                    self._transport.auth_publickey(username, pkey)\n                )\n                two_factor = allowed_types & two_factor_types\n                if not two_factor:\n                    return\n            except paramiko.SSHException as e:\n                saved_exception = e\n\n        if two_factor:\n            logging.info('Trying publickey 2fa')\n            return self.auth_interactive(username, self.handler)\n\n        if password is not None:\n            logging.info('Trying password authentication')\n            try:\n                self._transport.auth_password(username, password)\n                return\n            except paramiko.SSHException as e:\n                saved_exception = e\n                allowed_types = set(getattr(e, 'allowed_types', []))\n                two_factor = allowed_types & two_factor_types\n\n        if two_factor:\n            logging.info('Trying password 2fa')\n            return self.auth_interactive(username, self.handler)\n\n        assert saved_exception is not None\n        raise saved_exception\n\n\nclass PrivateKey(object):\n\n    max_length = 16384  # rough number\n\n    tag_to_name = {\n        'RSA': 'RSA',\n        'DSA': 'DSS',\n        'EC': 'ECDSA',\n        'OPENSSH': 'Ed25519'\n    }\n\n    def __init__(self, privatekey, password=None, filename=''):\n        self.privatekey = privatekey\n        self.filename = filename\n        self.password = password\n        self.check_length()\n        self.iostr = io.StringIO(privatekey)\n        self.last_exception = None\n\n    def check_length(self):\n        if len(self.privatekey) > self.max_length:\n            raise InvalidValueError('Invalid key length.')\n\n    def parse_name(self, iostr, tag_to_name):\n        name = None\n        for line_ in iostr:\n            line = line_.strip()\n            if line and line.startswith('-----BEGIN ') and \\\n                    line.endswith(' PRIVATE KEY-----'):\n                lst = line.split(' ')\n                if len(lst) == 4:\n                    tag = lst[1]\n                    if tag:\n                        name = tag_to_name.get(tag)\n                        if name:\n                            break\n        return name, len(line_)\n\n    def get_specific_pkey(self, name, offset, password):\n        self.iostr.seek(offset)\n        logging.debug('Reset offset to {}.'.format(offset))\n\n        logging.debug('Try parsing it as {} type key'.format(name))\n        pkeycls = getattr(paramiko, name+'Key')\n        pkey = None\n\n        try:\n            pkey = pkeycls.from_private_key(self.iostr, password=password)\n        except paramiko.PasswordRequiredException:\n            raise InvalidValueError('Need a passphrase to decrypt the key.')\n        except (paramiko.SSHException, ValueError) as exc:\n            self.last_exception = exc\n            logging.debug(str(exc))\n\n        return pkey\n\n    def get_pkey_obj(self):\n        logging.info('Parsing private key {!r}'.format(self.filename))\n        name, length = self.parse_name(self.iostr, self.tag_to_name)\n        if not name:\n            raise InvalidValueError('Invalid key {}.'.format(self.filename))\n\n        offset = self.iostr.tell() - length\n        password = to_bytes(self.password) if self.password else None\n        pkey = self.get_specific_pkey(name, offset, password)\n\n        if pkey is None and name == 'Ed25519':\n            for name in ['RSA', 'ECDSA', 'DSS']:\n                pkey = self.get_specific_pkey(name, offset, password)\n                if pkey:\n                    break\n\n        if pkey:\n            return pkey\n\n        logging.error(str(self.last_exception))\n        msg = 'Invalid key'\n        if self.password:\n            msg += ' or wrong passphrase \"{}\" for decrypting it.'.format(\n                    self.password)\n        raise InvalidValueError(msg)\n\n\nclass MixinHandler(object):\n\n    custom_headers = {\n        'Server': 'TornadoServer'\n    }\n\n    html = ('<html><head><title>{code} {reason}</title></head><body>{code} '\n            '{reason}</body></html>')\n\n    def initialize(self, loop=None):\n        self.check_request()\n        self.loop = loop\n        self.origin_policy = self.settings.get('origin_policy')\n\n    def check_request(self):\n        context = self.request.connection.context\n        result = self.is_forbidden(context, self.request.host_name)\n        self._transforms = []\n        if result:\n            self.set_status(403)\n            self.finish(\n                self.html.format(code=self._status_code, reason=self._reason)\n            )\n        elif result is False:\n            to_url = self.get_redirect_url(\n                self.request.host_name, options.sslport, self.request.uri\n            )\n            self.redirect(to_url, permanent=True)\n        else:\n            self.context = context\n\n    def check_origin(self, origin):\n        if self.origin_policy == '*':\n            return True\n\n        parsed_origin = urlparse(origin)\n        netloc = parsed_origin.netloc.lower()\n        logging.debug('netloc: {}'.format(netloc))\n\n        host = self.request.headers.get('Host')\n        logging.debug('host: {}'.format(host))\n\n        if netloc == host:\n            return True\n\n        if self.origin_policy == 'same':\n            return False\n        elif self.origin_policy == 'primary':\n            return is_same_primary_domain(netloc.rsplit(':', 1)[0],\n                                          host.rsplit(':', 1)[0])\n        else:\n            return origin in self.origin_policy\n\n    def is_forbidden(self, context, hostname):\n        ip = context.address[0]\n        lst = context.trusted_downstream\n        ip_address = None\n\n        if lst and ip not in lst:\n            logging.warning(\n                'IP {!r} not found in trusted downstream {!r}'.format(ip, lst)\n            )\n            return True\n\n        if context._orig_protocol == 'http':\n            if redirecting and not is_ip_hostname(hostname):\n                ip_address = to_ip_address(ip)\n                if not ip_address.is_private:\n                    # redirecting\n                    return False\n\n            if options.fbidhttp:\n                if ip_address is None:\n                    ip_address = to_ip_address(ip)\n                if not ip_address.is_private:\n                    logging.warning('Public plain http request is forbidden.')\n                    return True\n\n    def get_redirect_url(self, hostname, port, uri):\n        port = '' if port == 443 else ':%s' % port\n        return 'https://{}{}{}'.format(hostname, port, uri)\n\n    def set_default_headers(self):\n        for header in self.custom_headers.items():\n            self.set_header(*header)\n\n    def get_value(self, name):\n        value = self.get_argument(name)\n        if not value:\n            raise InvalidValueError('Missing value {}'.format(name))\n        return value\n\n    def get_context_addr(self):\n        return self.context.address[:2]\n\n    def get_client_addr(self):\n        if options.xheaders:\n            return self.get_real_client_addr() or self.get_context_addr()\n        else:\n            return self.get_context_addr()\n\n    def get_real_client_addr(self):\n        ip = self.request.remote_ip\n\n        if ip == self.request.headers.get('X-Real-Ip'):\n            port = self.request.headers.get('X-Real-Port')\n        elif ip in self.request.headers.get('X-Forwarded-For', ''):\n            port = self.request.headers.get('X-Forwarded-Port')\n        else:\n            # not running behind an nginx server\n            return\n\n        port = to_int(port)\n        if port is None or not is_valid_port(port):\n            # fake port\n            port = 65535\n\n        return (ip, port)\n\n\nclass NotFoundHandler(MixinHandler, tornado.web.ErrorHandler):\n\n    def initialize(self):\n        super(NotFoundHandler, self).initialize()\n\n    def prepare(self):\n        raise tornado.web.HTTPError(404)\n\n\nclass IndexHandler(MixinHandler, tornado.web.RequestHandler):\n\n    executor = ThreadPoolExecutor(max_workers=cpu_count()*5)\n\n    def initialize(self, loop, policy, host_keys_settings):\n        super(IndexHandler, self).initialize(loop)\n        self.policy = policy\n        self.host_keys_settings = host_keys_settings\n        self.ssh_client = self.get_ssh_client()\n        self.debug = self.settings.get('debug', False)\n        self.font = self.settings.get('font', '')\n        self.result = dict(id=None, status=None, encoding=None)\n\n    def write_error(self, status_code, **kwargs):\n        if swallow_http_errors and self.request.method == 'POST':\n            exc_info = kwargs.get('exc_info')\n            if exc_info:\n                reason = getattr(exc_info[1], 'log_message', None)\n                if reason:\n                    self._reason = reason\n            self.result.update(status=self._reason)\n            self.set_status(200)\n            self.finish(self.result)\n        else:\n            super(IndexHandler, self).write_error(status_code, **kwargs)\n\n    def get_ssh_client(self):\n        ssh = SSHClient()\n        ssh._system_host_keys = self.host_keys_settings['system_host_keys']\n        ssh._host_keys = self.host_keys_settings['host_keys']\n        ssh._host_keys_filename = self.host_keys_settings['host_keys_filename']\n        ssh.set_missing_host_key_policy(self.policy)\n        return ssh\n\n    def get_privatekey(self):\n        name = 'privatekey'\n        lst = self.request.files.get(name)\n        if lst:\n            # multipart form\n            filename = lst[0]['filename']\n            data = lst[0]['body']\n            value = self.decode_argument(data, name=name).strip()\n        else:\n            # urlencoded form\n            value = self.get_argument(name, u'')\n            filename = ''\n\n        return value, filename\n\n    def get_hostname(self):\n        value = self.get_value('hostname')\n        if not (is_valid_hostname(value) or is_valid_ip_address(value)):\n            raise InvalidValueError('Invalid hostname: {}'.format(value))\n        return value\n\n    def get_port(self):\n        value = self.get_argument('port', u'')\n        if not value:\n            return DEFAULT_PORT\n\n        port = to_int(value)\n        if port is None or not is_valid_port(port):\n            raise InvalidValueError('Invalid port: {}'.format(value))\n        return port\n\n    def lookup_hostname(self, hostname, port):\n        key = hostname if port == 22 else '[{}]:{}'.format(hostname, port)\n\n        if self.ssh_client._system_host_keys.lookup(key) is None:\n            if self.ssh_client._host_keys.lookup(key) is None:\n                raise tornado.web.HTTPError(\n                        403, 'Connection to {}:{} is not allowed.'.format(\n                            hostname, port)\n                    )\n\n    def get_args(self):\n        hostname = self.get_hostname()\n        port = self.get_port()\n        username = self.get_value('username')\n        password = self.get_argument('password', u'')\n        privatekey, filename = self.get_privatekey()\n        passphrase = self.get_argument('passphrase', u'')\n        totp = self.get_argument('totp', u'')\n\n        if isinstance(self.policy, paramiko.RejectPolicy):\n            self.lookup_hostname(hostname, port)\n\n        if privatekey:\n            pkey = PrivateKey(privatekey, passphrase, filename).get_pkey_obj()\n        else:\n            pkey = None\n\n        self.ssh_client.totp = totp\n        args = (hostname, port, username, password, pkey)\n        logging.debug(args)\n\n        return args\n\n    def parse_encoding(self, data):\n        try:\n            encoding = to_str(data.strip(), 'ascii')\n        except UnicodeDecodeError:\n            return\n\n        if is_valid_encoding(encoding):\n            return encoding\n\n    def get_default_encoding(self, ssh):\n        commands = [\n            '$SHELL -ilc \"locale charmap\"',\n            '$SHELL -ic \"locale charmap\"'\n        ]\n\n        for command in commands:\n            try:\n                _, stdout, _ = ssh.exec_command(command,\n                                                get_pty=True,\n                                                timeout=1)\n            except paramiko.SSHException as exc:\n                logging.info(str(exc))\n            else:\n                try:\n                    data = stdout.read()\n                except socket.timeout:\n                    pass\n                else:\n                    logging.debug('{!r} => {!r}'.format(command, data))\n                    result = self.parse_encoding(data)\n                    if result:\n                        return result\n\n        logging.warning('Could not detect the default encoding.')\n        return 'utf-8'\n\n    def ssh_connect(self, args):\n        ssh = self.ssh_client\n        dst_addr = args[:2]\n        logging.info('Connecting to {}:{}'.format(*dst_addr))\n\n        try:\n            ssh.connect(*args, timeout=options.timeout)\n        except socket.error:\n            raise ValueError('Unable to connect to {}:{}'.format(*dst_addr))\n        except paramiko.BadAuthenticationType:\n            raise ValueError('Bad authentication type.')\n        except paramiko.AuthenticationException:\n            raise ValueError('Authentication failed.')\n        except paramiko.BadHostKeyException:\n            raise ValueError('Bad host key.')\n\n        term = self.get_argument('term', u'') or u'xterm'\n        chan = ssh.invoke_shell(term=term)\n        chan.setblocking(0)\n        worker = Worker(self.loop, ssh, chan, dst_addr)\n        worker.encoding = options.encoding if options.encoding else \\\n            self.get_default_encoding(ssh)\n        return worker\n\n    def check_origin(self):\n        event_origin = self.get_argument('_origin', u'')\n        header_origin = self.request.headers.get('Origin')\n        origin = event_origin or header_origin\n\n        if origin:\n            if not super(IndexHandler, self).check_origin(origin):\n                raise tornado.web.HTTPError(\n                    403, 'Cross origin operation is not allowed.'\n                )\n\n            if not event_origin and self.origin_policy != 'same':\n                self.set_header('Access-Control-Allow-Origin', origin)\n\n    def head(self):\n        pass\n\n    def get(self):\n        self.render('index.html', debug=self.debug, font=self.font)\n\n    @tornado.gen.coroutine\n    def post(self):\n        if self.debug and self.get_argument('error', u''):\n            # for testing purpose only\n            raise ValueError('Uncaught exception')\n\n        ip, port = self.get_client_addr()\n        workers = clients.get(ip, {})\n        if workers and len(workers) >= options.maxconn:\n            raise tornado.web.HTTPError(403, 'Too many live connections.')\n\n        self.check_origin()\n\n        try:\n            args = self.get_args()\n        except InvalidValueError as exc:\n            raise tornado.web.HTTPError(400, str(exc))\n\n        future = self.executor.submit(self.ssh_connect, args)\n\n        try:\n            worker = yield future\n        except (ValueError, paramiko.SSHException) as exc:\n            logging.error(traceback.format_exc())\n            self.result.update(status=str(exc))\n        else:\n            if not workers:\n                clients[ip] = workers\n            worker.src_addr = (ip, port)\n            workers[worker.id] = worker\n            self.loop.call_later(options.delay, recycle_worker, worker)\n            self.result.update(id=worker.id, encoding=worker.encoding)\n\n        self.write(self.result)\n\n\nclass WsockHandler(MixinHandler, tornado.websocket.WebSocketHandler):\n\n    def initialize(self, loop):\n        super(WsockHandler, self).initialize(loop)\n        self.worker_ref = None\n\n    def open(self):\n        self.src_addr = self.get_client_addr()\n        logging.info('Connected from {}:{}'.format(*self.src_addr))\n\n        workers = clients.get(self.src_addr[0])\n        if not workers:\n            self.close(reason='Websocket authentication failed.')\n            return\n\n        try:\n            worker_id = self.get_value('id')\n        except (tornado.web.MissingArgumentError, InvalidValueError) as exc:\n            self.close(reason=str(exc))\n        else:\n            worker = workers.get(worker_id)\n            if worker:\n                workers[worker_id] = None\n                self.set_nodelay(True)\n                worker.set_handler(self)\n                self.worker_ref = weakref.ref(worker)\n                self.loop.add_handler(worker.fd, worker, IOLoop.READ)\n            else:\n                self.close(reason='Websocket authentication failed.')\n\n    def on_message(self, message):\n        logging.debug('{!r} from {}:{}'.format(message, *self.src_addr))\n        worker = self.worker_ref()\n        if not worker:\n            # The worker has likely been closed. Do not process.\n            logging.debug(\n                \"received message to closed worker from {}:{}\".format(\n                    *self.src_addr\n                )\n            )\n            self.close(reason='No worker found')\n            return\n\n        if worker.closed:\n            self.close(reason='Worker closed')\n            return\n\n        try:\n            msg = json.loads(message)\n        except JSONDecodeError:\n            return\n\n        if not isinstance(msg, dict):\n            return\n\n        resize = msg.get('resize')\n        if resize and len(resize) == 2:\n            try:\n                worker.chan.resize_pty(*resize)\n            except (TypeError, struct.error, paramiko.SSHException):\n                pass\n\n        data = msg.get('data')\n        if data and isinstance(data, UnicodeType):\n            worker.data_to_dst.append(data)\n            worker.on_write()\n\n    def on_close(self):\n        logging.info('Disconnected from {}:{}'.format(*self.src_addr))\n        if not self.close_reason:\n            self.close_reason = 'client disconnected'\n\n        worker = self.worker_ref() if self.worker_ref else None\n        if worker:\n            worker.close(reason=self.close_reason)\n"
  },
  {
    "path": "webssh/main.py",
    "content": "import logging\nimport tornado.web\nimport tornado.ioloop\n\nfrom tornado.options import options\nfrom webssh import handler\nfrom webssh.handler import IndexHandler, WsockHandler, NotFoundHandler\nfrom webssh.settings import (\n    get_app_settings,  get_host_keys_settings, get_policy_setting,\n    get_ssl_context, get_server_settings, check_encoding_setting\n)\n\n\ndef make_handlers(loop, options):\n    host_keys_settings = get_host_keys_settings(options)\n    policy = get_policy_setting(options, host_keys_settings)\n\n    handlers = [\n        (r'/', IndexHandler, dict(loop=loop, policy=policy,\n                                  host_keys_settings=host_keys_settings)),\n        (r'/ws', WsockHandler, dict(loop=loop))\n    ]\n    return handlers\n\n\ndef make_app(handlers, settings):\n    settings.update(default_handler_class=NotFoundHandler)\n    return tornado.web.Application(handlers, **settings)\n\n\ndef app_listen(app, port, address, server_settings):\n    app.listen(port, address, **server_settings)\n    if not server_settings.get('ssl_options'):\n        server_type = 'http'\n    else:\n        server_type = 'https'\n        handler.redirecting = True if options.redirect else False\n    logging.info(\n        'Listening on {}:{} ({})'.format(address, port, server_type)\n    )\n\n\ndef main():\n    options.parse_command_line()\n    check_encoding_setting(options.encoding)\n    loop = tornado.ioloop.IOLoop.current()\n    app = make_app(make_handlers(loop, options), get_app_settings(options))\n    ssl_ctx = get_ssl_context(options)\n    server_settings = get_server_settings(options)\n    app_listen(app, options.port, options.address, server_settings)\n    if ssl_ctx:\n        server_settings.update(ssl_options=ssl_ctx)\n        app_listen(app, options.sslport, options.ssladdress, server_settings)\n    loop.start()\n\n\nif __name__ == '__main__':\n    main()\n"
  },
  {
    "path": "webssh/policy.py",
    "content": "import logging\nimport os.path\nimport threading\nimport paramiko\n\n\ndef load_host_keys(path):\n    if os.path.exists(path) and os.path.isfile(path):\n        return paramiko.hostkeys.HostKeys(filename=path)\n    return paramiko.hostkeys.HostKeys()\n\n\ndef get_policy_dictionary():\n    dic = {\n       k.lower(): v for k, v in vars(paramiko.client).items() if type(v)\n       is type and issubclass(v, paramiko.client.MissingHostKeyPolicy)\n       and v is not paramiko.client.MissingHostKeyPolicy\n    }\n    return dic\n\n\ndef get_policy_class(policy):\n    origin_policy = policy\n    policy = policy.lower()\n    if not policy.endswith('policy'):\n        policy += 'policy'\n\n    dic = get_policy_dictionary()\n    logging.debug(dic)\n\n    try:\n        cls = dic[policy]\n    except KeyError:\n        raise ValueError('Unknown policy {!r}'.format(origin_policy))\n    return cls\n\n\ndef check_policy_setting(policy_class, host_keys_settings):\n    host_keys = host_keys_settings['host_keys']\n    host_keys_filename = host_keys_settings['host_keys_filename']\n    system_host_keys = host_keys_settings['system_host_keys']\n\n    if policy_class is paramiko.client.AutoAddPolicy:\n        host_keys.save(host_keys_filename)  # for permission test\n    elif policy_class is paramiko.client.RejectPolicy:\n        if not host_keys and not system_host_keys:\n            raise ValueError(\n                'Reject policy could not be used without host keys.'\n            )\n\n\nclass AutoAddPolicy(paramiko.client.MissingHostKeyPolicy):\n    \"\"\"\n    thread-safe AutoAddPolicy\n    \"\"\"\n    lock = threading.Lock()\n\n    def is_missing_host_key(self, client, hostname, key):\n        k = client._system_host_keys.lookup(hostname) or \\\n                client._host_keys.lookup(hostname)\n        if k is None:\n            return True\n        host_key = k.get(key.get_name(), None)\n        if host_key is None:\n            return True\n        if host_key != key:\n            raise paramiko.BadHostKeyException(hostname, key, host_key)\n\n    def missing_host_key(self, client, hostname, key):\n        with self.lock:\n            if self.is_missing_host_key(client, hostname, key):\n                keytype = key.get_name()\n                logging.info(\n                    'Adding {} host key for {}'.format(keytype, hostname)\n                )\n                client._host_keys._entries.append(\n                    paramiko.hostkeys.HostKeyEntry([hostname], key)\n                )\n\n                with open(client._host_keys_filename, 'a') as f:\n                    f.write('{} {} {}\\n'.format(\n                        hostname, keytype, key.get_base64()\n                    ))\n\n\nparamiko.client.AutoAddPolicy = AutoAddPolicy\n"
  },
  {
    "path": "webssh/settings.py",
    "content": "import logging\nimport os.path\nimport ssl\nimport sys\n\nfrom tornado.options import define\nfrom webssh.policy import (\n    load_host_keys, get_policy_class, check_policy_setting\n)\nfrom webssh.utils import (\n    to_ip_address, parse_origin_from_url, is_valid_encoding\n)\nfrom webssh._version import __version__\n\n\ndef print_version(flag):\n    if flag:\n        print(__version__)\n        sys.exit(0)\n\n\ndefine('address', default='', help='Listen address')\ndefine('port', type=int, default=8888,  help='Listen port')\ndefine('ssladdress', default='', help='SSL listen address')\ndefine('sslport', type=int, default=4433,  help='SSL listen port')\ndefine('certfile', default='', help='SSL certificate file')\ndefine('keyfile', default='', help='SSL private key file')\ndefine('debug', type=bool, default=False, help='Debug mode')\ndefine('policy', default='warning',\n       help='Missing host key policy, reject|autoadd|warning')\ndefine('hostfile', default='', help='User defined host keys file')\ndefine('syshostfile', default='', help='System wide host keys file')\ndefine('tdstream', default='', help='Trusted downstream, separated by comma')\ndefine('redirect', type=bool, default=True, help='Redirecting http to https')\ndefine('fbidhttp', type=bool, default=True,\n       help='Forbid public plain http incoming requests')\ndefine('xheaders', type=bool, default=True, help='Support xheaders')\ndefine('xsrf', type=bool, default=True, help='CSRF protection')\ndefine('origin', default='same', help='''Origin policy,\n'same': same origin policy, matches host name and port number;\n'primary': primary domain policy, matches primary domain only;\n'<domains>': custom domains policy, matches any domain in the <domains> list\nseparated by comma;\n'*': wildcard policy, matches any domain, allowed in debug mode only.''')\ndefine('wpintvl', type=float, default=0, help='Websocket ping interval')\ndefine('timeout', type=float, default=3, help='SSH connection timeout')\ndefine('delay', type=float, default=3, help='The delay to call recycle_worker')\ndefine('maxconn', type=int, default=20,\n       help='Maximum live connections (ssh sessions) per client')\ndefine('font', default='', help='custom font filename')\ndefine('encoding', default='utf-8',\n       help='''The default character encoding of ssh servers.\nExample: --encoding='utf-8' to solve the problem with some switches&routers''')\ndefine('version', type=bool, help='Show version information',\n       callback=print_version)\n\n\nbase_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))\nfont_dirs = ['webssh', 'static', 'css', 'fonts']\nmax_body_size = 1 * 1024 * 1024\n\n\nclass Font(object):\n\n    def __init__(self, filename, dirs):\n        self.family = self.get_family(filename)\n        self.url = self.get_url(filename, dirs)\n\n    def get_family(self, filename):\n        return filename.split('.')[0]\n\n    def get_url(self, filename, dirs):\n        return '/'.join(dirs + [filename])\n\n\ndef get_app_settings(options):\n    settings = dict(\n        template_path=os.path.join(base_dir, 'webssh', 'templates'),\n        static_path=os.path.join(base_dir, 'webssh', 'static'),\n        websocket_ping_interval=options.wpintvl,\n        debug=options.debug,\n        xsrf_cookies=options.xsrf,\n        font=Font(\n            get_font_filename(options.font,\n                              os.path.join(base_dir, *font_dirs)),\n            font_dirs[1:]\n        ),\n        origin_policy=get_origin_setting(options)\n    )\n    return settings\n\n\ndef get_server_settings(options):\n    settings = dict(\n        xheaders=options.xheaders,\n        max_body_size=max_body_size,\n        trusted_downstream=get_trusted_downstream(options.tdstream)\n    )\n    return settings\n\n\ndef get_host_keys_settings(options):\n    if not options.hostfile:\n        host_keys_filename = os.path.join(base_dir, 'known_hosts')\n    else:\n        host_keys_filename = options.hostfile\n    host_keys = load_host_keys(host_keys_filename)\n\n    if not options.syshostfile:\n        filename = os.path.expanduser('~/.ssh/known_hosts')\n    else:\n        filename = options.syshostfile\n    system_host_keys = load_host_keys(filename)\n\n    settings = dict(\n        host_keys=host_keys,\n        system_host_keys=system_host_keys,\n        host_keys_filename=host_keys_filename\n    )\n    return settings\n\n\ndef get_policy_setting(options, host_keys_settings):\n    policy_class = get_policy_class(options.policy)\n    logging.info(policy_class.__name__)\n    check_policy_setting(policy_class, host_keys_settings)\n    return policy_class()\n\n\ndef get_ssl_context(options):\n    if not options.certfile and not options.keyfile:\n        return None\n    elif not options.certfile:\n        raise ValueError('certfile is not provided')\n    elif not options.keyfile:\n        raise ValueError('keyfile is not provided')\n    elif not os.path.isfile(options.certfile):\n        raise ValueError('File {!r} does not exist'.format(options.certfile))\n    elif not os.path.isfile(options.keyfile):\n        raise ValueError('File {!r} does not exist'.format(options.keyfile))\n    else:\n        ssl_ctx = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH)\n        ssl_ctx.load_cert_chain(options.certfile, options.keyfile)\n        return ssl_ctx\n\n\ndef get_trusted_downstream(tdstream):\n    result = set()\n    for ip in tdstream.split(','):\n        ip = ip.strip()\n        if ip:\n            to_ip_address(ip)\n            result.add(ip)\n    return result\n\n\ndef get_origin_setting(options):\n    if options.origin == '*':\n        if not options.debug:\n            raise ValueError(\n                'Wildcard origin policy is only allowed in debug mode.'\n            )\n        else:\n            return '*'\n\n    origin = options.origin.lower()\n    if origin in ['same', 'primary']:\n        return origin\n\n    origins = set()\n    for url in origin.split(','):\n        orig = parse_origin_from_url(url)\n        if orig:\n            origins.add(orig)\n\n    if not origins:\n        raise ValueError('Empty origin list')\n\n    return origins\n\n\ndef get_font_filename(font, font_dir):\n    filenames = {f for f in os.listdir(font_dir) if not f.startswith('.')\n                 and os.path.isfile(os.path.join(font_dir, f))}\n    if font:\n        if font not in filenames:\n            raise ValueError(\n                'Font file {!r} not found'.format(os.path.join(font_dir, font))\n            )\n    elif filenames:\n        font = filenames.pop()\n\n    return font\n\n\ndef check_encoding_setting(encoding):\n    if encoding and not is_valid_encoding(encoding):\n        raise ValueError('Unknown character encoding {!r}.'.format(encoding))\n"
  },
  {
    "path": "webssh/static/css/fonts/.gitignore",
    "content": ""
  },
  {
    "path": "webssh/static/js/main.js",
    "content": "/*jslint browser:true */\n\nvar jQuery;\nvar wssh = {};\n\njQuery(function ($) {\n  var status = $('#status'),\n    button = $('.btn-primary'),\n    form_container = $('.form-container'),\n    waiter = $('#waiter'),\n    term_type = $('#term'),\n    style = {},\n    default_title = 'WebSSH',\n    title_element = document.querySelector('title'),\n    form_id = '#connect',\n    debug = document.querySelector(form_id).noValidate,\n    custom_font = document.fonts ? document.fonts.values().next().value : undefined,\n    default_fonts,\n    DISCONNECTED = 0,\n    CONNECTING = 1,\n    CONNECTED = 2,\n    state = DISCONNECTED,\n    messages = { 1: 'This client is connecting ...', 2: 'This client is already connnected.' },\n    key_max_size = 16384,\n    fields = ['hostname', 'port', 'username'],\n    form_keys = fields.concat(['password', 'totp']),\n    opts_keys = ['bgcolor', 'title', 'encoding', 'command', 'term', 'fontsize', 'fontcolor', 'cursor'],\n    url_form_data = {},\n    url_opts_data = {},\n    validated_form_data,\n    event_origin,\n    hostname_tester = /((^\\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]))\\s*$)|(^\\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)(\\.(25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)){3}))|:)))(%.+)?\\s*$))|(^\\s*((?=.{1,255}$)(?=.*[A-Za-z].*)[0-9A-Za-z](?:(?:[0-9A-Za-z]|\\b-){0,61}[0-9A-Za-z])?(?:\\.[0-9A-Za-z](?:(?:[0-9A-Za-z]|\\b-){0,61}[0-9A-Za-z])?)*)\\s*$)/;\n\n\n  function store_items(names, data) {\n    var i, name, value;\n\n    for (i = 0; i < names.length; i++) {\n      name = names[i];\n      value = data.get(name);\n      if (value) {\n        window.localStorage.setItem(name, value);\n      }\n    }\n  }\n\n\n  function restore_items(names) {\n    var i, name, value;\n\n    for (i = 0; i < names.length; i++) {\n      name = names[i];\n      value = window.localStorage.getItem(name);\n      if (value) {\n        $('#' + name).val(value);\n      }\n    }\n  }\n\n\n  function populate_form(data) {\n    var names = form_keys.concat(['passphrase']),\n      i, name;\n\n    for (i = 0; i < names.length; i++) {\n      name = names[i];\n      $('#' + name).val(data.get(name));\n    }\n  }\n\n\n  function get_object_length(object) {\n    return Object.keys(object).length;\n  }\n\n\n  function decode_uri_component(uri) {\n    try {\n      return decodeURIComponent(uri);\n    } catch (e) {\n      console.error(e);\n    }\n    return '';\n  }\n\n\n  function decode_password(encoded) {\n    try {\n      return window.atob(encoded);\n    } catch (e) {\n      console.error(e);\n    }\n    return null;\n  }\n\n\n  function parse_url_data(string, form_keys, opts_keys, form_map, opts_map) {\n    var i, pair, key, val,\n      arr = string.split('&');\n\n    for (i = 0; i < arr.length; i++) {\n      pair = arr[i].split('=');\n      key = pair[0].trim().toLowerCase();\n      val = pair.slice(1).join('=').trim();\n\n      if (form_keys.indexOf(key) >= 0) {\n        form_map[key] = val;\n      } else if (opts_keys.indexOf(key) >= 0) {\n        opts_map[key] = val;\n      }\n    }\n\n    if (form_map.password) {\n      form_map.password = decode_password(form_map.password);\n    }\n  }\n\n\n  function parse_xterm_style() {\n    var text = $('.xterm-helpers style').text();\n    var arr = text.split('xterm-normal-char{width:');\n    style.width = parseFloat(arr[1]);\n    arr = text.split('div{height:');\n    style.height = parseFloat(arr[1]);\n  }\n\n\n  function get_cell_size(term) {\n    style.width = term._core._renderService._renderer.dimensions.actualCellWidth;\n    style.height = term._core._renderService._renderer.dimensions.actualCellHeight;\n  }\n\n\n  function toggle_fullscreen(term) {\n    $('#terminal .terminal').toggleClass('fullscreen');\n    term.fitAddon.fit();\n  }\n\n\n  function current_geometry(term) {\n    if (!style.width || !style.height) {\n      try {\n        get_cell_size(term);\n      } catch (TypeError) {\n        parse_xterm_style();\n      }\n    }\n\n    var cols = parseInt(window.innerWidth / style.width, 10) - 1;\n    var rows = parseInt(window.innerHeight / style.height, 10);\n    return { 'cols': cols, 'rows': rows };\n  }\n\n\n  function resize_terminal(term) {\n    var geometry = current_geometry(term);\n    term.on_resize(geometry.cols, geometry.rows);\n  }\n\n\n  function set_backgound_color(term, color) {\n    term.setOption('theme', {\n      background: color\n    });\n  }\n\n  function set_font_color(term, color) {\n    term.setOption('theme', {\n      foreground: color\n    });\n  }\n\n  function custom_font_is_loaded() {\n    if (!custom_font) {\n      console.log('No custom font specified.');\n    } else {\n      console.log('Status of custom font ' + custom_font.family + ': ' + custom_font.status);\n      if (custom_font.status === 'loaded') {\n        return true;\n      }\n      if (custom_font.status === 'unloaded') {\n        return false;\n      }\n    }\n  }\n\n  function update_font_family(term) {\n    if (term.font_family_updated) {\n      console.log('Already using custom font family');\n      return;\n    }\n\n    if (!default_fonts) {\n      default_fonts = term.getOption('fontFamily');\n    }\n\n    if (custom_font_is_loaded()) {\n      var new_fonts = custom_font.family + ', ' + default_fonts;\n      term.setOption('fontFamily', new_fonts);\n      term.font_family_updated = true;\n      console.log('Using custom font family ' + new_fonts);\n    }\n  }\n\n\n  function reset_font_family(term) {\n    if (!term.font_family_updated) {\n      console.log('Already using default font family');\n      return;\n    }\n\n    if (default_fonts) {\n      term.setOption('fontFamily', default_fonts);\n      term.font_family_updated = false;\n      console.log('Using default font family ' + default_fonts);\n    }\n  }\n\n\n  function format_geometry(cols, rows) {\n    return JSON.stringify({ 'cols': cols, 'rows': rows });\n  }\n\n\n  function read_as_text_with_decoder(file, callback, decoder) {\n    var reader = new window.FileReader();\n\n    if (decoder === undefined) {\n      decoder = new window.TextDecoder('utf-8', { 'fatal': true });\n    }\n\n    reader.onload = function () {\n      var text;\n      try {\n        text = decoder.decode(reader.result);\n      } catch (TypeError) {\n        console.log('Decoding error happened.');\n      } finally {\n        if (callback) {\n          callback(text);\n        }\n      }\n    };\n\n    reader.onerror = function (e) {\n      console.error(e);\n    };\n\n    reader.readAsArrayBuffer(file);\n  }\n\n\n  function read_as_text_with_encoding(file, callback, encoding) {\n    var reader = new window.FileReader();\n\n    if (encoding === undefined) {\n      encoding = 'utf-8';\n    }\n\n    reader.onload = function () {\n      if (callback) {\n        callback(reader.result);\n      }\n    };\n\n    reader.onerror = function (e) {\n      console.error(e);\n    };\n\n    reader.readAsText(file, encoding);\n  }\n\n\n  function read_file_as_text(file, callback, decoder) {\n    if (!window.TextDecoder) {\n      read_as_text_with_encoding(file, callback, decoder);\n    } else {\n      read_as_text_with_decoder(file, callback, decoder);\n    }\n  }\n\n\n  function reset_wssh() {\n    var name;\n\n    for (name in wssh) {\n      if (wssh.hasOwnProperty(name) && name !== 'connect') {\n        delete wssh[name];\n      }\n    }\n  }\n\n\n  function log_status(text, to_populate) {\n    console.log(text);\n    status.html(text.split('\\n').join('<br/>'));\n\n    if (to_populate && validated_form_data) {\n      populate_form(validated_form_data);\n      validated_form_data = undefined;\n    }\n\n    if (waiter.css('display') !== 'none') {\n      waiter.hide();\n    }\n\n    if (form_container.css('display') === 'none') {\n      form_container.show();\n    }\n  }\n\n\n  function ajax_complete_callback(resp) {\n    button.prop('disabled', false);\n\n    if (resp.status !== 200) {\n      log_status(resp.status + ': ' + resp.statusText, true);\n      state = DISCONNECTED;\n      return;\n    }\n\n    var msg = resp.responseJSON;\n    if (!msg.id) {\n      log_status(msg.status, true);\n      state = DISCONNECTED;\n      return;\n    }\n\n    var ws_url = window.location.href.split(/\\?|#/, 1)[0].replace('http', 'ws'),\n      join = (ws_url[ws_url.length - 1] === '/' ? '' : '/'),\n      url = ws_url + join + 'ws?id=' + msg.id,\n      sock = new window.WebSocket(url),\n      encoding = 'utf-8',\n      decoder = window.TextDecoder ? new window.TextDecoder(encoding) : encoding,\n      terminal = document.getElementById('terminal'),\n      termOptions = {\n        cursorBlink: true,\n        theme: {\n          background: url_opts_data.bgcolor || 'black',\n          foreground: url_opts_data.fontcolor || 'white',\n          cursor: url_opts_data.cursor || url_opts_data.fontcolor || 'white'\n        }\n      };\n\n    if (url_opts_data.fontsize) {\n      var fontsize = window.parseInt(url_opts_data.fontsize);\n      if (fontsize && fontsize > 0) {\n        termOptions.fontSize = fontsize;\n      }\n    }\n\n    var term = new window.Terminal(termOptions);\n\n    term.fitAddon = new window.FitAddon.FitAddon();\n    term.loadAddon(term.fitAddon);\n\n    console.log(url);\n    if (!msg.encoding) {\n      console.log('Unable to detect the default encoding of your server');\n      msg.encoding = encoding;\n    } else {\n      console.log('The deault encoding of your server is ' + msg.encoding);\n    }\n\n    function term_write(text) {\n      if (term) {\n        term.write(text);\n        if (!term.resized) {\n          resize_terminal(term);\n          term.resized = true;\n        }\n      }\n    }\n\n    function set_encoding(new_encoding) {\n      // for console use\n      if (!new_encoding) {\n        console.log('An encoding is required');\n        return;\n      }\n\n      if (!window.TextDecoder) {\n        decoder = new_encoding;\n        encoding = decoder;\n        console.log('Set encoding to ' + encoding);\n      } else {\n        try {\n          decoder = new window.TextDecoder(new_encoding);\n          encoding = decoder.encoding;\n          console.log('Set encoding to ' + encoding);\n        } catch (RangeError) {\n          console.log('Unknown encoding ' + new_encoding);\n          return false;\n        }\n      }\n    }\n\n    wssh.set_encoding = set_encoding;\n\n    if (url_opts_data.encoding) {\n      if (set_encoding(url_opts_data.encoding) === false) {\n        set_encoding(msg.encoding);\n      }\n    } else {\n      set_encoding(msg.encoding);\n    }\n\n\n    wssh.geometry = function () {\n      // for console use\n      var geometry = current_geometry(term);\n      console.log('Current window geometry: ' + JSON.stringify(geometry));\n    };\n\n    wssh.send = function (data) {\n      // for console use\n      if (!sock) {\n        console.log('Websocket was already closed');\n        return;\n      }\n\n      if (typeof data !== 'string') {\n        console.log('Only string is allowed');\n        return;\n      }\n\n      try {\n        JSON.parse(data);\n        sock.send(data);\n      } catch (SyntaxError) {\n        data = data.trim() + '\\r';\n        sock.send(JSON.stringify({ 'data': data }));\n      }\n    };\n\n    wssh.reset_encoding = function () {\n      // for console use\n      if (encoding === msg.encoding) {\n        console.log('Already reset to ' + msg.encoding);\n      } else {\n        set_encoding(msg.encoding);\n      }\n    };\n\n    wssh.resize = function (cols, rows) {\n      // for console use\n      if (term === undefined) {\n        console.log('Terminal was already destroryed');\n        return;\n      }\n\n      var valid_args = false;\n\n      if (cols > 0 && rows > 0) {\n        var geometry = current_geometry(term);\n        if (cols <= geometry.cols && rows <= geometry.rows) {\n          valid_args = true;\n        }\n      }\n\n      if (!valid_args) {\n        console.log('Unable to resize terminal to geometry: ' + format_geometry(cols, rows));\n      } else {\n        term.on_resize(cols, rows);\n      }\n    };\n\n    wssh.set_bgcolor = function (color) {\n      set_backgound_color(term, color);\n    };\n\n    wssh.set_fontcolor = function (color) {\n      set_font_color(term, color);\n    };\n\n    wssh.custom_font = function () {\n      update_font_family(term);\n    };\n\n    wssh.default_font = function () {\n      reset_font_family(term);\n    };\n\n    term.on_resize = function (cols, rows) {\n      if (cols !== this.cols || rows !== this.rows) {\n        console.log('Resizing terminal to geometry: ' + format_geometry(cols, rows));\n        this.resize(cols, rows);\n        sock.send(JSON.stringify({ 'resize': [cols, rows] }));\n      }\n    };\n\n    term.onData(function (data) {\n      // console.log(data);\n      sock.send(JSON.stringify({ 'data': data }));\n    });\n\n    sock.onopen = function () {\n      // 连接成功时隐藏waiter\n      waiter.hide();\n      document.querySelector('.github-corner').classList.add('hidden');\n      term.open(terminal);\n      toggle_fullscreen(term);\n      update_font_family(term);\n      term.focus();\n      state = CONNECTED;\n      title_element.text = url_opts_data.title || default_title;\n      if (url_opts_data.command) {\n        setTimeout(function () {\n          sock.send(JSON.stringify({ 'data': url_opts_data.command + '\\r' }));\n        }, 500);\n      }\n    };\n\n    sock.onmessage = function (msg) {\n      read_file_as_text(msg.data, term_write, decoder);\n    };\n\n    sock.onerror = function (e) {\n      // 连接错误时隐藏waiter\n      waiter.hide();\n      document.querySelector('.github-corner').classList.remove('hidden');\n      console.error(e);\n    };\n\n    sock.onclose = function (e) {\n      term.dispose();\n      term = undefined;\n      sock = undefined;\n      reset_wssh();\n      log_status(e.reason, true);\n      state = DISCONNECTED;\n      default_title = 'WebSSH';\n      title_element.text = default_title;\n    };\n\n    $(window).resize(function () {\n      if (term) {\n        resize_terminal(term);\n      }\n    });\n  }\n\n\n  function wrap_object(opts) {\n    var obj = {};\n\n    obj.get = function (attr) {\n      return opts[attr] || '';\n    };\n\n    obj.set = function (attr, val) {\n      opts[attr] = val;\n    };\n\n    return obj;\n  }\n\n\n  function clean_data(data) {\n    var i, attr, val;\n    var attrs = form_keys.concat(['privatekey', 'passphrase']);\n\n    for (i = 0; i < attrs.length; i++) {\n      attr = attrs[i];\n      val = data.get(attr);\n      if (typeof val === 'string') {\n        data.set(attr, val.trim());\n      }\n    }\n  }\n\n\n  function validate_form_data(data) {\n    clean_data(data);\n\n    var hostname = data.get('hostname'),\n      port = data.get('port'),\n      username = data.get('username'),\n      pk = data.get('privatekey'),\n      result = {\n        valid: false,\n        data: data,\n        title: ''\n      },\n      errors = [], size;\n\n    if (!hostname) {\n      errors.push('Value of hostname is required.');\n    } else {\n      if (!hostname_tester.test(hostname)) {\n        errors.push('Invalid hostname: ' + hostname);\n      }\n    }\n\n    if (!port) {\n      port = 22;\n    } else {\n      if (!(port > 0 && port <= 65535)) {\n        errors.push('Invalid port: ' + port);\n      }\n    }\n\n    if (!username) {\n      errors.push('Value of username is required.');\n    }\n\n    if (pk) {\n      size = pk.size || pk.length;\n      if (size > key_max_size) {\n        errors.push('Invalid private key: ' + pk.name || '');\n      }\n    }\n\n    if (!errors.length || debug) {\n      result.valid = true;\n      result.title = username + '@' + hostname + ':' + port;\n    }\n    result.errors = errors;\n\n    return result;\n  }\n\n  // Fix empty input file ajax submission error for safari 11.x\n  function disable_file_inputs(inputs) {\n    var i, input;\n\n    for (i = 0; i < inputs.length; i++) {\n      input = inputs[i];\n      if (input.files.length === 0) {\n        input.setAttribute('disabled', '');\n      }\n    }\n  }\n\n\n  function enable_file_inputs(inputs) {\n    var i;\n\n    for (i = 0; i < inputs.length; i++) {\n      inputs[i].removeAttribute('disabled');\n    }\n  }\n\n\n  function connect_without_options() {\n    // use data from the form\n    var form = document.querySelector(form_id),\n      inputs = form.querySelectorAll('input[type=\"file\"]'),\n      url = form.action,\n      data, pk;\n\n    disable_file_inputs(inputs);\n    data = new FormData(form);\n    pk = data.get('privatekey');\n    enable_file_inputs(inputs);\n\n    function ajax_post() {\n      status.text('');\n      button.prop('disabled', true);\n\n      $.ajax({\n        url: url,\n        type: 'post',\n        data: data,\n        complete: ajax_complete_callback,\n        cache: false,\n        contentType: false,\n        processData: false\n      });\n    }\n\n    var result = validate_form_data(data);\n    if (!result.valid) {\n      log_status(result.errors.join('\\n'));\n      return;\n    }\n\n    if (pk && pk.size && !debug) {\n      read_file_as_text(pk, function (text) {\n        if (text === undefined) {\n          log_status('Invalid private key: ' + pk.name);\n        } else {\n          ajax_post();\n        }\n      });\n    } else {\n      ajax_post();\n    }\n\n    return result;\n  }\n\n\n  function connect_with_options(data) {\n    // use data from the arguments\n    var form = document.querySelector(form_id),\n      url = data.url || form.action,\n      _xsrf = form.querySelector('input[name=\"_xsrf\"]');\n\n    var result = validate_form_data(wrap_object(data));\n    if (!result.valid) {\n      log_status(result.errors.join('\\n'));\n      return;\n    }\n\n    data.term = term_type.val();\n    data._xsrf = _xsrf.value;\n    if (event_origin) {\n      data._origin = event_origin;\n    }\n\n    status.text('');\n    button.prop('disabled', true);\n\n    $.ajax({\n      url: url,\n      type: 'post',\n      data: data,\n      complete: ajax_complete_callback\n    });\n\n    return result;\n  }\n\n\n  function connect(hostname, port, username, password, privatekey, passphrase, totp) {\n    // for console use\n    var result, opts;\n\n    if (state !== DISCONNECTED) {\n      console.log(messages[state]);\n      return;\n    }\n\n    if (hostname === undefined) {\n      result = connect_without_options();\n    } else {\n      if (typeof hostname === 'string') {\n        opts = {\n          hostname: hostname,\n          port: port,\n          username: username,\n          password: password,\n          privatekey: privatekey,\n          passphrase: passphrase,\n          totp: totp\n        };\n      } else {\n        opts = hostname;\n      }\n\n      result = connect_with_options(opts);\n    }\n\n    if (result) {\n      state = CONNECTING;\n      default_title = result.title;\n      if (hostname) {\n        validated_form_data = result.data;\n      }\n      store_items(fields, result.data);\n    }\n  }\n\n  wssh.connect = connect;\n\n  $(form_id).submit(function (event) {\n    event.preventDefault();\n    connect();\n  });\n\n\n  function cross_origin_connect(event) {\n    console.log(event.origin);\n    var prop = 'connect',\n      args;\n\n    try {\n      args = JSON.parse(event.data);\n    } catch (SyntaxError) {\n      args = event.data.split('|');\n    }\n\n    if (!Array.isArray(args)) {\n      args = [args];\n    }\n\n    try {\n      event_origin = event.origin;\n      wssh[prop].apply(wssh, args);\n    } finally {\n      event_origin = undefined;\n    }\n  }\n\n  window.addEventListener('message', cross_origin_connect, false);\n\n  if (document.fonts) {\n    document.fonts.ready.then(\n      function () {\n        if (custom_font_is_loaded() === false) {\n          document.body.style.fontFamily = custom_font.family;\n        }\n      }\n    );\n  }\n\n\n  parse_url_data(\n    decode_uri_component(window.location.search.substring(1)) + '&' + decode_uri_component(window.location.hash.substring(1)),\n    form_keys, opts_keys, url_form_data, url_opts_data\n  );\n  // console.log(url_form_data);\n  // console.log(url_opts_data);\n\n  if (url_opts_data.term) {\n    term_type.val(url_opts_data.term);\n  }\n\n  if (url_form_data.password === null) {\n    log_status('Password via url must be encoded in base64.');\n  } else {\n    if (get_object_length(url_form_data)) {\n      waiter.show();\n      connect(url_form_data);\n    } else {\n      restore_items(fields);\n      form_container.show();\n    }\n  }\n\n});\n"
  },
  {
    "path": "webssh/static/js/service-worker.js",
    "content": "const CACHE_NAME = 'webssh-cache-v1';\r\nconst urlsToCache = [\r\n    '/',\r\n    '/static/css/bootstrap.min.css',\r\n    '/static/css/xterm.min.css',\r\n    '/static/css/fullscreen.min.css',\r\n    '/static/js/jquery.min.js',\r\n    '/static/js/popper.min.js',\r\n    '/static/js/bootstrap.min.js',\r\n    '/static/js/xterm.min.js',\r\n    '/static/js/xterm-addon-fit.min.js',\r\n    '/static/js/main.js',\r\n    '/static/img/favicon-16.png',\r\n    '/static/img/favicon-32.png',\r\n    '/static/img/favicon-96.png'\r\n];\r\n\r\nself.addEventListener('install', event => {\r\n    event.waitUntil(\r\n        caches.open(CACHE_NAME)\r\n            .then(cache => cache.addAll(urlsToCache))\r\n    );\r\n});\r\n\r\nself.addEventListener('fetch', event => {\r\n    event.respondWith(\r\n        caches.match(event.request)\r\n            .then(response => response || fetch(event.request))\r\n    );\r\n});\r\n"
  },
  {
    "path": "webssh/static/manifest.json",
    "content": "{\r\n    \"name\": \"WebSSH Console\",\r\n    \"short_name\": \"WebSSH\",\r\n    \"description\": \"Web-based SSH Client\",\r\n    \"start_url\": \"/\",\r\n    \"display\": \"standalone\",\r\n    \"background_color\": \"#ffffff\",\r\n    \"theme_color\": \"#2c3e50\",\r\n    \"icons\": [\r\n        {\r\n            \"src\": \"img/favicon-16.png\",\r\n            \"sizes\": \"16x16\",\r\n            \"type\": \"image/png\"\r\n        },\r\n        {\r\n            \"src\": \"img/favicon-32.png\",\r\n            \"sizes\": \"32x32\",\r\n            \"type\": \"image/png\"\r\n        },\r\n        {\r\n            \"src\": \"img/favicon-96.png\",\r\n            \"sizes\": \"96x96\",\r\n            \"type\": \"image/png\"\r\n        }\r\n    ]\r\n}"
  },
  {
    "path": "webssh/templates/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n\n<head>\n  <meta charset=\"UTF-8\">\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n  <title>WebSSH Console</title>\n  <link href=\"static/img/favicon-32.png\" rel=\"icon\" type=\"image/png\">\n  <link href=\"static/css/bootstrap.min.css\" rel=\"stylesheet\" type=\"text/css\" />\n  <link href=\"static/css/xterm.min.css\" rel=\"stylesheet\" type=\"text/css\" />\n  <link href=\"static/css/fullscreen.min.css\" rel=\"stylesheet\" type=\"text/css\" />\n  <link rel=\"stylesheet\" href=\"https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css\">\n  <link rel=\"manifest\" href=\"static/manifest.json\">\n  <meta name=\"theme-color\" content=\"#2c3e50\">\n  <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n  <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n  <meta name=\"apple-mobile-web-app-title\" content=\"WebSSH\">\n  <link rel=\"apple-touch-icon\" href=\"static/img/favicon-32.png\">\n  <style>\n    :root {\n      --primary-color: #2c3e50;\n      --secondary-color: #34495e;\n      --accent-color: #3498db;\n      --danger-color: #e74c3c;\n      --success-color: #2ecc71;\n      --warning-color: #f1c40f;\n      --light-bg: #ecf0f1;\n      --card-shadow: 0 10px 20px rgba(0, 0, 0, 0.1);\n    }\n\n    .main-container {\n      max-width: 1000px;\n      width: 100%;\n      padding: 2rem;\n      /* 移动内边距到容器 */\n    }\n\n    .form-container {\n      background: white;\n      border-radius: 15px;\n      box-shadow: var(--card-shadow);\n      padding: 2.5rem;\n      margin-bottom: 2rem;\n      backdrop-filter: blur(10px);\n      border: 1px solid rgba(255, 255, 255, 0.2);\n      height: auto;\n      /* 改为自动高度 */\n    }\n\n    .page-title {\n      color: var(--primary-color);\n      font-weight: 600;\n      font-size: 2rem;\n      margin-bottom: 2rem;\n      text-align: center;\n      position: relative;\n    }\n\n    .page-title:after {\n      content: '';\n      display: block;\n      width: 60px;\n      height: 4px;\n      background: var(--accent-color);\n      margin: 1rem auto;\n      border-radius: 2px;\n    }\n\n    .form-control {\n      border: 2px solid #eee;\n      border-radius: 8px;\n      padding: 1rem 1.2rem;\n      /* 增加内边距 */\n      height: calc(3rem + 2px);\n      /* 设置固定高度 */\n      line-height: 1.5;\n      /* 设置行高 */\n      transition: all 0.3s ease;\n      background: #f8f9fa;\n    }\n\n    .form-control:focus {\n      border-color: var(--accent-color);\n      box-shadow: 0 0 0 3px rgba(52, 152, 219, 0.1);\n      background: white;\n    }\n\n    label {\n      font-weight: 500;\n      color: var(--secondary-color);\n      margin-bottom: 0.5rem;\n      font-size: 0.9rem;\n    }\n\n    .form-section {\n      margin-bottom: 1.5rem;\n    }\n\n    .btn {\n      border-radius: 8px;\n      padding: 0.8rem 1.5rem;\n      font-weight: 500;\n      letter-spacing: 0.3px;\n      transition: all 0.3s ease;\n      text-transform: uppercase;\n      font-size: 0.9rem;\n    }\n\n    .btn-primary {\n      background: var(--accent-color);\n      border: none;\n      box-shadow: 0 4px 15px rgba(52, 152, 219, 0.2);\n    }\n\n    .btn-primary:hover {\n      background: #2980b9;\n      transform: translateY(-2px);\n    }\n\n    .btn-danger {\n      background: var(--danger-color);\n      border: none;\n    }\n\n    .btn-info {\n      background: var(--success-color);\n      border: none;\n    }\n\n    #waiter {\n      position: fixed;\n      top: 50%;\n      left: 50%;\n      transform: translate(-50%, -50%);\n      background: rgba(0, 0, 0, 0.9);\n      color: white;\n      padding: 1.5rem 3rem;\n      border-radius: 50px;\n      font-size: 1.1rem;\n      backdrop-filter: blur(5px);\n      z-index: 1000;\n    }\n\n    #terminal {\n      margin-top: 1.5rem;\n      border-radius: 10px;\n      overflow: hidden;\n      box-shadow: var(--card-shadow);\n    }\n\n    #sshlink {\n      height: 70px;\n      /* 固定高度 */\n      flex: 1;\n      overflow-x: auto;\n      white-space: nowrap;\n      margin: 0;\n      padding: 0.5rem 1rem;\n      background: var(--light-bg);\n      border-radius: 8px;\n      font-size: 0.9rem;\n      transition: opacity 0.3s ease;\n      display: flex;\n      align-items: center;\n      transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);\n    }\n\n    .link-text {\n      padding: 0.5rem;\n      flex: 1;\n      overflow-x: scroll;\n      white-space: nowrap;\n      scrollbar-width: none;\n      -ms-overflow-style: none;\n    }\n\n    .link-text::-webkit-scrollbar {\n      display: none;\n    }\n\n    .github-corner {\n      position: fixed;\n      top: 0;\n      right: 0;\n      z-index: 100;\n      transition: opacity 0.3s ease;\n      /* 添加过渡效果 */\n    }\n\n    .github-corner.hidden {\n      opacity: 0;\n      pointer-events: none;\n    }\n\n    .github-corner svg {\n      fill: var(--accent-color);\n      color: var(--light-bg);\n      position: absolute;\n      top: 0;\n      right: 0;\n      border: 0;\n      width: 80px;\n      height: 80px;\n    }\n\n    .github-corner:hover .octo-arm {\n      animation: octocat-wave 560ms ease-in-out;\n    }\n\n    .action-buttons {\n      display: flex;\n      align-items: center;\n      gap: 1rem;\n      margin-top: 2rem;\n      margin-bottom: 2rem;\n      /* 添加底部间距 */\n    }\n\n    .button-group {\n      display: flex;\n      gap: 1rem;\n      flex-shrink: 0;\n      /* 防止按钮被压缩 */\n    }\n\n    /* 文件选择按钮的基本样式 */\n    .file-btn {\n      /* 移除右上角和右下角的圆角 */\n      border-top-right-radius: 0;\n      border-bottom-right-radius: 0;\n      /* 设置内边距,使按钮更大更易点击 */\n      padding: 0.8rem 1.2rem;\n      /* 使用预定义的主题色作为背景色 */\n      background: var(--secondary-color);\n      /* 移除边框 */\n      border: none;\n      /* 添加过渡效果,使样式变化更平滑 */\n      transition: all 0.3s ease;\n      /* 设置相对定位,为光效动画做准备 */\n      position: relative;\n      /* 隐藏超出按钮范围的内容 */\n      overflow: hidden;\n    }\n\n    /* 鼠标悬停时的按钮样式 */\n    .file-btn:hover {\n      /* 添加渐变背景,使按钮更有层次感 */\n      background: linear-gradient(45deg, var(--primary-color), var(--secondary-color));\n      /* 添加发光效果 */\n      box-shadow: 0 0 15px rgba(52, 152, 219, 0.5);\n      /* 轻微放大按钮,增加交互感 */\n      transform: scale(1.05);\n    }\n\n    /* 创建光效动画的元素 */\n    .file-btn:before {\n      /* 创建伪元素 */\n      content: '';\n      /* 绝对定位,相对于按钮定位 */\n      position: absolute;\n      top: 0;\n      /* 初始位置在按钮左侧外部 */\n      left: -100%;\n      width: 100%;\n      height: 100%;\n      /* 创建透明渐变,形成光效 */\n      background: linear-gradient(120deg,\n          transparent,\n          rgba(255, 255, 255, 0.2),\n          transparent);\n      /* 添加过渡效果 */\n      transition: 0.5s;\n    }\n\n    /* 鼠标悬停时触发光效动画 */\n    .file-btn:hover:before {\n      /* 将光效移动到按钮右侧,形成扫光效果 */\n      left: 100%;\n    }\n\n    .file-input {\n      border-top-left-radius: 0;\n      border-bottom-left-radius: 0;\n      background: #f8f9fa;\n    }\n\n    .input-group-btn {\n      margin: 0;\n      padding: 0;\n    }\n\n    .copy-btn {\n      color: var(--primary-color);\n      margin-left: auto;\n      cursor: pointer;\n    }\n\n    @media (max-width: 768px) {\n      .main-container {\n        padding: 1rem;\n        /* 移动端减小内边距 */\n      }\n\n      .form-container {\n        padding: 1.5rem;\n      }\n\n      .btn {\n        width: 100%;\n        margin-bottom: 0.5rem;\n      }\n\n      .github-corner:hover .octo-arm {\n        animation: none;\n      }\n\n      .github-corner .octo-arm {\n        animation: octocat-wave 560ms ease-in-out;\n      }\n\n      .action-buttons {\n        flex-direction: column;\n      }\n\n      .button-group {\n        width: 100%;\n        flex-direction: column;\n      }\n\n      #sshlink {\n        width: 100%;\n      }\n    }\n\n    {% if font.family %}\n\n    @font-face {\n      font-family: '{{ font.family }}';\n      src: url('{{ font.url }}');\n    }\n\n    body {\n      font-family: '{{ font.family }}',\n      -apple-system,\n      BlinkMacSystemFont,\n      'Segoe UI',\n      system-ui,\n      Roboto,\n      Oxygen,\n      Ubuntu,\n      Cantarell,\n      'Open Sans',\n      'Helvetica Neue',\n      sans-serif;\n      background: linear-gradient(135deg, #f6f8fa 0%, #e9ecef 100%);\n      min-height: 100vh;\n      padding: 0;\n      /* 移除原有内边距 */\n      color: var(--primary-color);\n      display: flex;\n      /* 添加flex布局 */\n      align-items: center;\n      /* 垂直居中 */\n      justify-content: center;\n      /* 水平居中 */\n      margin: 0;\n    }\n\n    {% end %}\n  </style>\n</head>\n\n<body>\n  <script>\n    function updateSSHlink() {\n      var thisPageProtocol = window.location.protocol;\n      var thisPageUrl = window.location.host;\n\n      var hostnamestr = document.getElementById(\"hostname\").value;\n      var portstr = document.getElementById(\"port\").value;\n      if (portstr == \"\") {\n        portstr = \"22\"\n      }\n      var usrnamestr = document.getElementById(\"username\").value;\n      if (usrnamestr == \"\") {\n        usrnamestr = \"root\"\n      }\n      var passwdstr = document.getElementById(\"password\").value;\n      var passwdstrAfterBase64 = window.btoa(passwdstr);\n\n      var initcmdstr = document.getElementById(\"initcmd\").value;\n      var initcmdstrAfterURI = encodeURIComponent(initcmdstr);\n\n      var sshlinkstr = thisPageProtocol + \"//\" + thisPageUrl + \"/?hostname=\" + hostnamestr + \"&port=\" + portstr + \"&username=\" + usrnamestr + \"&password=\" + passwdstrAfterBase64 + \"&command=\" + initcmdstrAfterURI;\n\n      // 获取元素\n      var sshlinkElement = document.querySelector(\"#sshlink .link-text\");\n      var sshlink = document.getElementById(\"sshlink\");\n\n      // 清空之前的内容\n      sshlinkElement.textContent = '';\n\n      // 显示容器\n      sshlink.classList.add('active');\n\n      // 实现打字机效果\n      let i = 0;\n      const typeWriter = () => {\n        if (i < sshlinkstr.length) {\n          sshlinkElement.textContent += sshlinkstr.charAt(i);\n          i++;\n          setTimeout(typeWriter, 6); // 每个字符之间的延迟\n        } else {\n          sshlinkElement.classList.remove('typing');\n        }\n      }\n\n      // 开始打字效果\n      setTimeout(typeWriter, 300); // 等待容器动画开始后再开始打字\n    }\n\n\n    function copySSHLink() {\n      const linkText = document.querySelector(\"#sshlink .link-text\").textContent;\n      navigator.clipboard.writeText(linkText).then(() => {\n        const copyBtn = document.querySelector(\".copy-btn\");\n        copyBtn.classList.remove(\"fa-copy\");\n        copyBtn.classList.add(\"fa-check\");\n        setTimeout(() => {\n          copyBtn.classList.remove(\"fa-check\");\n          copyBtn.classList.add(\"fa-copy\");\n        }, 2000);\n      });\n    }\n\n    // 确保DOM加载完成后绑定事件\n    document.addEventListener('DOMContentLoaded', function () {\n      document.getElementById('sshlinkBtn').addEventListener('click', updateSSHlink);\n    });\n\n    // 注册 Service Worker\n    if ('serviceWorker' in navigator) {\n      window.addEventListener('load', () => {\n        navigator.serviceWorker.register('/static/js/service-worker.js')\n          .then(registration => {\n            console.log('ServiceWorker 注册成功');\n          })\n          .catch(err => {\n            console.log('ServiceWorker 注册失败：', err);\n          });\n      });\n    }\n  </script>\n  <a href=\"https://github.com/cmliu/webssh\" target=\"_blank\" class=\"github-corner\" aria-label=\"View source on Github\">\n    <svg viewBox=\"0 0 250 250\" aria-hidden=\"true\">\n      <path d=\"M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z\"></path>\n      <path\n        d=\"M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2\"\n        fill=\"currentColor\" style=\"transform-origin: 130px 106px;\" class=\"octo-arm\"></path>\n      <path\n        d=\"M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z\"\n        fill=\"currentColor\" class=\"octo-body\"></path>\n    </svg>\n  </a>\n  <div id=\"waiter\" style=\"display: none\">正在建立连接...</div>\n\n  <div class=\"main-container\">\n    <div class=\"form-container\" style=\"display: none\">\n      <h1 class=\"page-title\">WebSSH Console</h1>\n\n      <form id=\"connect\" action=\"\" method=\"post\" enctype=\"multipart/form-data\" {% if debug %} novalidate{% end %}>\n        <div class=\"form-section\">\n          <div class=\"row\">\n            <div class=\"col-md-6 mb-3\">\n              <label for=\"hostname\">主机地址 (Hostname)</label>\n              <input class=\"form-control\" type=\"text\" id=\"hostname\" name=\"hostname\" value=\"\" required\n                placeholder=\"请输入主机地址\">\n            </div>\n            <div class=\"col-md-6 mb-3\">\n              <label for=\"port\">端口 (Port)</label>\n              <input class=\"form-control\" type=\"number\" id=\"port\" name=\"port\" placeholder=\"22\" value=\"\" min=1 max=65535>\n            </div>\n          </div>\n        </div>\n\n        <div class=\"form-section\">\n          <div class=\"row\">\n            <div class=\"col-md-6 mb-3\">\n              <label for=\"username\">用户名 (Username)</label>\n              <input class=\"form-control\" type=\"text\" id=\"username\" name=\"username\" value=\"\" required\n                placeholder=\"请输入用户名\">\n            </div>\n            <div class=\"col-md-6 mb-3\">\n              <label for=\"password\">密码 (Password)</label>\n              <input class=\"form-control\" type=\"password\" id=\"password\" name=\"password\" value=\"\" placeholder=\"请输入密码\">\n            </div>\n          </div>\n        </div>\n\n        <div class=\"form-section\">\n          <div class=\"row\">\n            <div class=\"col-md-6 mb-3\">\n              <label for=\"privatekey\">私钥 (Private Key)</label>\n              <div class=\"input-group\">\n                <label class=\"input-group-btn\">\n                  <span class=\"btn btn-primary file-btn\">\n                    <i class=\"fas fa-folder-open\"></i> 选择文件\n                    <input type=\"file\" id=\"privatekey\" name=\"privatekey\" style=\"display: none;\"\n                      onchange=\"document.getElementById('showFilename').value = this.files.length ? this.files[0].name : '未选择文件'\">\n                  </span>\n                </label>\n                <input type=\"text\" class=\"form-control file-input\" id=\"showFilename\" placeholder=\"未选择私钥文件\" readonly>\n              </div>\n            </div>\n            <div class=\"col-md-6 mb-3\">\n              <label for=\"passphrase\">密钥口令 (Passphrase)</label>\n              <input class=\"form-control\" type=\"password\" id=\"passphrase\" name=\"passphrase\" value=\"\"\n                placeholder=\"如果需要请输入密钥口令\">\n            </div>\n          </div>\n        </div>\n\n        <div class=\"form-section\">\n          <div class=\"row\">\n            <div class=\"col-md-6 mb-3\">\n              <label for=\"totp\">动态验证码 (TOTP)</label>\n              <input class=\"form-control\" type=\"password\" id=\"totp\" name=\"totp\" value=\"\" placeholder=\"如果启用请输入动态验证码\">\n            </div>\n            <div class=\"col-md-6 mb-3\">\n              <label for=\"initcmd\">初始命令 (Init Command)</label>\n              <input class=\"form-control\" type=\"text\" id=\"initcmd\" name=\"initcmd\" value=\"\" placeholder=\"登录后要执行的命令\">\n            </div>\n          </div>\n        </div>\n\n        <input type=\"hidden\" id=\"term\" name=\"term\" value=\"xterm-256color\">\n        {% module xsrf_form_html() %}\n\n        <div class=\"action-buttons\">\n          <div class=\"button-group\">\n            <button type=\"submit\" class=\"btn btn-primary\">\n              <i class=\"fas fa-terminal\"></i> 连接\n            </button>\n            <button type=\"reset\" class=\"btn btn-danger\">\n              <i class=\"fas fa-redo\"></i> 重置\n            </button>\n            <button type=\"button\" class=\"btn btn-info\" id=\"sshlinkBtn\">\n              <i class=\"fas fa-link\"></i> 生成链接\n            </button>\n          </div>\n        </div>\n\n        <div id=\"sshlink\">\n          <div class=\"link-text\"></div>\n          <i class=\"fas fa-copy copy-btn\" onclick=\"copySSHLink()\"></i>\n        </div>\n      </form>\n    </div>\n\n    <div class=\"terminal-container\">\n      <div id=\"status\" style=\"color: var(--danger-color);\"></div>\n      <div id=\"terminal\"></div>\n    </div>\n  </div>\n\n  <script src=\"static/js/jquery.min.js\"></script>\n  <script src=\"static/js/popper.min.js\"></script>\n  <script src=\"static/js/bootstrap.min.js\"></script>\n  <script src=\"static/js/xterm.min.js\"></script>\n  <script src=\"static/js/xterm-addon-fit.min.js\"></script>\n  <script src=\"static/js/main.js\"></script>\n</body>\n\n</html>"
  },
  {
    "path": "webssh/utils.py",
    "content": "import ipaddress\nimport re\n\ntry:\n    from types import UnicodeType\nexcept ImportError:\n    UnicodeType = str\n\ntry:\n    from urllib.parse import urlparse\nexcept ImportError:\n    from urlparse import urlparse\n\n\nnumeric = re.compile(r'[0-9]+$')\nallowed = re.compile(r'(?!-)[a-z0-9-]{1,63}(?<!-)$', re.IGNORECASE)\n\n\ndef to_str(bstr, encoding='utf-8'):\n    if isinstance(bstr, bytes):\n        return bstr.decode(encoding)\n    return bstr\n\n\ndef to_bytes(ustr, encoding='utf-8'):\n    if isinstance(ustr, UnicodeType):\n        return ustr.encode(encoding)\n    return ustr\n\n\ndef to_int(string):\n    try:\n        return int(string)\n    except (TypeError, ValueError):\n        pass\n\n\ndef to_ip_address(ipstr):\n    ip = to_str(ipstr)\n    if ip.startswith('fe80::'):\n        ip = ip.split('%')[0]\n    return ipaddress.ip_address(ip)\n\n\ndef is_valid_ip_address(ipstr):\n    try:\n        to_ip_address(ipstr)\n    except ValueError:\n        return False\n    return True\n\n\ndef is_valid_port(port):\n    return 0 < port < 65536\n\n\ndef is_valid_encoding(encoding):\n    try:\n        u'test'.encode(encoding)\n    except LookupError:\n        return False\n    except ValueError:\n        return False\n    return True\n\n\ndef is_ip_hostname(hostname):\n    it = iter(hostname)\n    if next(it) == '[':\n        return True\n    for ch in it:\n        if ch != '.' and not ch.isdigit():\n            return False\n    return True\n\n\ndef is_valid_hostname(hostname):\n    if hostname[-1] == '.':\n        # strip exactly one dot from the right, if present\n        hostname = hostname[:-1]\n    if len(hostname) > 253:\n        return False\n\n    labels = hostname.split('.')\n\n    # the TLD must be not all-numeric\n    if numeric.match(labels[-1]):\n        return False\n\n    return all(allowed.match(label) for label in labels)\n\n\ndef is_same_primary_domain(domain1, domain2):\n    i = -1\n    dots = 0\n    l1 = len(domain1)\n    l2 = len(domain2)\n    m = min(l1, l2)\n\n    while i >= -m:\n        c1 = domain1[i]\n        c2 = domain2[i]\n\n        if c1 == c2:\n            if c1 == '.':\n                dots += 1\n                if dots == 2:\n                    return True\n        else:\n            return False\n\n        i -= 1\n\n    if l1 == l2:\n        return True\n\n    if dots == 0:\n        return False\n\n    c = domain1[i] if l1 > m else domain2[i]\n    return c == '.'\n\n\ndef parse_origin_from_url(url):\n    url = url.strip()\n    if not url:\n        return\n\n    if not (url.startswith('http://') or url.startswith('https://') or\n            url.startswith('//')):\n        url = '//' + url\n\n    parsed = urlparse(url)\n    port = parsed.port\n    scheme = parsed.scheme\n\n    if scheme == '':\n        scheme = 'https' if port == 443 else 'http'\n\n    if port == 443 and scheme == 'https':\n        netloc = parsed.netloc.replace(':443', '')\n    elif port == 80 and scheme == 'http':\n        netloc = parsed.netloc.replace(':80', '')\n    else:\n        netloc = parsed.netloc\n\n    return '{}://{}'.format(scheme, netloc)\n"
  },
  {
    "path": "webssh/worker.py",
    "content": "import logging\ntry:\n    import secrets\nexcept ImportError:\n    secrets = None\nimport tornado.websocket\n\nfrom uuid import uuid4\nfrom tornado.ioloop import IOLoop\nfrom tornado.iostream import _ERRNO_CONNRESET\nfrom tornado.util import errno_from_exception\n\n\nBUF_SIZE = 32 * 1024\nclients = {}  # {ip: {id: worker}}\n\n\ndef clear_worker(worker, clients):\n    ip = worker.src_addr[0]\n    workers = clients.get(ip)\n    assert worker.id in workers\n    workers.pop(worker.id)\n\n    if not workers:\n        clients.pop(ip)\n        if not clients:\n            clients.clear()\n\n\ndef recycle_worker(worker):\n    if worker.handler:\n        return\n    logging.warning('Recycling worker {}'.format(worker.id))\n    worker.close(reason='worker recycled')\n\n\nclass Worker(object):\n    def __init__(self, loop, ssh, chan, dst_addr):\n        self.loop = loop\n        self.ssh = ssh\n        self.chan = chan\n        self.dst_addr = dst_addr\n        self.fd = chan.fileno()\n        self.id = self.gen_id()\n        self.data_to_dst = []\n        self.handler = None\n        self.mode = IOLoop.READ\n        self.closed = False\n\n    def __call__(self, fd, events):\n        if events & IOLoop.READ:\n            self.on_read()\n        if events & IOLoop.WRITE:\n            self.on_write()\n        if events & IOLoop.ERROR:\n            self.close(reason='error event occurred')\n\n    @classmethod\n    def gen_id(cls):\n        return secrets.token_urlsafe(nbytes=32) if secrets else uuid4().hex\n\n    def set_handler(self, handler):\n        if not self.handler:\n            self.handler = handler\n\n    def update_handler(self, mode):\n        if self.mode != mode:\n            self.loop.update_handler(self.fd, mode)\n            self.mode = mode\n        if mode == IOLoop.WRITE:\n            self.loop.call_later(0.1, self, self.fd, IOLoop.WRITE)\n\n    def on_read(self):\n        logging.debug('worker {} on read'.format(self.id))\n        try:\n            data = self.chan.recv(BUF_SIZE)\n        except (OSError, IOError) as e:\n            logging.error(e)\n            if self.chan.closed or errno_from_exception(e) in _ERRNO_CONNRESET:\n                self.close(reason='chan error on reading')\n        else:\n            logging.debug('{!r} from {}:{}'.format(data, *self.dst_addr))\n            if not data:\n                self.close(reason='chan closed')\n                return\n\n            logging.debug('{!r} to {}:{}'.format(data, *self.handler.src_addr))\n            try:\n                self.handler.write_message(data, binary=True)\n            except tornado.websocket.WebSocketClosedError:\n                self.close(reason='websocket closed')\n\n    def on_write(self):\n        logging.debug('worker {} on write'.format(self.id))\n        if not self.data_to_dst:\n            return\n\n        data = ''.join(self.data_to_dst)\n        logging.debug('{!r} to {}:{}'.format(data, *self.dst_addr))\n\n        try:\n            sent = self.chan.send(data)\n        except (OSError, IOError) as e:\n            logging.error(e)\n            if self.chan.closed or errno_from_exception(e) in _ERRNO_CONNRESET:\n                self.close(reason='chan error on writing')\n            else:\n                self.update_handler(IOLoop.WRITE)\n        else:\n            self.data_to_dst = []\n            data = data[sent:]\n            if data:\n                self.data_to_dst.append(data)\n                self.update_handler(IOLoop.WRITE)\n            else:\n                self.update_handler(IOLoop.READ)\n\n    def close(self, reason=None):\n        if self.closed:\n            return\n        self.closed = True\n\n        logging.info(\n            'Closing worker {} with reason: {}'.format(self.id, reason)\n        )\n        if self.handler:\n            self.loop.remove_handler(self.fd)\n            self.handler.close(reason=reason)\n        self.chan.close()\n        self.ssh.close()\n        logging.info('Connection to {}:{} lost'.format(*self.dst_addr))\n\n        clear_worker(self, clients)\n        logging.debug(clients)\n"
  }
]