From 0377594e85044309fd634486a65d110e9df11f94 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Wed, 25 May 2022 15:34:51 +0100 Subject: [PATCH 01/20] Change the base image from python:3.7-slim to python:3-alpine --- Dockerfile | 5 +++-- requirements.txt | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index 38dd2e08..ddd1e19f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,10 @@ -FROM python:3.7-slim +FROM python:3-alpine ADD . /code WORKDIR /code RUN \ + apk --no-cache add libc-dev libffi-dev gcc && \ groupadd -r webssh && \ - useradd -r -s /bin/false -g webssh webssh && \ + adduser -Ss /bin/false -g webssh webssh && \ chown -R webssh:webssh /code && \ pip install -r requirements.txt diff --git a/requirements.txt b/requirements.txt index ff0d3596..7f958d48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ paramiko==2.10.4 tornado==5.1.1; python_version < '3.5' tornado==6.1.0; python_version >= '3.5' +PyYAML==6.0 From e40a6e9a2387c40d25be936187ab79dec7dc255c Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Wed, 25 May 2022 15:34:51 +0100 Subject: [PATCH 02/20] Create the gitlab ci support --- .gitlab-ci.yaml | 222 +++++++++++++++++++++++++++++++++++++++++++++++ Dockerfile | 11 ++- requirements.txt | 1 + 3 files changed, 231 insertions(+), 3 deletions(-) create mode 100644 .gitlab-ci.yaml diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml new file mode 100644 index 00000000..ac458b35 --- /dev/null +++ b/.gitlab-ci.yaml @@ -0,0 +1,222 @@ +# The file used for the GitLab-CI for automation in webssh project. +stages: + - pre + - test + - build + - release + - deploy + - post + +# Default Variables here +variables: + GIT_STRATEGY: "fetch" + GIT_SUBMODULE_STRATEGY: "recursive" + REGISTRY_REPO: "kensonman/webssh" + REGISTRY_TEMP_TAG: "CICD" + DOCKER_HOST: tcp://docker:2376 + DOCKER_TLS_CERTDIR: "/certs" + DOCKER_TLS_VERIFY: 1 + DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client" + +services: + - docker:19.03.13-dind + +# Default Job Specification +default: + image: "python:3-alpine" + +.default-rules: + rules: + # Ignorethe releasing + - if: $CI_COMMIT_TAG =~ /^release(s)?\// + when: never + +.on-versioning-branch: + rules: + - !reference [.default-rules, rules] + + # For debug CICD + - if: $CI_COMMIT_BRANCH == 'migrate2gitlab' + + # If the tag name is matched with regex: 1.2.3-rc.4+build.5 + - if: $CI_COMMIT_TAG =~ /^([0-9]+\.){2}([0-9]+){1}(\-[a-zA-Z\.]*[0-9]+)?(\+[a-zA-Z\.]*[0-9]+)?/ + + # If the commit-title is match with regex: 1.2.3-rc.4+build.5 + - if: $CI_COMMIT_TITLE =~ /^([0-9]+\.){2}([0-9]+){1}(\-[a-zA-Z\.]*[0-9]+)?(\+[a-zA-Z\.]*[0-9]+)? -/ + + # If the branch is match with regex, e.g.: 1.2.3-rc.4+build.x + - if: $CI_COMMIT_BRANCH =~ /^([0-9]+\.){0,3}([0-9]+[\+\-]){0,2}x$/ + +# Prepare the versioning +prepare: + stage: pre + image: "kensonman/versioning:1.0.10" + rules: + - !reference [.default-rules, rules] + - when: always + variables: + #To override the last version when detecting the versioning + #VERSION: 1.0.0 +# before_script: +# -> git config --global --add safe.directory `pwd` + #For testing + BRANCH: 0.0.0-dev.0-test.x + LOGGER_LEVEL: "10" + script: + - > + echo "Preparing the CI/CD environment: `pwd`..." + && echo " > CI_COMMIT_TITLE:" + && echo ${CI_COMMIT_TITLE} + && echo " > CI_COMMIT_DESCRIPTION:" + && echo ${CI_COMMIT_DESCRIPTION} + && echo " > CI_COMMIT_MESSAGE:" + && echo ${CI_COMMIT_MESSAGE} + && echo " > CI_COMMIT_TAG:" + && echo ${CI_COMMIT_TAG} + && echo " > Detecting the versioning pattern: `version.py getVersionPattern`" + && echo " > Getting the next version..." + && export VERSION=`version.py getNextVersion` + && echo " > Next version: ${VERSION}" + && export DOCKERIZED_VERSION=`version.py toDockerized --option ${VERSION}` + && echo " > Dockerized next version: ${DOCKERIZED_VERSION}" + && echo " > Exporting..." + && echo "VERSION=${VERSION}" > VERSION.env + && echo "DOCKERIZED_VERSION=${DOCKERIZED_VERSION}" > VERSION.env + && echo "${VERSION}" > VERSION + artifacts: + reports: + # Put the version into env for sharing the variable between job + dotenv: VERSION.env + paths: + # Put the VERSION as an artifact for futer development + - VERSION + +# Teting the binary/output which builded by "building" job. +testing: + stage: test + rules: + - !reference [.default-rules, rules] + - if: $CI_COMMIT_DESCRIPTION =~ /CI:NoTesting/ + when: never + - when: always + before_script: + - apk --no-cache add libc-dev libffi-dev gcc + - pip install pytest pytest-cov codecov flake8 mock + - pip install -r requirements.txt + script: + - > + if [ $VERSION == "value-from-prepare" || $VERSION == 'None' || -z $VERSION ]; then + echo "Skip the job due to the VERSION did not specified: JobName: ${CI_JOB_NAME}, Version: ${VERSION}" + exit 1; + fi + - pytest --junitxml=test-results.xml tests + artifacts: + reports: + junit: test-results.xml + +# Building the binary/output according to the string. It may be the docker image +building: + stage: build + image: "docker:19.03.13" + dependencies: + - prepare + - testing + rules: + - !reference [.on-versioning-branch, rules] + - if: $CI_COMMIT_DESCRIPTION =~ /CI:NoBuilding/ + when: never + variables: + VERSION: value-from-prepare + DOCKERIZED_VERSION: value-from-prepare + script: + - > + if [ "$VERSION" == "value-from-prepare" || "$VERSION" == "None" || -z "$VERSION" ]; then + echo "Skip the job due to the VERSION did not specified: JobName: ${CI_JOB_NAME}, Version: ${VERSION}" + exit 1; + fi + - > + echo "Building the repo..." + && export TEMP_IMAGE="${REGISTRY_SERVER}${REGISTRY_REPO}:${REGISTRY_TEMP_TAG}" + && export TARGET_IMAGE="${REGISTRY_SERVER}${REGISTRY_REPO}:${DOCKERIZED_VERSION}" + && echo " > the docker image: ${TEMP_IMAGE}" + && sed 's//${CI_COMMIT_AUTHOR}/g' Dockerfile + && sed 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(\-\(\w*\.\)[0-9]\+\)\{0,2\}/${DOCKERIZED_VERSION}/g' Dockerfile + && docker build -t ${TEMP_IMAGE} . + && echo " > Tagging ${TEMP_IMAGE} => ${TARGET_IMAGE}" + && docker tag ${TEMP_IMAGE} ${TARGET_IMAGE} + && echo " > Sign-In to registry server: ${REGISTRY_SERVER} with username:password ${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" + && echo -n ${REGISTRY_PASSWORD} | base64 -d | docker login --username `echo -n ${REGISTRY_USERNAME} | base64 -d` --password-stdin ${REGISTRY_SERVER} + && docker push ${TARGET_IMAGE} + +# Releasing the binary/output. Usually push the docker image into the registry +releasing: + image: registry.gitlab.com/gitlab-org/release-cli:latest + stage: release + dependencies: + - prepare + - testing + - building + rules: + - !reference [.on-versioning-branch, rules] + - if: $CI_COMMIT_DESCRIPTION =~ /CI:NoReleasing/ + when: never + variables: + VERSION: value-from-prepare + DOCKERIZED_VERSION: value-from-prepare + #Don't pull the source code + GIT_STRATEGY: none + before_script: + - > + if [ "$VERSION" == "value-from-prepare" || "$VERSION" == "None" || -z "$VERSION" ]; then + echo "Skip the job due to the VERSION did not specified: JobName: ${CI_JOB_NAME}, Version: ${VERSION}"; + exit 1; + fi + script: + - > + echo "Creating release: $VERSION..." + release: + name: "Release - $VERSION" + description: "$CI_COMMIT_REF_NAME - $CI_COMMIT_MESSAGE" + tag_name: "releases/$VERSION" + ref: '$CI_COMMIT_REF_NAME' + +# Deploy the binary/output into production. Usually askying the k8s to update the image +deploying: + stage: deploy + dependencies: + - prepare + - testing + - building + - releasing + rules: + - !reference [.on-versioning-branch, rules] + - if: $CI_COMMIT_TITLE =~ /CI:NoDeploying/ + when: never + variables: + VERSION: value-from-prepare + DOCKERIZED_VERSION: value-from-prepare + #Don't pull the source code + GIT_STRATEGY: none + script: + - > + if [ $VERSION == "value-from-prepare" || $VERSION == 'None' || -z $VERSION ]; then + echo "Skip the job due to the VERSION did not specified: JobName: ${CI_JOB_NAME}, Version: ${VERSION}" + exit 1; + fi + - > + echo "Deploying..." + && echo "No implemented yet!" + +# Cleaning the CI/CD +clean: + stage: post + rules: + - !reference [.default-rules, rules] + - if: $CI_COMMIT_TITLE =~ /CI:NoCleaning/ + when: never + variables: + #Don't pull the source code + GIT_STRATEGY: none + script: + - > + echo "Cleaning up..." diff --git a/Dockerfile b/Dockerfile index 38dd2e08..87fedaca 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,14 @@ -FROM python:3.7-slim +FROM python:3-alpine + +LABEL maintainer='' +LABEL version='0.0.0-dev.0-build.0' + ADD . /code WORKDIR /code RUN \ - groupadd -r webssh && \ - useradd -r -s /bin/false -g webssh webssh && \ + apk --no-cache add libc-dev libffi-dev gcc && \ + addgroup webssh && \ + adduser -Ss /bin/false -g webssh webssh && \ chown -R webssh:webssh /code && \ pip install -r requirements.txt diff --git a/requirements.txt b/requirements.txt index ff0d3596..7f958d48 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ paramiko==2.10.4 tornado==5.1.1; python_version < '3.5' tornado==6.1.0; python_version >= '3.5' +PyYAML==6.0 From decc471761c29ed8d23075ed1492df3da3e08bf3 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Fri, 27 May 2022 13:58:01 +0100 Subject: [PATCH 03/20] Update the gitlab ci flow for main branch\ --- .gitlab-ci.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml index ac458b35..c8513e8b 100644 --- a/.gitlab-ci.yaml +++ b/.gitlab-ci.yaml @@ -35,9 +35,6 @@ default: rules: - !reference [.default-rules, rules] - # For debug CICD - - if: $CI_COMMIT_BRANCH == 'migrate2gitlab' - # If the tag name is matched with regex: 1.2.3-rc.4+build.5 - if: $CI_COMMIT_TAG =~ /^([0-9]+\.){2}([0-9]+){1}(\-[a-zA-Z\.]*[0-9]+)?(\+[a-zA-Z\.]*[0-9]+)?/ @@ -99,6 +96,8 @@ testing: - if: $CI_COMMIT_DESCRIPTION =~ /CI:NoTesting/ when: never - when: always + dependencies: + - prepare before_script: - apk --no-cache add libc-dev libffi-dev gcc - pip install pytest pytest-cov codecov flake8 mock From 3d60d4881abe2cb51bf4ba57e234d6b3bca238f3 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Fri, 27 May 2022 16:07:18 +0100 Subject: [PATCH 04/20] Add the test-cases. Add the test case: - test_app_4_profiles_reading - The test used to testing the PROFILES environment variable; - test_profile_with_username - The testing the profile[username] - test_profile_with_privatekey - The testing the profile[username and private-key] --- tests/data/profiles-sample.yaml | 50 ++++++++++++ tests/test_profiles.py | 140 ++++++++++++++++++++++++++++++++ webssh/settings.py | 16 ++++ 3 files changed, 206 insertions(+) create mode 100644 tests/data/profiles-sample.yaml create mode 100644 tests/test_profiles.py diff --git a/tests/data/profiles-sample.yaml b/tests/data/profiles-sample.yaml new file mode 100644 index 00000000..371600a9 --- /dev/null +++ b/tests/data/profiles-sample.yaml @@ -0,0 +1,50 @@ +profiles: + - name: sample1 + description: "Long description" + host: localhost + port: 22 + #optional, if specified, the username field should not be shown on the template + username: user + #optional, if specified. + #The below private key is clone from ./tests/data/user_rsa_key + private_key: | + -----BEGIN RSA PRIVATE KEY----- + MIICXQIBAAKBgQDI7iK3d8eWYZlYloat94c5VjtFY7c/0zuGl8C7uMnZ3t6i2G99 + 66hEW0nCFSZkOW5F0XKEVj+EUCHvo8koYC6wiohAqWQnEwIoOoh7GSAcB8gP/qaq + +adIl/Rvlby/mHakj+y05LBND6nFWHAn1y1gOFFKUXSJNRZPXSFy47gqzwIBIwKB + gQCbANjz7q/pCXZLp1Hz6tYHqOvlEmjK1iabB1oqafrMpJ0eibUX/u+FMHq6StR5 + M5413BaDWHokPdEJUnabfWXXR3SMlBUKrck0eAer1O8m78yxu3OEdpRk+znVo4DL + guMeCdJB/qcF0kEsx+Q8HP42MZU1oCmk3PbfXNFwaHbWuwJBAOQ/ry/hLD7AqB8x + DmCM82A9E59ICNNlHOhxpJoh6nrNTPCsBAEu/SmqrL8mS6gmbRKUaya5Lx1pkxj2 + s/kWOokCQQDhXCcYXjjWiIfxhl6Rlgkk1vmI0l6785XSJNv4P7pXjGmShXfIzroh + S8uWK3tL0GELY7+UAKDTUEVjjQdGxYSXAkEA3bo1JzKCwJ3lJZ1ebGuqmADRO6UP + 40xH977aadfN1mEI6cusHmgpISl0nG5YH7BMsvaT+bs1FUH8m+hXDzoqOwJBAK3Z + X/za+KV/REya2z0b+GzgWhkXUGUa/owrEBdHGriQ47osclkUgPUdNqcLmaDilAF4 + 1Z4PHPrI5RJIONAx+JECQQC/fChqjBgFpk6iJ+BOdSexQpgfxH/u/457W10Y43HR + soS+8btbHqjQkowQ/2NTlUfWvqIlfxs6ZbFsIp/HrhZL + -----END RSA PRIVATE KEY----- + + - name: sample2 + description: "Long description" + host: localhost + port: 22 + #optional, if specified, the username field should not be shown on the template + username: user + #optional, if specified. + #The below private key is clone from ./tests/data/user_rsa_key + private_key: > + -----BEGIN RSA PRIVATE KEY----- + MIICXQIBAAKBgQDI7iK3d8eWYZlYloat94c5VjtFY7c/0zuGl8C7uMnZ3t6i2G99 + 66hEW0nCFSZkOW5F0XKEVj+EUCHvo8koYC6wiohAqWQnEwIoOoh7GSAcB8gP/qaq + +adIl/Rvlby/mHakj+y05LBND6nFWHAn1y1gOFFKUXSJNRZPXSFy47gqzwIBIwKB + gQCbANjz7q/pCXZLp1Hz6tYHqOvlEmjK1iabB1oqafrMpJ0eibUX/u+FMHq6StR5 + M5413BaDWHokPdEJUnabfWXXR3SMlBUKrck0eAer1O8m78yxu3OEdpRk+znVo4DL + guMeCdJB/qcF0kEsx+Q8HP42MZU1oCmk3PbfXNFwaHbWuwJBAOQ/ry/hLD7AqB8x + DmCM82A9E59ICNNlHOhxpJoh6nrNTPCsBAEu/SmqrL8mS6gmbRKUaya5Lx1pkxj2 + s/kWOokCQQDhXCcYXjjWiIfxhl6Rlgkk1vmI0l6785XSJNv4P7pXjGmShXfIzroh + S8uWK3tL0GELY7+UAKDTUEVjjQdGxYSXAkEA3bo1JzKCwJ3lJZ1ebGuqmADRO6UP + 40xH977aadfN1mEI6cusHmgpISl0nG5YH7BMsvaT+bs1FUH8m+hXDzoqOwJBAK3Z + X/za+KV/REya2z0b+GzgWhkXUGUa/owrEBdHGriQ47osclkUgPUdNqcLmaDilAF4 + 1Z4PHPrI5RJIONAx+JECQQC/fChqjBgFpk6iJ+BOdSexQpgfxH/u/457W10Y43HR + soS+8btbHqjQkowQ/2NTlUfWvqIlfxs6ZbFsIp/HrhZL + -----END RSA PRIVATE KEY----- diff --git a/tests/test_profiles.py b/tests/test_profiles.py new file mode 100644 index 00000000..52333a56 --- /dev/null +++ b/tests/test_profiles.py @@ -0,0 +1,140 @@ +import unittest, yaml, tornado.websocket, tornado.gen, threading, random, json, os +from tornado.testing import AsyncHTTPTestCase +from tornado.httpclient import HTTPError +from tornado.options import options + +from tests.utils import make_tests_data_path +from yaml.loader import SafeLoader + +from webssh import handler +from webssh.main import make_app, make_handlers +from webssh.utils import to_str +from webssh.settings import ( + get_app_settings, get_server_settings, max_body_size +) + +from tests.sshserver import run_ssh_server, banner, Server +from tests.test_app import TestAppBase + +class TestProfiles(TestAppBase): + running = [True] + sshserver_port = 2200 + body = 'hostname={host}&port={port}&profile={profile}&username={username}&password={password}' + headers = {'Cookie': '_xsrf=yummy'} + + def get_app(self): + self.body_dict = { + 'hostname': '127.0.0.1', + 'port': str(self.sshserver_port), + 'username': 'robey', + 'password': '', + '_xsrf': 'yummy' + } + loop = self.io_loop + options.debug = False + options.policy = random.choice(['warning', 'autoadd']) + options.hostfile = '' + options.syshostfile = '' + options.tdstream = '' + options.delay = 0.1 + #options.profiles=make_tests_data_path('tests/data/profiles-sample.yaml') + app = make_app(make_handlers(loop, options), get_app_settings(options)) + return app + + def test_app_4_profiles_reading(self): + if 'PROFILES' in os.environ: del os.environ['PROFILES'] + assert 'profiles' not in get_app_settings(options) + + os.environ['PROFILES']=make_tests_data_path('profiles-sample.yaml') + assert 'profiles' in get_app_settings(options) + profiles=get_app_settings(options)['profiles'] + assert profiles[0]['name']=='sample1' + assert profiles[0]['description']=='Long description' + assert profiles[0]['host']=='localhost' + assert profiles[0]['port']==22 + assert profiles[0]['username']=='user' + assert profiles[0]['private_key']==open(make_tests_data_path('user_rsa_key'), 'r').read() + + assert profiles[1]['name']=='sample2' + assert profiles[1]['description']=='Long description' + assert profiles[1]['host']=='localhost' + assert profiles[1]['port']==22 + assert profiles[1]['username']=='user' + del os.environ['PROFILES'] + + @classmethod + def setUpClass(cls): + print('='*20) + t = threading.Thread( + target=run_ssh_server, args=(cls.sshserver_port, cls.running) + ) + t.setDaemon(True) + t.start() + + @classmethod + def tearDownClass(cls): + cls.running.pop() + print('='*20) + + def _testBody_(self, body): + url = self.get_url('/') + response = yield self.async_post(url, body) + data = json.loads(to_str(response.body)) + self.assert_status_none(data) + + url = url.replace('http', 'ws') + ws_url = url + 'ws?id=' + data['id'] + ws = yield tornado.websocket.websocket_connect(ws_url) + msg = yield ws.read_message() + self.assertEqual(to_str(msg, data['encoding']), banner) + + # messages below will be ignored silently + yield ws.write_message('hello') + yield ws.write_message('"hello"') + yield ws.write_message('[hello]') + yield ws.write_message(json.dumps({'resize': []})) + yield ws.write_message(json.dumps({'resize': {}})) + yield ws.write_message(json.dumps({'resize': 'ab'})) + yield ws.write_message(json.dumps({'resize': ['a', 'b']})) + yield ws.write_message(json.dumps({'resize': {'a': 1, 'b': 2}})) + yield ws.write_message(json.dumps({'resize': [100]})) + yield ws.write_message(json.dumps({'resize': [100]*10})) + yield ws.write_message(json.dumps({'resize': [-1, -1]})) + yield ws.write_message(json.dumps({'data': [1]})) + yield ws.write_message(json.dumps({'data': (1,)})) + yield ws.write_message(json.dumps({'data': {'a': 2}})) + yield ws.write_message(json.dumps({'data': 1})) + yield ws.write_message(json.dumps({'data': 2.1})) + yield ws.write_message(json.dumps({'key-non-existed': 'hello'})) + # end - those just for testing webssh websocket stablity + + yield ws.write_message(json.dumps({'resize': [79, 23]})) + msg = yield ws.read_message() + self.assertEqual(b'resized', msg) + + yield ws.write_message(json.dumps({'data': 'bye'})) + msg = yield ws.read_message() + self.assertEqual(b'bye', msg) + ws.close() + + @tornado.testing.gen_test + def test_profile_with_username(self): + body=self.body.format( + host="127.0.0.1", + port=22, + username="robey", + password="foo", + profile="", + ) + self._testBody_(body) + + @tornado.testing.gen_test + def test_profile_with_privatekey(self): + body=self.body.format( + host="127.0.0.1", + port=22, + username="robey", + password="", + profile="", + ) + self._testBody_(body) diff --git a/webssh/settings.py b/webssh/settings.py index c9dbbbe3..e510018d 100644 --- a/webssh/settings.py +++ b/webssh/settings.py @@ -3,6 +3,10 @@ import ssl import sys +import os +import yaml +from yaml.loader import SafeLoader + from tornado.options import define from webssh.policy import ( load_host_keys, get_policy_class, check_policy_setting @@ -87,6 +91,18 @@ def get_app_settings(options): ), origin_policy=get_origin_setting(options) ) + filename=os.getenv('PROFILES', None) + if filename: + if not filename.startswith(os.sep): filename=os.path.join(os.path.abspath(os.sep), filename) + try: + if not os.path.exists(filename): raise FileNotFoundError() + with open(filename, 'r') as fp: + settings['profiles']=yaml.load(fp, Loader=SafeLoader) + if settings['profiles']: settings['profiles']=settings['profiles']['profiles'] + except FileNotFoundError: + logging.warning('Cannot found file profiles: {0}'.format(filename)) + except: + logging.warning('Unexpected error', exc_info=True) return settings From 91454cca1f98836854135337f7ca381dca2d20ea Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Mon, 30 May 2022 15:14:09 +0100 Subject: [PATCH 05/20] Update the UI, test-cases and README.md. The commit was implemented the profiling feature. --- README.md | 31 +++++ requirements.txt | 7 ++ tests/data/profiles-sample.yaml | 25 +--- tests/test_app.py | 7 ++ tests/test_profiles.py | 191 ++++++++++++------------------ webssh/handler.py | 34 +++++- webssh/settings.py | 34 ++++-- webssh/static/css/cookiealert.css | 36 ++++++ webssh/static/js/cookiealert.js | 56 +++++++++ webssh/static/js/js.cookie.min.js | 2 + webssh/static/js/profiles.js | 66 +++++++++++ webssh/templates/profiles.html | 139 ++++++++++++++++++++++ 12 files changed, 474 insertions(+), 154 deletions(-) create mode 100644 webssh/static/css/cookiealert.css create mode 100644 webssh/static/js/cookiealert.js create mode 100644 webssh/static/js/js.cookie.min.js create mode 100644 webssh/static/js/profiles.js create mode 100644 webssh/templates/profiles.html diff --git a/README.md b/README.md index ad77175e..660c77ca 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,37 @@ Running as a standalone server wssh --port=8080 --sslport=4433 --certfile='cert.crt' --keyfile='cert.key' --xheaders=False --policy=reject ``` +### Profiling + +Due to security, we should not disclose our private keys to anybody. Especially transfer +the private key and the passphrase in the same transaction, although the HTTPS protocol +can protect the transaction data. + +That is the reason I implement the profiling feature. + +This feature can provide the selectable profiles (just like ~/.ssh/config), it provides +the features just like the SSH Client config file (normally located at ~/.ssh/config) like this: +```yaml +required: False #If true, the profile is required to be selected before connect +profiles: + - name: The label will be shown on the profiles dropdown box + description: "It will be shown on the tooltip" + host: my-server.com + port: 22 + username: user + private-key: | + -----BEGIN OPENSSH PRIVATE KEY----- + ABCD........ + ...... + ...... + -----END OPENSSH PRIVATE KEY----- + - name: Profile 2 + description: "It will shown on the tooltip" + host: my-server.com + port: 22 + username: user2 +``` + ### Tips diff --git a/requirements.txt b/requirements.txt index 7f958d48..d4e47c43 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,10 @@ paramiko==2.10.4 tornado==5.1.1; python_version < '3.5' tornado==6.1.0; python_version >= '3.5' PyYAML==6.0 + +#The following package used for testing +#pytest +#pytest-cov +#codecov +#flake8 +#mock diff --git a/tests/data/profiles-sample.yaml b/tests/data/profiles-sample.yaml index 371600a9..c1b34132 100644 --- a/tests/data/profiles-sample.yaml +++ b/tests/data/profiles-sample.yaml @@ -1,38 +1,21 @@ +required: true #If true, user have to select one of the profiles profiles: - name: sample1 description: "Long description" host: localhost port: 22 #optional, if specified, the username field should not be shown on the template - username: user - #optional, if specified. - #The below private key is clone from ./tests/data/user_rsa_key - private_key: | - -----BEGIN RSA PRIVATE KEY----- - MIICXQIBAAKBgQDI7iK3d8eWYZlYloat94c5VjtFY7c/0zuGl8C7uMnZ3t6i2G99 - 66hEW0nCFSZkOW5F0XKEVj+EUCHvo8koYC6wiohAqWQnEwIoOoh7GSAcB8gP/qaq - +adIl/Rvlby/mHakj+y05LBND6nFWHAn1y1gOFFKUXSJNRZPXSFy47gqzwIBIwKB - gQCbANjz7q/pCXZLp1Hz6tYHqOvlEmjK1iabB1oqafrMpJ0eibUX/u+FMHq6StR5 - M5413BaDWHokPdEJUnabfWXXR3SMlBUKrck0eAer1O8m78yxu3OEdpRk+znVo4DL - guMeCdJB/qcF0kEsx+Q8HP42MZU1oCmk3PbfXNFwaHbWuwJBAOQ/ry/hLD7AqB8x - DmCM82A9E59ICNNlHOhxpJoh6nrNTPCsBAEu/SmqrL8mS6gmbRKUaya5Lx1pkxj2 - s/kWOokCQQDhXCcYXjjWiIfxhl6Rlgkk1vmI0l6785XSJNv4P7pXjGmShXfIzroh - S8uWK3tL0GELY7+UAKDTUEVjjQdGxYSXAkEA3bo1JzKCwJ3lJZ1ebGuqmADRO6UP - 40xH977aadfN1mEI6cusHmgpISl0nG5YH7BMsvaT+bs1FUH8m+hXDzoqOwJBAK3Z - X/za+KV/REya2z0b+GzgWhkXUGUa/owrEBdHGriQ47osclkUgPUdNqcLmaDilAF4 - 1Z4PHPrI5RJIONAx+JECQQC/fChqjBgFpk6iJ+BOdSexQpgfxH/u/457W10Y43HR - soS+8btbHqjQkowQ/2NTlUfWvqIlfxs6ZbFsIp/HrhZL - -----END RSA PRIVATE KEY----- + username: robey - name: sample2 description: "Long description" host: localhost port: 22 #optional, if specified, the username field should not be shown on the template - username: user + username: robey #optional, if specified. #The below private key is clone from ./tests/data/user_rsa_key - private_key: > + private-key: | -----BEGIN RSA PRIVATE KEY----- MIICXQIBAAKBgQDI7iK3d8eWYZlYloat94c5VjtFY7c/0zuGl8C7uMnZ3t6i2G99 66hEW0nCFSZkOW5F0XKEVj+EUCHvo8koYC6wiohAqWQnEwIoOoh7GSAcB8gP/qaq diff --git a/tests/test_app.py b/tests/test_app.py index bd31b5f1..0d69ca23 100644 --- a/tests/test_app.py +++ b/tests/test_app.py @@ -76,6 +76,13 @@ def sync_post(self, url, body, headers={}): def async_post(self, url, body, headers={}): return self.fetch_request(url, 'POST', body, headers, sync=False) + def sync_get(self, url, body, headers={}): + assert body == None + return self.fetch_request(url, 'GET', body, headers) + + def async_get(self, url, body, headers={}): + assert body==None + return self.fetch_request(url, 'GET', body, headers, sync=False) class TestAppBasic(TestAppBase): diff --git a/tests/test_profiles.py b/tests/test_profiles.py index 52333a56..9a454fe5 100644 --- a/tests/test_profiles.py +++ b/tests/test_profiles.py @@ -1,140 +1,99 @@ -import unittest, yaml, tornado.websocket, tornado.gen, threading, random, json, os -from tornado.testing import AsyncHTTPTestCase -from tornado.httpclient import HTTPError +import pytest, os, re, yaml, random from tornado.options import options - +from tornado.testing import AsyncTestCase, AsyncHTTPTestCase +from webssh.main import make_app, make_handlers +from webssh.settings import get_app_settings from tests.utils import make_tests_data_path from yaml.loader import SafeLoader -from webssh import handler -from webssh.main import make_app, make_handlers -from webssh.utils import to_str -from webssh.settings import ( - get_app_settings, get_server_settings, max_body_size -) - -from tests.sshserver import run_ssh_server, banner, Server -from tests.test_app import TestAppBase - -class TestProfiles(TestAppBase): - running = [True] - sshserver_port = 2200 - body = 'hostname={host}&port={port}&profile={profile}&username={username}&password={password}' - headers = {'Cookie': '_xsrf=yummy'} - - def get_app(self): - self.body_dict = { - 'hostname': '127.0.0.1', - 'port': str(self.sshserver_port), - 'username': 'robey', - 'password': '', - '_xsrf': 'yummy' - } - loop = self.io_loop - options.debug = False - options.policy = random.choice(['warning', 'autoadd']) - options.hostfile = '' - options.syshostfile = '' - options.tdstream = '' - options.delay = 0.1 - #options.profiles=make_tests_data_path('tests/data/profiles-sample.yaml') - app = make_app(make_handlers(loop, options), get_app_settings(options)) - return app - - def test_app_4_profiles_reading(self): +class TestYAMLLoading(object): + def test_profile_samples(self): if 'PROFILES' in os.environ: del os.environ['PROFILES'] assert 'profiles' not in get_app_settings(options) os.environ['PROFILES']=make_tests_data_path('profiles-sample.yaml') assert 'profiles' in get_app_settings(options) - profiles=get_app_settings(options)['profiles'] + profiles=get_app_settings(options)['profiles']['profiles'] assert profiles[0]['name']=='sample1' assert profiles[0]['description']=='Long description' assert profiles[0]['host']=='localhost' assert profiles[0]['port']==22 - assert profiles[0]['username']=='user' - assert profiles[0]['private_key']==open(make_tests_data_path('user_rsa_key'), 'r').read() + assert profiles[0]['username']=='robey' assert profiles[1]['name']=='sample2' assert profiles[1]['description']=='Long description' assert profiles[1]['host']=='localhost' assert profiles[1]['port']==22 - assert profiles[1]['username']=='user' + assert profiles[1]['username']=='robey' + assert profiles[1]['private-key']==open(make_tests_data_path('user_rsa_key'), 'r').read() del os.environ['PROFILES'] - @classmethod - def setUpClass(cls): - print('='*20) - t = threading.Thread( - target=run_ssh_server, args=(cls.sshserver_port, cls.running) - ) - t.setDaemon(True) - t.start() - - @classmethod - def tearDownClass(cls): - cls.running.pop() - print('='*20) - - def _testBody_(self, body): - url = self.get_url('/') - response = yield self.async_post(url, body) - data = json.loads(to_str(response.body)) - self.assert_status_none(data) - - url = url.replace('http', 'ws') - ws_url = url + 'ws?id=' + data['id'] - ws = yield tornado.websocket.websocket_connect(ws_url) - msg = yield ws.read_message() - self.assertEqual(to_str(msg, data['encoding']), banner) - - # messages below will be ignored silently - yield ws.write_message('hello') - yield ws.write_message('"hello"') - yield ws.write_message('[hello]') - yield ws.write_message(json.dumps({'resize': []})) - yield ws.write_message(json.dumps({'resize': {}})) - yield ws.write_message(json.dumps({'resize': 'ab'})) - yield ws.write_message(json.dumps({'resize': ['a', 'b']})) - yield ws.write_message(json.dumps({'resize': {'a': 1, 'b': 2}})) - yield ws.write_message(json.dumps({'resize': [100]})) - yield ws.write_message(json.dumps({'resize': [100]*10})) - yield ws.write_message(json.dumps({'resize': [-1, -1]})) - yield ws.write_message(json.dumps({'data': [1]})) - yield ws.write_message(json.dumps({'data': (1,)})) - yield ws.write_message(json.dumps({'data': {'a': 2}})) - yield ws.write_message(json.dumps({'data': 1})) - yield ws.write_message(json.dumps({'data': 2.1})) - yield ws.write_message(json.dumps({'key-non-existed': 'hello'})) - # end - those just for testing webssh websocket stablity +class _TestBasic_(object): + running = [True] + sshserver_port = 2200 + body = 'hostname={host}&port={port}&profile={profile}&username={username}&password={password}' + headers = {'Cookie': '_xsrf=yummy'} - yield ws.write_message(json.dumps({'resize': [79, 23]})) - msg = yield ws.read_message() - self.assertEqual(b'resized', msg) + def _getApp_(self, **kwargs): + loop = self.io_loop + options.debug = False + options.policy = random.choice(['warning', 'autoadd']) + options.hostfile = '' + options.syshostfile = '' + options.tdstream = '' + options.delay = 0.1 + #options.profiles=make_tests_data_path('tests/data/profiles-sample.yaml') + app = make_app(make_handlers(loop, options), get_app_settings(options)) + return app - yield ws.write_message(json.dumps({'data': 'bye'})) - msg = yield ws.read_message() - self.assertEqual(b'bye', msg) - ws.close() +class TestWebGUIWithProfiles(AsyncHTTPTestCase, _TestBasic_): + def get_app(self): + try: + os.environ['PROFILES']=make_tests_data_path('profiles-sample.yaml') + return self._getApp_() + finally: + del os.environ['PROFILES'] + + + def test_get_app_settings(self): + try: + os.environ['PROFILES']=make_tests_data_path('profiles-sample.yaml') + settings=get_app_settings(options) + assert 'profiles' in settings + profiles=settings['profiles']['profiles'] + assert profiles[0]['name']=='sample1' + assert profiles[0]['description']=='Long description' + assert profiles[0]['host']=='localhost' + assert profiles[0]['port']==22 + assert profiles[0]['username']=='robey' - @tornado.testing.gen_test - def test_profile_with_username(self): - body=self.body.format( - host="127.0.0.1", - port=22, - username="robey", - password="foo", - profile="", - ) - self._testBody_(body) + assert profiles[1]['name']=='sample2' + assert profiles[1]['description']=='Long description' + assert profiles[1]['host']=='localhost' + assert profiles[1]['port']==22 + assert profiles[1]['username']=='robey' + assert profiles[1]['private-key']==open(make_tests_data_path('user_rsa_key'), 'r').read() + finally: + del os.environ['PROFILES'] + + def test_without_profiles(self): + rep = self.fetch('/') + assert rep.code==200, 'Testing server response status code: {0}'.format(rep.code) + assert str(rep.body).index('')>=0, 'Expected the "profiles.html" but "index.html"' + +class TestWebGUIWithoutProfiles(AsyncHTTPTestCase, _TestBasic_): + def get_app(self): + if 'PROFILES' in os.environ: del os.environ['PROFILES'] + return self._getApp_() - @tornado.testing.gen_test - def test_profile_with_privatekey(self): - body=self.body.format( - host="127.0.0.1", - port=22, - username="robey", - password="", - profile="", - ) - self._testBody_(body) + def test_get_app_settings(self): + if 'PROFILES' in os.environ: del os.environ['PROFILES'] + settings=get_app_settings(options) + assert 'profiles' not in settings + + def test_with_profiles(self): + rep = self.fetch('/') + assert rep.code==200, 'Testing server response status code: {0}'.format(rep.code) + with pytest.raises(ValueError): + str(rep.body).index('') + assert False, 'Expected the origin "index.html" but "profiles.html"' diff --git a/webssh/handler.py b/webssh/handler.py index ced7819e..aabe8af5 100644 --- a/webssh/handler.py +++ b/webssh/handler.py @@ -387,12 +387,33 @@ def lookup_hostname(self, hostname, port): hostname, port) ) + def get_profile(self): + profiles=self.settings.get('profiles', None) + if profiles: #if the profiles is configurated + value=self.get_argument('profile', None) + if profiles.get('required', False) and len(profiles['profiles'])>0 and not value: + raise InvalidValueError('Argument "profile" is required according to your settings.') + if not (value is None or profiles['profiles'] is None): + return profiles['profiles'][int(value)] + return None + def get_args(self): - hostname = self.get_hostname() - port = self.get_port() - username = self.get_value('username') + profile=self.get_profile() + if profile is not None and len(profile)>0: + hostname=profile['host'] if 'host' in profile else self.get_hostname() + port=profile['port'] if 'port' in profile else self.get_port() + username=profile['username'] if 'username' in profile else self.get_value('username') + if 'private-key' in profile: + filename='' + privatekey=profile['private-key'] + else: + privatekey, filename = self.get_privatekey() + else: + hostname = self.get_hostname() + port = self.get_port() + username = self.get_value('username') + privatekey, filename = self.get_privatekey() password = self.get_argument('password', u'') - privatekey, filename = self.get_privatekey() passphrase = self.get_argument('passphrase', u'') totp = self.get_argument('totp', u'') @@ -488,7 +509,10 @@ def head(self): pass def get(self): - self.render('index.html', debug=self.debug, font=self.font) + if self.settings.get('profiles') is not None and len(self.settings.get('profiles'))>0: + self.render('profiles.html', profiles=self.settings.get('profiles'), debug=self.debug, font=self.font) + else: + self.render('index.html', debug=self.debug, font=self.font) @tornado.gen.coroutine def post(self): diff --git a/webssh/settings.py b/webssh/settings.py index e510018d..e9304e83 100644 --- a/webssh/settings.py +++ b/webssh/settings.py @@ -76,6 +76,26 @@ def get_family(self, filename): def get_url(self, filename, dirs): return os.path.join(*(dirs + [filename])) +def get_profiles(): + filename=os.getenv('PROFILES', None) + if filename: + if not filename.startswith(os.sep): filename=os.path.join(os.path.abspath(os.sep), filename) + try: + if not os.path.exists(filename): raise FileNotFoundError() + with open(filename, 'r') as fp: + result=yaml.load(fp, Loader=SafeLoader) + if result: + idx=0 + for p in result['profiles']: + p['index']=idx + idx+=1 + result['required']=bool(result.get('required', 'False')) + return result + except FileNotFoundError: + logging.warning('Cannot found file profiles: {0}'.format(filename)) + except: + logging.warning('Unexpected error', exc_info=True) + return None def get_app_settings(options): settings = dict( @@ -91,18 +111,8 @@ def get_app_settings(options): ), origin_policy=get_origin_setting(options) ) - filename=os.getenv('PROFILES', None) - if filename: - if not filename.startswith(os.sep): filename=os.path.join(os.path.abspath(os.sep), filename) - try: - if not os.path.exists(filename): raise FileNotFoundError() - with open(filename, 'r') as fp: - settings['profiles']=yaml.load(fp, Loader=SafeLoader) - if settings['profiles']: settings['profiles']=settings['profiles']['profiles'] - except FileNotFoundError: - logging.warning('Cannot found file profiles: {0}'.format(filename)) - except: - logging.warning('Unexpected error', exc_info=True) + settings['profiles']=get_profiles() + if not settings['profiles']: del settings['profiles'] return settings diff --git a/webssh/static/css/cookiealert.css b/webssh/static/css/cookiealert.css new file mode 100644 index 00000000..72e9362c --- /dev/null +++ b/webssh/static/css/cookiealert.css @@ -0,0 +1,36 @@ +/* + * Bootstrap Cookie Alert by Wruczek + * https://github.com/Wruczek/Bootstrap-Cookie-Alert + * Released under MIT license + */ +.cookiealert { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + margin: 0 !important; + z-index: 999; + opacity: 0; + visibility: hidden; + border-radius: 0; + transform: translateY(100%); + transition: all 500ms ease-out; + color: #ecf0f1; + background: #212327 url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAASwAAAEECAIAAAAd4J55AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyhpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTMyIDc5LjE1OTI4NCwgMjAxNi8wNC8xOS0xMzoxMzo0MCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIDIwMTUuNSAoV2luZG93cykiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OUIyQzg0Q0RDQ0ExMTFFNjkyMDJGQkMzNjQ3OUEyMTQiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OUIyQzg0Q0VDQ0ExMTFFNjkyMDJGQkMzNjQ3OUEyMTQiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo5QjJDODRDQkNDQTExMUU2OTIwMkZCQzM2NDc5QTIxNCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo5QjJDODRDQ0NDQTExMUU2OTIwMkZCQzM2NDc5QTIxNCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PnMLhJsAAB3qSURBVHja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'); +} + +.cookiealert.show { + opacity: 1; + visibility: visible; + transform: translateY(0%); + transition-delay: 1000ms; +} + +.cookiealert a { + text-decoration: underline +} + +.cookiealert .acceptcookies { + margin-left: 10px; + vertical-align: baseline; +} diff --git a/webssh/static/js/cookiealert.js b/webssh/static/js/cookiealert.js new file mode 100644 index 00000000..4f982f5f --- /dev/null +++ b/webssh/static/js/cookiealert.js @@ -0,0 +1,56 @@ +/* + * Bootstrap Cookie Alert by Wruczek + * https://github.com/Wruczek/Bootstrap-Cookie-Alert + * Released under MIT license + */ +(function () { + "use strict"; + + var cookieAlert = document.querySelector(".cookiealert"); + var acceptCookies = document.querySelector(".acceptcookies"); + + if (!cookieAlert) { + return; + } + + cookieAlert.offsetHeight; // Force browser to trigger reflow (https://stackoverflow.com/a/39451131) + + // Show the alert if we cant find the "acceptCookies" cookie + if (!getCookie("acceptCookies")) { + cookieAlert.classList.add("show"); + } + + // When clicking on the agree button, create a 1 year + // cookie to remember user's choice and close the banner + acceptCookies.addEventListener("click", function () { + setCookie("acceptCookies", true, 365); + cookieAlert.classList.remove("show"); + + // dispatch the accept event + window.dispatchEvent(new Event("cookieAlertAccept")) + }); + + // Cookie functions from w3schools + function setCookie(cname, cvalue, exdays) { + var d = new Date(); + d.setTime(d.getTime() + (exdays * 24 * 60 * 60 * 1000)); + var expires = "expires=" + d.toUTCString(); + document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; + } + + function getCookie(cname) { + var name = cname + "="; + var decodedCookie = decodeURIComponent(document.cookie); + var ca = decodedCookie.split(';'); + for (var i = 0; i < ca.length; i++) { + var c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1); + } + if (c.indexOf(name) === 0) { + return c.substring(name.length, c.length); + } + } + return ""; + } +})(); diff --git a/webssh/static/js/js.cookie.min.js b/webssh/static/js/js.cookie.min.js new file mode 100644 index 00000000..90a76722 --- /dev/null +++ b/webssh/static/js/js.cookie.min.js @@ -0,0 +1,2 @@ +/*! js-cookie v3.0.1 | MIT */ +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e=e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t + + + + WebSSH + + + + + + + + + + + + + +
+
+
+
+ + + + + + + + + + + + + + + + + From 3093298eb1f8a0caaba31504ded574770138a270 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Mon, 6 Jun 2022 18:25:01 +0100 Subject: [PATCH 06/20] Empty commit to trigger the CICD. Tag: 1.7.0 From af99feae42d3395b2e7411fa791f5c77e1f6d1b8 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Tue, 7 Jun 2022 12:00:06 +0100 Subject: [PATCH 07/20] Hide the server connection setting for security reason. Due to security reason, the server connection was hidden during selection of the profile. --- webssh/static/js/jquery.validation-1.19.3.min.js | 4 ++++ webssh/static/js/profiles.js | 10 ++++++---- webssh/templates/profiles.html | 15 ++++++++------- 3 files changed, 18 insertions(+), 11 deletions(-) create mode 100644 webssh/static/js/jquery.validation-1.19.3.min.js diff --git a/webssh/static/js/jquery.validation-1.19.3.min.js b/webssh/static/js/jquery.validation-1.19.3.min.js new file mode 100644 index 00000000..7f5f510e --- /dev/null +++ b/webssh/static/js/jquery.validation-1.19.3.min.js @@ -0,0 +1,4 @@ +/*! jQuery Validation Plugin - v1.19.3 - 1/9/2021 + * https://jqueryvalidation.org/ + * Copyright (c) 2021 Jörn Zaefferer; Licensed MIT */ +!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof module&&module.exports?module.exports=a(require("jquery")):a(jQuery)}(function(a){a.extend(a.fn,{validate:function(b){if(!this.length)return void(b&&b.debug&&window.console&&console.warn("Nothing selected, can't validate, returning nothing."));var c=a.data(this[0],"validator");return c?c:(this.attr("novalidate","novalidate"),c=new a.validator(b,this[0]),a.data(this[0],"validator",c),c.settings.onsubmit&&(this.on("click.validate",":submit",function(b){c.submitButton=b.currentTarget,a(this).hasClass("cancel")&&(c.cancelSubmit=!0),void 0!==a(this).attr("formnovalidate")&&(c.cancelSubmit=!0)}),this.on("submit.validate",function(b){function d(){var d,e;return c.submitButton&&(c.settings.submitHandler||c.formSubmitted)&&(d=a("").attr("name",c.submitButton.name).val(a(c.submitButton).val()).appendTo(c.currentForm)),!(c.settings.submitHandler&&!c.settings.debug)||(e=c.settings.submitHandler.call(c,c.currentForm,b),d&&d.remove(),void 0!==e&&e)}return c.settings.debug&&b.preventDefault(),c.cancelSubmit?(c.cancelSubmit=!1,d()):c.form()?c.pendingRequest?(c.formSubmitted=!0,!1):d():(c.focusInvalid(),!1)})),c)},valid:function(){var b,c,d;return a(this[0]).is("form")?b=this.validate().form():(d=[],b=!0,c=a(this[0].form).validate(),this.each(function(){b=c.element(this)&&b,b||(d=d.concat(c.errorList))}),c.errorList=d),b},rules:function(b,c){var d,e,f,g,h,i,j=this[0],k="undefined"!=typeof this.attr("contenteditable")&&"false"!==this.attr("contenteditable");if(null!=j&&(!j.form&&k&&(j.form=this.closest("form")[0],j.name=this.attr("name")),null!=j.form)){if(b)switch(d=a.data(j.form,"validator").settings,e=d.rules,f=a.validator.staticRules(j),b){case"add":a.extend(f,a.validator.normalizeRule(c)),delete f.messages,e[j.name]=f,c.messages&&(d.messages[j.name]=a.extend(d.messages[j.name],c.messages));break;case"remove":return c?(i={},a.each(c.split(/\s/),function(a,b){i[b]=f[b],delete f[b]}),i):(delete e[j.name],f)}return g=a.validator.normalizeRules(a.extend({},a.validator.classRules(j),a.validator.attributeRules(j),a.validator.dataRules(j),a.validator.staticRules(j)),j),g.required&&(h=g.required,delete g.required,g=a.extend({required:h},g)),g.remote&&(h=g.remote,delete g.remote,g=a.extend(g,{remote:h})),g}}});var b=function(a){return a.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g,"")};a.extend(a.expr.pseudos||a.expr[":"],{blank:function(c){return!b(""+a(c).val())},filled:function(c){var d=a(c).val();return null!==d&&!!b(""+d)},unchecked:function(b){return!a(b).prop("checked")}}),a.validator=function(b,c){this.settings=a.extend(!0,{},a.validator.defaults,b),this.currentForm=c,this.init()},a.validator.format=function(b,c){return 1===arguments.length?function(){var c=a.makeArray(arguments);return c.unshift(b),a.validator.format.apply(this,c)}:void 0===c?b:(arguments.length>2&&c.constructor!==Array&&(c=a.makeArray(arguments).slice(1)),c.constructor!==Array&&(c=[c]),a.each(c,function(a,c){b=b.replace(new RegExp("\\{"+a+"\\}","g"),function(){return c})}),b)},a.extend(a.validator,{defaults:{messages:{},groups:{},rules:{},errorClass:"error",pendingClass:"pending",validClass:"valid",errorElement:"label",focusCleanup:!1,focusInvalid:!0,errorContainer:a([]),errorLabelContainer:a([]),onsubmit:!0,ignore:":hidden",ignoreTitle:!1,onfocusin:function(a){this.lastActive=a,this.settings.focusCleanup&&(this.settings.unhighlight&&this.settings.unhighlight.call(this,a,this.settings.errorClass,this.settings.validClass),this.hideThese(this.errorsFor(a)))},onfocusout:function(a){this.checkable(a)||!(a.name in this.submitted)&&this.optional(a)||this.element(a)},onkeyup:function(b,c){var d=[16,17,18,20,35,36,37,38,39,40,45,144,225];9===c.which&&""===this.elementValue(b)||a.inArray(c.keyCode,d)!==-1||(b.name in this.submitted||b.name in this.invalid)&&this.element(b)},onclick:function(a){a.name in this.submitted?this.element(a):a.parentNode.name in this.submitted&&this.element(a.parentNode)},highlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).addClass(c).removeClass(d):a(b).addClass(c).removeClass(d)},unhighlight:function(b,c,d){"radio"===b.type?this.findByName(b.name).removeClass(c).addClass(d):a(b).removeClass(c).addClass(d)}},setDefaults:function(b){a.extend(a.validator.defaults,b)},messages:{required:"This field is required.",remote:"Please fix this field.",email:"Please enter a valid email address.",url:"Please enter a valid URL.",date:"Please enter a valid date.",dateISO:"Please enter a valid date (ISO).",number:"Please enter a valid number.",digits:"Please enter only digits.",equalTo:"Please enter the same value again.",maxlength:a.validator.format("Please enter no more than {0} characters."),minlength:a.validator.format("Please enter at least {0} characters."),rangelength:a.validator.format("Please enter a value between {0} and {1} characters long."),range:a.validator.format("Please enter a value between {0} and {1}."),max:a.validator.format("Please enter a value less than or equal to {0}."),min:a.validator.format("Please enter a value greater than or equal to {0}."),step:a.validator.format("Please enter a multiple of {0}.")},autoCreateRanges:!1,prototype:{init:function(){function b(b){var c="undefined"!=typeof a(this).attr("contenteditable")&&"false"!==a(this).attr("contenteditable");if(!this.form&&c&&(this.form=a(this).closest("form")[0],this.name=a(this).attr("name")),d===this.form){var e=a.data(this.form,"validator"),f="on"+b.type.replace(/^validate/,""),g=e.settings;g[f]&&!a(this).is(g.ignore)&&g[f].call(e,this,b)}}this.labelContainer=a(this.settings.errorLabelContainer),this.errorContext=this.labelContainer.length&&this.labelContainer||a(this.currentForm),this.containers=a(this.settings.errorContainer).add(this.settings.errorLabelContainer),this.submitted={},this.valueCache={},this.pendingRequest=0,this.pending={},this.invalid={},this.reset();var c,d=this.currentForm,e=this.groups={};a.each(this.settings.groups,function(b,c){"string"==typeof c&&(c=c.split(/\s/)),a.each(c,function(a,c){e[c]=b})}),c=this.settings.rules,a.each(c,function(b,d){c[b]=a.validator.normalizeRule(d)}),a(this.currentForm).on("focusin.validate focusout.validate keyup.validate",":text, [type='password'], [type='file'], select, textarea, [type='number'], [type='search'], [type='tel'], [type='url'], [type='email'], [type='datetime'], [type='date'], [type='month'], [type='week'], [type='time'], [type='datetime-local'], [type='range'], [type='color'], [type='radio'], [type='checkbox'], [contenteditable], [type='button']",b).on("click.validate","select, option, [type='radio'], [type='checkbox']",b),this.settings.invalidHandler&&a(this.currentForm).on("invalid-form.validate",this.settings.invalidHandler)},form:function(){return this.checkForm(),a.extend(this.submitted,this.errorMap),this.invalid=a.extend({},this.errorMap),this.valid()||a(this.currentForm).triggerHandler("invalid-form",[this]),this.showErrors(),this.valid()},checkForm:function(){this.prepareForm();for(var a=0,b=this.currentElements=this.elements();b[a];a++)this.check(b[a]);return this.valid()},element:function(b){var c,d,e=this.clean(b),f=this.validationTargetFor(e),g=this,h=!0;return void 0===f?delete this.invalid[e.name]:(this.prepareElement(f),this.currentElements=a(f),d=this.groups[f.name],d&&a.each(this.groups,function(a,b){b===d&&a!==f.name&&(e=g.validationTargetFor(g.clean(g.findByName(a))),e&&e.name in g.invalid&&(g.currentElements.push(e),h=g.check(e)&&h))}),c=this.check(f)!==!1,h=h&&c,c?this.invalid[f.name]=!1:this.invalid[f.name]=!0,this.numberOfInvalids()||(this.toHide=this.toHide.add(this.containers)),this.showErrors(),a(b).attr("aria-invalid",!c)),h},showErrors:function(b){if(b){var c=this;a.extend(this.errorMap,b),this.errorList=a.map(this.errorMap,function(a,b){return{message:a,element:c.findByName(b)[0]}}),this.successList=a.grep(this.successList,function(a){return!(a.name in b)})}this.settings.showErrors?this.settings.showErrors.call(this,this.errorMap,this.errorList):this.defaultShowErrors()},resetForm:function(){a.fn.resetForm&&a(this.currentForm).resetForm(),this.invalid={},this.submitted={},this.prepareForm(),this.hideErrors();var b=this.elements().removeData("previousValue").removeAttr("aria-invalid");this.resetElements(b)},resetElements:function(a){var b;if(this.settings.unhighlight)for(b=0;a[b];b++)this.settings.unhighlight.call(this,a[b],this.settings.errorClass,""),this.findByName(a[b].name).removeClass(this.settings.validClass);else a.removeClass(this.settings.errorClass).removeClass(this.settings.validClass)},numberOfInvalids:function(){return this.objectLength(this.invalid)},objectLength:function(a){var b,c=0;for(b in a)void 0!==a[b]&&null!==a[b]&&a[b]!==!1&&c++;return c},hideErrors:function(){this.hideThese(this.toHide)},hideThese:function(a){a.not(this.containers).text(""),this.addWrapper(a).hide()},valid:function(){return 0===this.size()},size:function(){return this.errorList.length},focusInvalid:function(){if(this.settings.focusInvalid)try{a(this.findLastActive()||this.errorList.length&&this.errorList[0].element||[]).filter(":visible").trigger("focus").trigger("focusin")}catch(b){}},findLastActive:function(){var b=this.lastActive;return b&&1===a.grep(this.errorList,function(a){return a.element.name===b.name}).length&&b},elements:function(){var b=this,c={};return a(this.currentForm).find("input, select, textarea, [contenteditable]").not(":submit, :reset, :image, :disabled").not(this.settings.ignore).filter(function(){var d=this.name||a(this).attr("name"),e="undefined"!=typeof a(this).attr("contenteditable")&&"false"!==a(this).attr("contenteditable");return!d&&b.settings.debug&&window.console&&console.error("%o has no name assigned",this),e&&(this.form=a(this).closest("form")[0],this.name=d),this.form===b.currentForm&&(!(d in c||!b.objectLength(a(this).rules()))&&(c[d]=!0,!0))})},clean:function(b){return a(b)[0]},errors:function(){var b=this.settings.errorClass.split(" ").join(".");return a(this.settings.errorElement+"."+b,this.errorContext)},resetInternals:function(){this.successList=[],this.errorList=[],this.errorMap={},this.toShow=a([]),this.toHide=a([])},reset:function(){this.resetInternals(),this.currentElements=a([])},prepareForm:function(){this.reset(),this.toHide=this.errors().add(this.containers)},prepareElement:function(a){this.reset(),this.toHide=this.errorsFor(a)},elementValue:function(b){var c,d,e=a(b),f=b.type,g="undefined"!=typeof e.attr("contenteditable")&&"false"!==e.attr("contenteditable");return"radio"===f||"checkbox"===f?this.findByName(b.name).filter(":checked").val():"number"===f&&"undefined"!=typeof b.validity?b.validity.badInput?"NaN":e.val():(c=g?e.text():e.val(),"file"===f?"C:\\fakepath\\"===c.substr(0,12)?c.substr(12):(d=c.lastIndexOf("/"),d>=0?c.substr(d+1):(d=c.lastIndexOf("\\"),d>=0?c.substr(d+1):c)):"string"==typeof c?c.replace(/\r/g,""):c)},check:function(b){b=this.validationTargetFor(this.clean(b));var c,d,e,f,g=a(b).rules(),h=a.map(g,function(a,b){return b}).length,i=!1,j=this.elementValue(b);"function"==typeof g.normalizer?f=g.normalizer:"function"==typeof this.settings.normalizer&&(f=this.settings.normalizer),f&&(j=f.call(b,j),delete g.normalizer);for(d in g){e={method:d,parameters:g[d]};try{if(c=a.validator.methods[d].call(this,j,b,e.parameters),"dependency-mismatch"===c&&1===h){i=!0;continue}if(i=!1,"pending"===c)return void(this.toHide=this.toHide.not(this.errorsFor(b)));if(!c)return this.formatAndAdd(b,e),!1}catch(k){throw this.settings.debug&&window.console&&console.log("Exception occurred when checking element "+b.id+", check the '"+e.method+"' method.",k),k instanceof TypeError&&(k.message+=". Exception occurred when checking element "+b.id+", check the '"+e.method+"' method."),k}}if(!i)return this.objectLength(g)&&this.successList.push(b),!0},customDataMessage:function(b,c){return a(b).data("msg"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase())||a(b).data("msg")},customMessage:function(a,b){var c=this.settings.messages[a];return c&&(c.constructor===String?c:c[b])},findDefined:function(){for(var a=0;aWarning: No message defined for "+b.name+""),e=/\$?\{(\d+)\}/g;return"function"==typeof d?d=d.call(this,c.parameters,b):e.test(d)&&(d=a.validator.format(d.replace(e,"{$1}"),c.parameters)),d},formatAndAdd:function(a,b){var c=this.defaultMessage(a,b);this.errorList.push({message:c,element:a,method:b.method}),this.errorMap[a.name]=c,this.submitted[a.name]=c},addWrapper:function(a){return this.settings.wrapper&&(a=a.add(a.parent(this.settings.wrapper))),a},defaultShowErrors:function(){var a,b,c;for(a=0;this.errorList[a];a++)c=this.errorList[a],this.settings.highlight&&this.settings.highlight.call(this,c.element,this.settings.errorClass,this.settings.validClass),this.showLabel(c.element,c.message);if(this.errorList.length&&(this.toShow=this.toShow.add(this.containers)),this.settings.success)for(a=0;this.successList[a];a++)this.showLabel(this.successList[a]);if(this.settings.unhighlight)for(a=0,b=this.validElements();b[a];a++)this.settings.unhighlight.call(this,b[a],this.settings.errorClass,this.settings.validClass);this.toHide=this.toHide.not(this.toShow),this.hideErrors(),this.addWrapper(this.toShow).show()},validElements:function(){return this.currentElements.not(this.invalidElements())},invalidElements:function(){return a(this.errorList).map(function(){return this.element})},showLabel:function(b,c){var d,e,f,g,h=this.errorsFor(b),i=this.idOrName(b),j=a(b).attr("aria-describedby");h.length?(h.removeClass(this.settings.validClass).addClass(this.settings.errorClass),h.html(c)):(h=a("<"+this.settings.errorElement+">").attr("id",i+"-error").addClass(this.settings.errorClass).html(c||""),d=h,this.settings.wrapper&&(d=h.hide().show().wrap("<"+this.settings.wrapper+"/>").parent()),this.labelContainer.length?this.labelContainer.append(d):this.settings.errorPlacement?this.settings.errorPlacement.call(this,d,a(b)):d.insertAfter(b),h.is("label")?h.attr("for",i):0===h.parents("label[for='"+this.escapeCssMeta(i)+"']").length&&(f=h.attr("id"),j?j.match(new RegExp("\\b"+this.escapeCssMeta(f)+"\\b"))||(j+=" "+f):j=f,a(b).attr("aria-describedby",j),e=this.groups[b.name],e&&(g=this,a.each(g.groups,function(b,c){c===e&&a("[name='"+g.escapeCssMeta(b)+"']",g.currentForm).attr("aria-describedby",h.attr("id"))})))),!c&&this.settings.success&&(h.text(""),"string"==typeof this.settings.success?h.addClass(this.settings.success):this.settings.success(h,b)),this.toShow=this.toShow.add(h)},errorsFor:function(b){var c=this.escapeCssMeta(this.idOrName(b)),d=a(b).attr("aria-describedby"),e="label[for='"+c+"'], label[for='"+c+"'] *";return d&&(e=e+", #"+this.escapeCssMeta(d).replace(/\s+/g,", #")),this.errors().filter(e)},escapeCssMeta:function(a){return a.replace(/([\\!"#$%&'()*+,.\/:;<=>?@\[\]^`{|}~])/g,"\\$1")},idOrName:function(a){return this.groups[a.name]||(this.checkable(a)?a.name:a.id||a.name)},validationTargetFor:function(b){return this.checkable(b)&&(b=this.findByName(b.name)),a(b).not(this.settings.ignore)[0]},checkable:function(a){return/radio|checkbox/i.test(a.type)},findByName:function(b){return a(this.currentForm).find("[name='"+this.escapeCssMeta(b)+"']")},getLength:function(b,c){switch(c.nodeName.toLowerCase()){case"select":return a("option:selected",c).length;case"input":if(this.checkable(c))return this.findByName(c.name).filter(":checked").length}return b.length},depend:function(a,b){return!this.dependTypes[typeof a]||this.dependTypes[typeof a](a,b)},dependTypes:{"boolean":function(a){return a},string:function(b,c){return!!a(b,c.form).length},"function":function(a,b){return a(b)}},optional:function(b){var c=this.elementValue(b);return!a.validator.methods.required.call(this,c,b)&&"dependency-mismatch"},startRequest:function(b){this.pending[b.name]||(this.pendingRequest++,a(b).addClass(this.settings.pendingClass),this.pending[b.name]=!0)},stopRequest:function(b,c){this.pendingRequest--,this.pendingRequest<0&&(this.pendingRequest=0),delete this.pending[b.name],a(b).removeClass(this.settings.pendingClass),c&&0===this.pendingRequest&&this.formSubmitted&&this.form()?(a(this.currentForm).submit(),this.submitButton&&a("input:hidden[name='"+this.submitButton.name+"']",this.currentForm).remove(),this.formSubmitted=!1):!c&&0===this.pendingRequest&&this.formSubmitted&&(a(this.currentForm).triggerHandler("invalid-form",[this]),this.formSubmitted=!1)},previousValue:function(b,c){return c="string"==typeof c&&c||"remote",a.data(b,"previousValue")||a.data(b,"previousValue",{old:null,valid:!0,message:this.defaultMessage(b,{method:c})})},destroy:function(){this.resetForm(),a(this.currentForm).off(".validate").removeData("validator").find(".validate-equalTo-blur").off(".validate-equalTo").removeClass("validate-equalTo-blur").find(".validate-lessThan-blur").off(".validate-lessThan").removeClass("validate-lessThan-blur").find(".validate-lessThanEqual-blur").off(".validate-lessThanEqual").removeClass("validate-lessThanEqual-blur").find(".validate-greaterThanEqual-blur").off(".validate-greaterThanEqual").removeClass("validate-greaterThanEqual-blur").find(".validate-greaterThan-blur").off(".validate-greaterThan").removeClass("validate-greaterThan-blur")}},classRuleSettings:{required:{required:!0},email:{email:!0},url:{url:!0},date:{date:!0},dateISO:{dateISO:!0},number:{number:!0},digits:{digits:!0},creditcard:{creditcard:!0}},addClassRules:function(b,c){b.constructor===String?this.classRuleSettings[b]=c:a.extend(this.classRuleSettings,b)},classRules:function(b){var c={},d=a(b).attr("class");return d&&a.each(d.split(" "),function(){this in a.validator.classRuleSettings&&a.extend(c,a.validator.classRuleSettings[this])}),c},normalizeAttributeRule:function(a,b,c,d){/min|max|step/.test(c)&&(null===b||/number|range|text/.test(b))&&(d=Number(d),isNaN(d)&&(d=void 0)),d||0===d?a[c]=d:b===c&&"range"!==b&&(a[c]=!0)},attributeRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)"required"===c?(d=b.getAttribute(c),""===d&&(d=!0),d=!!d):d=f.attr(c),this.normalizeAttributeRule(e,g,c,d);return e.maxlength&&/-1|2147483647|524288/.test(e.maxlength)&&delete e.maxlength,e},dataRules:function(b){var c,d,e={},f=a(b),g=b.getAttribute("type");for(c in a.validator.methods)d=f.data("rule"+c.charAt(0).toUpperCase()+c.substring(1).toLowerCase()),""===d&&(d=!0),this.normalizeAttributeRule(e,g,c,d);return e},staticRules:function(b){var c={},d=a.data(b.form,"validator");return d.settings.rules&&(c=a.validator.normalizeRule(d.settings.rules[b.name])||{}),c},normalizeRules:function(b,c){return a.each(b,function(d,e){if(e===!1)return void delete b[d];if(e.param||e.depends){var f=!0;switch(typeof e.depends){case"string":f=!!a(e.depends,c.form).length;break;case"function":f=e.depends.call(c,c)}f?b[d]=void 0===e.param||e.param:(a.data(c.form,"validator").resetElements(a(c)),delete b[d])}}),a.each(b,function(a,d){b[a]="function"==typeof d&&"normalizer"!==a?d(c):d}),a.each(["minlength","maxlength"],function(){b[this]&&(b[this]=Number(b[this]))}),a.each(["rangelength","range"],function(){var a;b[this]&&(Array.isArray(b[this])?b[this]=[Number(b[this][0]),Number(b[this][1])]:"string"==typeof b[this]&&(a=b[this].replace(/[\[\]]/g,"").split(/[\s,]+/),b[this]=[Number(a[0]),Number(a[1])]))}),a.validator.autoCreateRanges&&(null!=b.min&&null!=b.max&&(b.range=[b.min,b.max],delete b.min,delete b.max),null!=b.minlength&&null!=b.maxlength&&(b.rangelength=[b.minlength,b.maxlength],delete b.minlength,delete b.maxlength)),b},normalizeRule:function(b){if("string"==typeof b){var c={};a.each(b.split(/\s/),function(){c[this]=!0}),b=c}return b},addMethod:function(b,c,d){a.validator.methods[b]=c,a.validator.messages[b]=void 0!==d?d:a.validator.messages[b],c.length<3&&a.validator.addClassRules(b,a.validator.normalizeRule(b))},methods:{required:function(b,c,d){if(!this.depend(d,c))return"dependency-mismatch";if("select"===c.nodeName.toLowerCase()){var e=a(c).val();return e&&e.length>0}return this.checkable(c)?this.getLength(b,c)>0:void 0!==b&&null!==b&&b.length>0},email:function(a,b){return this.optional(b)||/^[a-zA-Z0-9.!#$%&'*+\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/.test(a)},url:function(a,b){return this.optional(b)||/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[\/?#]\S*)?$/i.test(a)},date:function(){var a=!1;return function(b,c){return a||(a=!0,this.settings.debug&&window.console&&console.warn("The `date` method is deprecated and will be removed in version '2.0.0'.\nPlease don't use it, since it relies on the Date constructor, which\nbehaves very differently across browsers and locales. Use `dateISO`\ninstead or one of the locale specific methods in `localizations/`\nand `additional-methods.js`.")),this.optional(c)||!/Invalid|NaN/.test(new Date(b).toString())}}(),dateISO:function(a,b){return this.optional(b)||/^\d{4}[\/\-](0?[1-9]|1[012])[\/\-](0?[1-9]|[12][0-9]|3[01])$/.test(a)},number:function(a,b){return this.optional(b)||/^(?:-?\d+|-?\d{1,3}(?:,\d{3})+)?(?:\.\d+)?$/.test(a)},digits:function(a,b){return this.optional(b)||/^\d+$/.test(a)},minlength:function(a,b,c){var d=Array.isArray(a)?a.length:this.getLength(a,b);return this.optional(b)||d>=c},maxlength:function(a,b,c){var d=Array.isArray(a)?a.length:this.getLength(a,b);return this.optional(b)||d<=c},rangelength:function(a,b,c){var d=Array.isArray(a)?a.length:this.getLength(a,b);return this.optional(b)||d>=c[0]&&d<=c[1]},min:function(a,b,c){return this.optional(b)||a>=c},max:function(a,b,c){return this.optional(b)||a<=c},range:function(a,b,c){return this.optional(b)||a>=c[0]&&a<=c[1]},step:function(b,c,d){var e,f=a(c).attr("type"),g="Step attribute on input type "+f+" is not supported.",h=["text","number","range"],i=new RegExp("\\b"+f+"\\b"),j=f&&!i.test(h.join()),k=function(a){var b=(""+a).match(/(?:\.(\d+))?$/);return b&&b[1]?b[1].length:0},l=function(a){return Math.round(a*Math.pow(10,e))},m=!0;if(j)throw new Error(g);return e=k(d),(k(b)>e||l(b)%l(d)!==0)&&(m=!1),this.optional(c)||m},equalTo:function(b,c,d){var e=a(d);return this.settings.onfocusout&&e.not(".validate-equalTo-blur").length&&e.addClass("validate-equalTo-blur").on("blur.validate-equalTo",function(){a(c).valid()}),b===e.val()},remote:function(b,c,d,e){if(this.optional(c))return"dependency-mismatch";e="string"==typeof e&&e||"remote";var f,g,h,i=this.previousValue(c,e);return this.settings.messages[c.name]||(this.settings.messages[c.name]={}),i.originalMessage=i.originalMessage||this.settings.messages[c.name][e],this.settings.messages[c.name][e]=i.message,d="string"==typeof d&&{url:d}||d,h=a.param(a.extend({data:b},d.data)),i.old===h?i.valid:(i.old=h,f=this,this.startRequest(c),g={},g[c.name]=b,a.ajax(a.extend(!0,{mode:"abort",port:"validate"+c.name,dataType:"json",data:g,context:f.currentForm,success:function(a){var d,g,h,j=a===!0||"true"===a;f.settings.messages[c.name][e]=i.originalMessage,j?(h=f.formSubmitted,f.resetInternals(),f.toHide=f.errorsFor(c),f.formSubmitted=h,f.successList.push(c),f.invalid[c.name]=!1,f.showErrors()):(d={},g=a||f.defaultMessage(c,{method:e,parameters:b}),d[c.name]=i.message=g,f.invalid[c.name]=!0,f.showErrors(d)),i.valid=j,f.stopRequest(c,j)}},d)),"pending")}}});var c,d={};return a.ajaxPrefilter?a.ajaxPrefilter(function(a,b,c){var e=a.port;"abort"===a.mode&&(d[e]&&d[e].abort(),d[e]=c)}):(c=a.ajax,a.ajax=function(b){var e=("mode"in b?b:a.ajaxSettings).mode,f=("port"in b?b:a.ajaxSettings).port;return"abort"===e?(d[f]&&d[f].abort(),d[f]=c.apply(this,arguments),d[f]):c.apply(this,arguments)}),a}); \ No newline at end of file diff --git a/webssh/static/js/profiles.js b/webssh/static/js/profiles.js index ec53c872..43bbd3fd 100644 --- a/webssh/static/js/profiles.js +++ b/webssh/static/js/profiles.js @@ -2,6 +2,7 @@ $(document).ready(function(){ $('[data-toggle="tooltip"]').tooltip(); + $('form').validate({'ignore':'.ignore-validation'}); $('.profile-item').click(function(evt){ console.log('Selected a profile: '+$(this).text()); @@ -10,6 +11,7 @@ $('input:first').val($(this).attr('value')); let profile=$(this).attr('value'); + if(profile=='')profile='-1'; let found=false; for(var i=0; i - + @@ -119,14 +119,15 @@ const profiles=[{%for p in profiles['profiles']%}{ "index": {{p['index']}}, "name": "{{p['name']}}", - {%if 'host' in p%}"host": "{{p['host']}}",{%end%} - {%if 'port' in p%}"port": {{p['port']}},{%end%} - {%if 'username' in p%}"username": "{{p['username']}}",{%end%} - "private-key": {%if 'private-key' in p and len(p['private-key'])>0%}true{%else%}false{%end%}, + "host": {{'true' if 'host' in p else 'false'}}, + "port": {{'true' if 'port' in p else 'false'}}, + "username": {{'true' if 'username' in p else 'false'}}, + "private-key": {{'true' if ('private-key' in p and len(p['private-key'])>0) else 'false'}}, },{%end%} ]; //> + @@ -134,6 +135,6 @@ - + From bd7ba275953729f67524f01d0755220dde0a14fa Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Tue, 12 Jul 2022 10:29:28 +0100 Subject: [PATCH 08/20] Set the cookies to 30days instead of default expiry (current session only) --- webssh/static/js/profiles.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webssh/static/js/profiles.js b/webssh/static/js/profiles.js index 43bbd3fd..7f857c95 100644 --- a/webssh/static/js/profiles.js +++ b/webssh/static/js/profiles.js @@ -40,8 +40,10 @@ if(Boolean(Cookies.get('acceptCookies'))){ console.debug('Store the profile: '+profile['index']+' - '+profile['name']); - Cookies.set('profileIndex', profile['index']); - Cookies.set('profileName', profile['name']); + let expired=new Date(); + expired.setTime(expired.getTime()*30*86400000); //expired=now+30days; 86400000=1000*60*60*24 + Cookies.set('profileIndex', profile['index'], {'expires':expired, 'path':'/'}); + Cookies.set('profileName', profile['name'], {'expires':expired, 'path':'/'}); } return this; From 3dfd712fadd83a93abaf69f5ce14607728db8757 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Tue, 12 Jul 2022 11:14:24 +0100 Subject: [PATCH 09/20] Update the release script --- .gitlab-ci.yaml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml index c8513e8b..dacd24e0 100644 --- a/.gitlab-ci.yaml +++ b/.gitlab-ci.yaml @@ -173,11 +173,13 @@ releasing: script: - > echo "Creating release: $VERSION..." - release: - name: "Release - $VERSION" - description: "$CI_COMMIT_REF_NAME - $CI_COMMIT_MESSAGE" - tag_name: "releases/$VERSION" - ref: '$CI_COMMIT_REF_NAME' + - > + release-cli create --name "Release - ${VERSION}" --description "${CI_COMMIT_REF_NAME} - ${CI_COMMIT_MESSAGE}" --tag-name "releases/${VERSION}" --ref "${CI_COMMIT_REF_NAME}" +# release: +# name: "Release - $VERSION" +# description: "$CI_COMMIT_REF_NAME - $CI_COMMIT_MESSAGE" +# tag_name: "releases/$VERSION" +# ref: '$CI_COMMIT_REF_NAME' # Deploy the binary/output into production. Usually askying the k8s to update the image deploying: @@ -203,7 +205,7 @@ deploying: exit 1; fi - > - echo "Deploying..." + echo "Deploying ${VERSION} ..." && echo "No implemented yet!" # Cleaning the CI/CD From fc0d7b1eb3f6e87061e15ed393df1df22306b43c Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Tue, 12 Jul 2022 11:36:53 +0100 Subject: [PATCH 10/20] Fixing the expires --- .gitlab-ci.yaml | 14 ++++++++------ webssh/static/js/profiles.js | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml index c8513e8b..dacd24e0 100644 --- a/.gitlab-ci.yaml +++ b/.gitlab-ci.yaml @@ -173,11 +173,13 @@ releasing: script: - > echo "Creating release: $VERSION..." - release: - name: "Release - $VERSION" - description: "$CI_COMMIT_REF_NAME - $CI_COMMIT_MESSAGE" - tag_name: "releases/$VERSION" - ref: '$CI_COMMIT_REF_NAME' + - > + release-cli create --name "Release - ${VERSION}" --description "${CI_COMMIT_REF_NAME} - ${CI_COMMIT_MESSAGE}" --tag-name "releases/${VERSION}" --ref "${CI_COMMIT_REF_NAME}" +# release: +# name: "Release - $VERSION" +# description: "$CI_COMMIT_REF_NAME - $CI_COMMIT_MESSAGE" +# tag_name: "releases/$VERSION" +# ref: '$CI_COMMIT_REF_NAME' # Deploy the binary/output into production. Usually askying the k8s to update the image deploying: @@ -203,7 +205,7 @@ deploying: exit 1; fi - > - echo "Deploying..." + echo "Deploying ${VERSION} ..." && echo "No implemented yet!" # Cleaning the CI/CD diff --git a/webssh/static/js/profiles.js b/webssh/static/js/profiles.js index 7f857c95..3f7e31a8 100644 --- a/webssh/static/js/profiles.js +++ b/webssh/static/js/profiles.js @@ -41,7 +41,7 @@ if(Boolean(Cookies.get('acceptCookies'))){ console.debug('Store the profile: '+profile['index']+' - '+profile['name']); let expired=new Date(); - expired.setTime(expired.getTime()*30*86400000); //expired=now+30days; 86400000=1000*60*60*24 + expired.setTime(expired.getTime()+30*86400000); //expired=now+30days; 86400000=1000*60*60*24 Cookies.set('profileIndex', profile['index'], {'expires':expired, 'path':'/'}); Cookies.set('profileName', profile['name'], {'expires':expired, 'path':'/'}); } From 00aea9d01bcaa265b4f1d8b2947c144c047dceb3 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Tue, 12 Jul 2022 11:45:02 +0100 Subject: [PATCH 11/20] Update the 1.7.3 --- .gitlab-ci.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml index dacd24e0..3779e502 100644 --- a/.gitlab-ci.yaml +++ b/.gitlab-ci.yaml @@ -173,13 +173,11 @@ releasing: script: - > echo "Creating release: $VERSION..." - - > - release-cli create --name "Release - ${VERSION}" --description "${CI_COMMIT_REF_NAME} - ${CI_COMMIT_MESSAGE}" --tag-name "releases/${VERSION}" --ref "${CI_COMMIT_REF_NAME}" -# release: -# name: "Release - $VERSION" -# description: "$CI_COMMIT_REF_NAME - $CI_COMMIT_MESSAGE" -# tag_name: "releases/$VERSION" -# ref: '$CI_COMMIT_REF_NAME' + release: + name: "Release - $CI_COMMIT_REF_NAME" + description: "$CI_COMMIT_REF_NAME - $CI_COMMIT_MESSAGE" + tag_name: "releases/$CI_COMMIT_REF_NAME" + ref: '$CI_COMMIT_REF_NAME' # Deploy the binary/output into production. Usually askying the k8s to update the image deploying: From f72b253eb1796b18bd331fde4d3f46649b13f350 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Tue, 4 Oct 2022 15:34:49 +0100 Subject: [PATCH 12/20] Do not show the clear passphrase for security reason --- webssh/handler.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/webssh/handler.py b/webssh/handler.py index aabe8af5..6e5cbcfa 100644 --- a/webssh/handler.py +++ b/webssh/handler.py @@ -178,8 +178,7 @@ def get_pkey_obj(self): logging.error(str(self.last_exception)) msg = 'Invalid key' if self.password: - msg += ' or wrong passphrase "{}" for decrypting it.'.format( - self.password) + msg += ' or wrong passphrase "{}...{}" for decrypting it.'.format(self.password[0:3], self.password[:-3]) raise InvalidValueError(msg) From 6056fb3184a46cee6510250ee7625c13d15caa49 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Mon, 17 Oct 2022 09:23:39 +0100 Subject: [PATCH 13/20] Update the building script --- .gitlab-ci.yaml | 73 ++++++++++++++++++++++++------------------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml index 3779e502..b77fa2dd 100644 --- a/.gitlab-ci.yaml +++ b/.gitlab-ci.yaml @@ -38,16 +38,13 @@ default: # If the tag name is matched with regex: 1.2.3-rc.4+build.5 - if: $CI_COMMIT_TAG =~ /^([0-9]+\.){2}([0-9]+){1}(\-[a-zA-Z\.]*[0-9]+)?(\+[a-zA-Z\.]*[0-9]+)?/ - # If the commit-title is match with regex: 1.2.3-rc.4+build.5 - - if: $CI_COMMIT_TITLE =~ /^([0-9]+\.){2}([0-9]+){1}(\-[a-zA-Z\.]*[0-9]+)?(\+[a-zA-Z\.]*[0-9]+)? -/ - - # If the branch is match with regex, e.g.: 1.2.3-rc.4+build.x - - if: $CI_COMMIT_BRANCH =~ /^([0-9]+\.){0,3}([0-9]+[\+\-]){0,2}x$/ + # If this is protected ref + - if: '$CI_COMMIT_REF_NAME == "true"' # Prepare the versioning prepare: stage: pre - image: "kensonman/versioning:1.0.10" + image: "kensonman/versioning:2.0.1" rules: - !reference [.default-rules, rules] - when: always @@ -60,30 +57,30 @@ prepare: BRANCH: 0.0.0-dev.0-test.x LOGGER_LEVEL: "10" script: - - > + - | echo "Preparing the CI/CD environment: `pwd`..." - && echo " > CI_COMMIT_TITLE:" - && echo ${CI_COMMIT_TITLE} - && echo " > CI_COMMIT_DESCRIPTION:" - && echo ${CI_COMMIT_DESCRIPTION} - && echo " > CI_COMMIT_MESSAGE:" - && echo ${CI_COMMIT_MESSAGE} - && echo " > CI_COMMIT_TAG:" - && echo ${CI_COMMIT_TAG} - && echo " > Detecting the versioning pattern: `version.py getVersionPattern`" - && echo " > Getting the next version..." - && export VERSION=`version.py getNextVersion` - && echo " > Next version: ${VERSION}" - && export DOCKERIZED_VERSION=`version.py toDockerized --option ${VERSION}` - && echo " > Dockerized next version: ${DOCKERIZED_VERSION}" - && echo " > Exporting..." - && echo "VERSION=${VERSION}" > VERSION.env - && echo "DOCKERIZED_VERSION=${DOCKERIZED_VERSION}" > VERSION.env - && echo "${VERSION}" > VERSION + echo " > CI_COMMIT_TITLE:" + echo ${CI_COMMIT_TITLE} + echo " > CI_COMMIT_DESCRIPTION:" + echo ${CI_COMMIT_DESCRIPTION} + echo " > CI_COMMIT_MESSAGE:" + echo ${CI_COMMIT_MESSAGE} + echo " > CI_COMMIT_REF_NAME:CI_COMMIT_REF_PROTECTED" + echo "${CI_COMMIT_REF_NAME}:${CI_COMMIT_REF_PROTECTED" + echo " > Detecting the versioning pattern: `version.py getVersionPattern`" + echo " > Getting the next version..." + export VERSION=`versioning.py getVersion` + echo " > Next version: ${VERSION}" + export DOCKERIZED_VERSION=`versioning.py toDockerized --option ${VERSION}` + echo " > Dockerized next version: ${DOCKERIZED_VERSION}" + echo " > Exporting..." + echo "VERSION=${VERSION}" > VERSION.txt + echo "DOCKERIZED_VERSION=${DOCKERIZED_VERSION}" > VERSION.txt + echo "${VERSION}" > VERSION artifacts: reports: # Put the version into env for sharing the variable between job - dotenv: VERSION.env + dotenv: VERSION.txt paths: # Put the VERSION as an artifact for futer development - VERSION @@ -133,19 +130,19 @@ building: echo "Skip the job due to the VERSION did not specified: JobName: ${CI_JOB_NAME}, Version: ${VERSION}" exit 1; fi - - > + - | echo "Building the repo..." - && export TEMP_IMAGE="${REGISTRY_SERVER}${REGISTRY_REPO}:${REGISTRY_TEMP_TAG}" - && export TARGET_IMAGE="${REGISTRY_SERVER}${REGISTRY_REPO}:${DOCKERIZED_VERSION}" - && echo " > the docker image: ${TEMP_IMAGE}" - && sed 's//${CI_COMMIT_AUTHOR}/g' Dockerfile - && sed 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(\-\(\w*\.\)[0-9]\+\)\{0,2\}/${DOCKERIZED_VERSION}/g' Dockerfile - && docker build -t ${TEMP_IMAGE} . - && echo " > Tagging ${TEMP_IMAGE} => ${TARGET_IMAGE}" - && docker tag ${TEMP_IMAGE} ${TARGET_IMAGE} - && echo " > Sign-In to registry server: ${REGISTRY_SERVER} with username:password ${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" - && echo -n ${REGISTRY_PASSWORD} | base64 -d | docker login --username `echo -n ${REGISTRY_USERNAME} | base64 -d` --password-stdin ${REGISTRY_SERVER} - && docker push ${TARGET_IMAGE} + export TEMP_IMAGE="${REGISTRY_SERVER}${REGISTRY_REPO}:${REGISTRY_TEMP_TAG}" + export TARGET_IMAGE="${REGISTRY_SERVER}${REGISTRY_REPO}:${DOCKERIZED_VERSION}" + echo " > the docker image: ${TEMP_IMAGE}" + sed 's//${CI_COMMIT_AUTHOR}/g' Dockerfile + sed 's/[0-9]\+\.[0-9]\+\.[0-9]\+\(\-\(\w*\.\)[0-9]\+\)\{0,2\}/${DOCKERIZED_VERSION}/g' Dockerfile + docker build -t ${TEMP_IMAGE} . + echo " > Tagging ${TEMP_IMAGE} => ${TARGET_IMAGE}" + docker tag ${TEMP_IMAGE} ${TARGET_IMAGE} + echo " > Sign-In to registry server: ${REGISTRY_SERVER} with username:password ${REGISTRY_USERNAME}:${REGISTRY_PASSWORD}" + echo -n ${REGISTRY_PASSWORD} | base64 -d | docker login --username `echo -n ${REGISTRY_USERNAME} | base64 -d` --password-stdin ${REGISTRY_SERVER} + docker push ${TARGET_IMAGE} # Releasing the binary/output. Usually push the docker image into the registry releasing: From c111b6ecfc511ab58e344e42624c26ac025071c6 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Mon, 17 Oct 2022 10:10:50 +0100 Subject: [PATCH 14/20] Fixing the building script --- .gitlab-ci.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml index b77fa2dd..c3080b32 100644 --- a/.gitlab-ci.yaml +++ b/.gitlab-ci.yaml @@ -66,7 +66,7 @@ prepare: echo " > CI_COMMIT_MESSAGE:" echo ${CI_COMMIT_MESSAGE} echo " > CI_COMMIT_REF_NAME:CI_COMMIT_REF_PROTECTED" - echo "${CI_COMMIT_REF_NAME}:${CI_COMMIT_REF_PROTECTED" + echo "${CI_COMMIT_REF_NAME}:${CI_COMMIT_REF_PROTECTED}" echo " > Detecting the versioning pattern: `version.py getVersionPattern`" echo " > Getting the next version..." export VERSION=`versioning.py getVersion` From 2bb7736c41f2e6dcf325420e92d1b6c76f683df3 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Mon, 17 Oct 2022 10:23:41 +0100 Subject: [PATCH 15/20] Release {version:1.7.4} to hide the passphrase/password when failed login --- .gitlab-ci.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml index c3080b32..f69d144b 100644 --- a/.gitlab-ci.yaml +++ b/.gitlab-ci.yaml @@ -37,9 +37,11 @@ default: # If the tag name is matched with regex: 1.2.3-rc.4+build.5 - if: $CI_COMMIT_TAG =~ /^([0-9]+\.){2}([0-9]+){1}(\-[a-zA-Z\.]*[0-9]+)?(\+[a-zA-Z\.]*[0-9]+)?/ + when: always # If this is protected ref - - if: '$CI_COMMIT_REF_NAME == "true"' + - if: '$CI_COMMIT_REF_PROTECTED == "true"' + when: always # Prepare the versioning prepare: From 796a198228bb0652964e879bcad3a161c5183b9e Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Mon, 17 Oct 2022 11:09:05 +0100 Subject: [PATCH 16/20] Update the building criteria --- .gitlab-ci.yaml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.gitlab-ci.yaml b/.gitlab-ci.yaml index f69d144b..7747d712 100644 --- a/.gitlab-ci.yaml +++ b/.gitlab-ci.yaml @@ -35,12 +35,8 @@ default: rules: - !reference [.default-rules, rules] - # If the tag name is matched with regex: 1.2.3-rc.4+build.5 - - if: $CI_COMMIT_TAG =~ /^([0-9]+\.){2}([0-9]+){1}(\-[a-zA-Z\.]*[0-9]+)?(\+[a-zA-Z\.]*[0-9]+)?/ - when: always - # If this is protected ref - - if: '$CI_COMMIT_REF_PROTECTED == "true"' + - if: '$CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_MESSAGE =~ /\{\s*version:\s*\d+(\.\d){2}(\-\w*\.\d+)?(\+\w*\.\d)?\s*\}/' when: always # Prepare the versioning From bcc5d5134e26a8803de3706cc94ac314937c3a1b Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Mon, 17 Oct 2022 11:34:38 +0100 Subject: [PATCH 17/20] Hide the password/passphrase when invalid for security reason --- webssh/handler.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/webssh/handler.py b/webssh/handler.py index 6e5cbcfa..c19163c7 100644 --- a/webssh/handler.py +++ b/webssh/handler.py @@ -32,6 +32,13 @@ DEFAULT_PORT = 22 +# Config should system hide the password when the password/passphrase invalid. +# When: +# HIDE_PASS == 0, Don't hide the password/passphrase +# HIDE_PASS >= 1, Hide the digits of password/passphrase, e.g.: HIDE_PASS==2, PASSWORD="0123456789abcdefg", then show "01...fg" +# HIDE_PASS <=-1, Don't show the password/passphrase +HIDE_PASS = 2 + swallow_http_errors = True redirecting = None @@ -178,7 +185,12 @@ def get_pkey_obj(self): logging.error(str(self.last_exception)) msg = 'Invalid key' if self.password: - msg += ' or wrong passphrase "{}...{}" for decrypting it.'.format(self.password[0:3], self.password[:-3]) + if HIDE_PASS == 0: + msg += ' or wrong passphrase "{}" for decrypting it.'.format(self.password) + elif HIDE_PASS > 0: + msg += ' or wrong passphrase "{}...{}" for decrypting it.'.format(self.password[0:HIDE_PASS], self.password[HIDE_PASS*-1:]) + else: + msg += ' or wrong passphrase for decrypting it.' raise InvalidValueError(msg) From eb129aaf0e982405bf4d7e01a493ccad2daa0f0e Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Thu, 20 Oct 2022 08:40:54 +0100 Subject: [PATCH 18/20] Fixing the Travis CI building failed --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index d4e47c43..4b1e95a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ paramiko==2.10.4 tornado==5.1.1; python_version < '3.5' tornado==6.1.0; python_version >= '3.5' -PyYAML==6.0 +PyYAML>=5.4.1 #The following package used for testing #pytest From a74b0efba63f36fc6c04fb33384285036662b143 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Thu, 20 Oct 2022 09:49:53 +0100 Subject: [PATCH 19/20] Fixing the FileNotFoundError in python 2.7~3.5 Fixing the TravisCI building error --- .travis.yml | 1 - Dockerfile | 4 +-- requirements.txt | 2 +- webssh/handler.py | 74 ++++++++++++++++++++++++++++------------------ webssh/settings.py | 48 ++++++++++++++++++------------ 5 files changed, 77 insertions(+), 52 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0418b01f..b617d2f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: python python: - "2.7" - - "3.4" - "3.5" - "3.6" - "3.7" diff --git a/Dockerfile b/Dockerfile index cbf7f71e..dba5c9bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,9 @@ LABEL version='0.0.0-dev.0-build.0' ADD . /code WORKDIR /code RUN \ - apk add --no-cache libc-dev libffi-dev gcc && \ + apk add --no-cache libc-dev libffi-dev gcc make openssl-dev && \ pip install -r requirements.txt --no-cache-dir && \ - apk del gcc libc-dev libffi-dev && \ + apk del gcc libc-dev libffi-dev make openssl-dev && \ addgroup webssh && \ adduser -Ss /bin/false -g webssh webssh && \ chown -R webssh:webssh /code diff --git a/requirements.txt b/requirements.txt index 4b1e95a2..e5acc2f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ paramiko==2.10.4 tornado==5.1.1; python_version < '3.5' tornado==6.1.0; python_version >= '3.5' -PyYAML>=5.4.1 +PyYAML>=5.3.1; python_version >= '3.5' #The following package used for testing #pytest diff --git a/webssh/handler.py b/webssh/handler.py index c19163c7..3559d7f7 100644 --- a/webssh/handler.py +++ b/webssh/handler.py @@ -35,7 +35,8 @@ # Config should system hide the password when the password/passphrase invalid. # When: # HIDE_PASS == 0, Don't hide the password/passphrase -# HIDE_PASS >= 1, Hide the digits of password/passphrase, e.g.: HIDE_PASS==2, PASSWORD="0123456789abcdefg", then show "01...fg" +# HIDE_PASS >= 1, Hide the digits of password/passphrase, e.g.: HIDE_PASS==2 +# , PASSWORD="0123456789abcdefg", then show "01...fg" # HIDE_PASS <=-1, Don't show the password/passphrase HIDE_PASS = 2 @@ -186,11 +187,16 @@ def get_pkey_obj(self): msg = 'Invalid key' if self.password: if HIDE_PASS == 0: - msg += ' or wrong passphrase "{}" for decrypting it.'.format(self.password) + msg += ' or wrong passphrase "{}" for decrypting it.'.format( + self.password) elif HIDE_PASS > 0: - msg += ' or wrong passphrase "{}...{}" for decrypting it.'.format(self.password[0:HIDE_PASS], self.password[HIDE_PASS*-1:]) + msg += ' or wrong passphrase "{}...{}" for decrypting it.'\ + .format( + self.password[0:HIDE_PASS], + self.password[HIDE_PASS*-1:] + ) else: - msg += ' or wrong passphrase for decrypting it.' + msg += ' or wrong passphrase for decrypting it.' raise InvalidValueError(msg) @@ -399,31 +405,35 @@ def lookup_hostname(self, hostname, port): ) def get_profile(self): - profiles=self.settings.get('profiles', None) - if profiles: #if the profiles is configurated - value=self.get_argument('profile', None) - if profiles.get('required', False) and len(profiles['profiles'])>0 and not value: - raise InvalidValueError('Argument "profile" is required according to your settings.') - if not (value is None or profiles['profiles'] is None): - return profiles['profiles'][int(value)] + profiles = self.settings.get('profiles', None) + if profiles: # if the profiles is configurated + value = self.get_argument('profile', None) + if profiles.get('required', False) and \ + len(profiles['profiles']) > 0 and \ + not value: + raise InvalidValueError( + 'Argument "profile" is required according to your settings.' + ) + if not (value is None or profiles['profiles'] is None): + return profiles['profiles'][int(value)] return None def get_args(self): - profile=self.get_profile() - if profile is not None and len(profile)>0: - hostname=profile['host'] if 'host' in profile else self.get_hostname() - port=profile['port'] if 'port' in profile else self.get_port() - username=profile['username'] if 'username' in profile else self.get_value('username') - if 'private-key' in profile: - filename='' - privatekey=profile['private-key'] - else: - privatekey, filename = self.get_privatekey() + profile = self.get_profile() + if profile is not None and len(profile) > 0: + hostname = profile.get('host', self.get_hostname()) + port = profile.get('port', self.get_port()) + username = profile.get('username', self.get_value('username')) + if 'private-key' in profile: + filename = '' + privatekey = profile['private-key'] + else: + privatekey, filename = self.get_privatekey() else: - hostname = self.get_hostname() - port = self.get_port() - username = self.get_value('username') - privatekey, filename = self.get_privatekey() + hostname = self.get_hostname() + port = self.get_port() + username = self.get_value('username') + privatekey, filename = self.get_privatekey() password = self.get_argument('password', u'') passphrase = self.get_argument('passphrase', u'') totp = self.get_argument('totp', u'') @@ -520,10 +530,16 @@ def head(self): pass def get(self): - if self.settings.get('profiles') is not None and len(self.settings.get('profiles'))>0: - self.render('profiles.html', profiles=self.settings.get('profiles'), debug=self.debug, font=self.font) - else: - self.render('index.html', debug=self.debug, font=self.font) + profiles = self.settings.get('profiles') + if profiles and len(profiles) > 0: + self.render( + 'profiles.html', + profiles=self.settings.get('profiles'), + debug=self.debug, + font=self.font + ) + else: + self.render('index.html', debug=self.debug, font=self.font) @tornado.gen.coroutine def post(self): diff --git a/webssh/settings.py b/webssh/settings.py index e9304e83..b96ffd34 100644 --- a/webssh/settings.py +++ b/webssh/settings.py @@ -16,6 +16,11 @@ ) from webssh._version import __version__ +try: + FileNotFoundError +except NameError: + FileNotFoundError = IOError + def print_version(flag): if flag: @@ -76,27 +81,31 @@ def get_family(self, filename): def get_url(self, filename, dirs): return os.path.join(*(dirs + [filename])) + def get_profiles(): - filename=os.getenv('PROFILES', None) + filename = os.getenv('PROFILES', None) if filename: - if not filename.startswith(os.sep): filename=os.path.join(os.path.abspath(os.sep), filename) - try: - if not os.path.exists(filename): raise FileNotFoundError() - with open(filename, 'r') as fp: - result=yaml.load(fp, Loader=SafeLoader) - if result: - idx=0 - for p in result['profiles']: - p['index']=idx - idx+=1 - result['required']=bool(result.get('required', 'False')) - return result - except FileNotFoundError: - logging.warning('Cannot found file profiles: {0}'.format(filename)) - except: - logging.warning('Unexpected error', exc_info=True) + if not filename.startswith(os.sep): + filename = os.path.join(os.path.abspath(os.sep), filename) + try: + if not os.path.exists(filename): + raise FileNotFoundError() + with open(filename, 'r') as fp: + result = yaml.load(fp, Loader=SafeLoader) + if result: + idx = 0 + for p in result['profiles']: + p['index'] = idx + idx += 1 + result['required'] = bool(result.get('required', 'False')) + return result + except FileNotFoundError: + logging.warning('Cannot found file profiles: {0}'.format(filename)) + except Exception: + logging.warning('Unexpected error', exc_info=True) return None + def get_app_settings(options): settings = dict( template_path=os.path.join(base_dir, 'webssh', 'templates'), @@ -111,8 +120,9 @@ def get_app_settings(options): ), origin_policy=get_origin_setting(options) ) - settings['profiles']=get_profiles() - if not settings['profiles']: del settings['profiles'] + settings['profiles'] = get_profiles() + if not settings['profiles']: + del settings['profiles'] return settings From 57fa41fdb966b8a02dae1e75eae29bc0c8ca1b81 Mon Sep 17 00:00:00 2001 From: Kenson Man Date: Thu, 20 Oct 2022 09:49:53 +0100 Subject: [PATCH 20/20] Fixing the FileNotFoundError in python 2.7~3.5 Fixing the TravisCI building error --- .travis.yml | 1 - Dockerfile | 4 +-- requirements.txt | 3 +- webssh/handler.py | 74 ++++++++++++++++++++++++++++------------------ webssh/settings.py | 48 ++++++++++++++++++------------ 5 files changed, 78 insertions(+), 52 deletions(-) diff --git a/.travis.yml b/.travis.yml index 0418b01f..b617d2f0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,6 @@ language: python python: - "2.7" - - "3.4" - "3.5" - "3.6" - "3.7" diff --git a/Dockerfile b/Dockerfile index cbf7f71e..dba5c9bb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,9 +6,9 @@ LABEL version='0.0.0-dev.0-build.0' ADD . /code WORKDIR /code RUN \ - apk add --no-cache libc-dev libffi-dev gcc && \ + apk add --no-cache libc-dev libffi-dev gcc make openssl-dev && \ pip install -r requirements.txt --no-cache-dir && \ - apk del gcc libc-dev libffi-dev && \ + apk del gcc libc-dev libffi-dev make openssl-dev && \ addgroup webssh && \ adduser -Ss /bin/false -g webssh webssh && \ chown -R webssh:webssh /code diff --git a/requirements.txt b/requirements.txt index 4b1e95a2..6d0e8d68 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,8 @@ paramiko==2.10.4 tornado==5.1.1; python_version < '3.5' tornado==6.1.0; python_version >= '3.5' -PyYAML>=5.4.1 +PyYAML>=5.3.1; python_version == '2.7' +PyYAML>=5.3.1; python_version >= '3.5' #The following package used for testing #pytest diff --git a/webssh/handler.py b/webssh/handler.py index c19163c7..3559d7f7 100644 --- a/webssh/handler.py +++ b/webssh/handler.py @@ -35,7 +35,8 @@ # Config should system hide the password when the password/passphrase invalid. # When: # HIDE_PASS == 0, Don't hide the password/passphrase -# HIDE_PASS >= 1, Hide the digits of password/passphrase, e.g.: HIDE_PASS==2, PASSWORD="0123456789abcdefg", then show "01...fg" +# HIDE_PASS >= 1, Hide the digits of password/passphrase, e.g.: HIDE_PASS==2 +# , PASSWORD="0123456789abcdefg", then show "01...fg" # HIDE_PASS <=-1, Don't show the password/passphrase HIDE_PASS = 2 @@ -186,11 +187,16 @@ def get_pkey_obj(self): msg = 'Invalid key' if self.password: if HIDE_PASS == 0: - msg += ' or wrong passphrase "{}" for decrypting it.'.format(self.password) + msg += ' or wrong passphrase "{}" for decrypting it.'.format( + self.password) elif HIDE_PASS > 0: - msg += ' or wrong passphrase "{}...{}" for decrypting it.'.format(self.password[0:HIDE_PASS], self.password[HIDE_PASS*-1:]) + msg += ' or wrong passphrase "{}...{}" for decrypting it.'\ + .format( + self.password[0:HIDE_PASS], + self.password[HIDE_PASS*-1:] + ) else: - msg += ' or wrong passphrase for decrypting it.' + msg += ' or wrong passphrase for decrypting it.' raise InvalidValueError(msg) @@ -399,31 +405,35 @@ def lookup_hostname(self, hostname, port): ) def get_profile(self): - profiles=self.settings.get('profiles', None) - if profiles: #if the profiles is configurated - value=self.get_argument('profile', None) - if profiles.get('required', False) and len(profiles['profiles'])>0 and not value: - raise InvalidValueError('Argument "profile" is required according to your settings.') - if not (value is None or profiles['profiles'] is None): - return profiles['profiles'][int(value)] + profiles = self.settings.get('profiles', None) + if profiles: # if the profiles is configurated + value = self.get_argument('profile', None) + if profiles.get('required', False) and \ + len(profiles['profiles']) > 0 and \ + not value: + raise InvalidValueError( + 'Argument "profile" is required according to your settings.' + ) + if not (value is None or profiles['profiles'] is None): + return profiles['profiles'][int(value)] return None def get_args(self): - profile=self.get_profile() - if profile is not None and len(profile)>0: - hostname=profile['host'] if 'host' in profile else self.get_hostname() - port=profile['port'] if 'port' in profile else self.get_port() - username=profile['username'] if 'username' in profile else self.get_value('username') - if 'private-key' in profile: - filename='' - privatekey=profile['private-key'] - else: - privatekey, filename = self.get_privatekey() + profile = self.get_profile() + if profile is not None and len(profile) > 0: + hostname = profile.get('host', self.get_hostname()) + port = profile.get('port', self.get_port()) + username = profile.get('username', self.get_value('username')) + if 'private-key' in profile: + filename = '' + privatekey = profile['private-key'] + else: + privatekey, filename = self.get_privatekey() else: - hostname = self.get_hostname() - port = self.get_port() - username = self.get_value('username') - privatekey, filename = self.get_privatekey() + hostname = self.get_hostname() + port = self.get_port() + username = self.get_value('username') + privatekey, filename = self.get_privatekey() password = self.get_argument('password', u'') passphrase = self.get_argument('passphrase', u'') totp = self.get_argument('totp', u'') @@ -520,10 +530,16 @@ def head(self): pass def get(self): - if self.settings.get('profiles') is not None and len(self.settings.get('profiles'))>0: - self.render('profiles.html', profiles=self.settings.get('profiles'), debug=self.debug, font=self.font) - else: - self.render('index.html', debug=self.debug, font=self.font) + profiles = self.settings.get('profiles') + if profiles and len(profiles) > 0: + self.render( + 'profiles.html', + profiles=self.settings.get('profiles'), + debug=self.debug, + font=self.font + ) + else: + self.render('index.html', debug=self.debug, font=self.font) @tornado.gen.coroutine def post(self): diff --git a/webssh/settings.py b/webssh/settings.py index e9304e83..b96ffd34 100644 --- a/webssh/settings.py +++ b/webssh/settings.py @@ -16,6 +16,11 @@ ) from webssh._version import __version__ +try: + FileNotFoundError +except NameError: + FileNotFoundError = IOError + def print_version(flag): if flag: @@ -76,27 +81,31 @@ def get_family(self, filename): def get_url(self, filename, dirs): return os.path.join(*(dirs + [filename])) + def get_profiles(): - filename=os.getenv('PROFILES', None) + filename = os.getenv('PROFILES', None) if filename: - if not filename.startswith(os.sep): filename=os.path.join(os.path.abspath(os.sep), filename) - try: - if not os.path.exists(filename): raise FileNotFoundError() - with open(filename, 'r') as fp: - result=yaml.load(fp, Loader=SafeLoader) - if result: - idx=0 - for p in result['profiles']: - p['index']=idx - idx+=1 - result['required']=bool(result.get('required', 'False')) - return result - except FileNotFoundError: - logging.warning('Cannot found file profiles: {0}'.format(filename)) - except: - logging.warning('Unexpected error', exc_info=True) + if not filename.startswith(os.sep): + filename = os.path.join(os.path.abspath(os.sep), filename) + try: + if not os.path.exists(filename): + raise FileNotFoundError() + with open(filename, 'r') as fp: + result = yaml.load(fp, Loader=SafeLoader) + if result: + idx = 0 + for p in result['profiles']: + p['index'] = idx + idx += 1 + result['required'] = bool(result.get('required', 'False')) + return result + except FileNotFoundError: + logging.warning('Cannot found file profiles: {0}'.format(filename)) + except Exception: + logging.warning('Unexpected error', exc_info=True) return None + def get_app_settings(options): settings = dict( template_path=os.path.join(base_dir, 'webssh', 'templates'), @@ -111,8 +120,9 @@ def get_app_settings(options): ), origin_policy=get_origin_setting(options) ) - settings['profiles']=get_profiles() - if not settings['profiles']: del settings['profiles'] + settings['profiles'] = get_profiles() + if not settings['profiles']: + del settings['profiles'] return settings