Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hide pass #296

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0377594
Change the base image from python:3.7-slim to python:3-alpine
May 25, 2022
e40a6e9
Create the gitlab ci support
May 25, 2022
726ef31
Merging the alpine branch.
May 27, 2022
decc471
Update the gitlab ci flow for main branch\
May 27, 2022
4f1f1c4
Merging with main
May 27, 2022
3d60d48
Add the test-cases.
May 27, 2022
91454cc
Update the UI, test-cases and README.md.
May 30, 2022
05f98af
Merge the parent's master branch.
Jun 6, 2022
0f40d52
Merge branch 'profiling' into main.
Jun 6, 2022
3093298
Empty commit to trigger the CICD.
Jun 6, 2022
af99fea
Hide the server connection setting for security reason.
Jun 7, 2022
766d755
Merge branch 'profiling' into main
Jun 7, 2022
903f586
Merge remote-tracking branch 'github/master' into profiling
Jun 10, 2022
bd7ba27
Set the cookies to 30days instead of default expiry (current session …
Jul 12, 2022
cc0fc3c
Merge branch 'profiling' into 'main'
kensonman Jul 12, 2022
3dfd712
Update the release script
Jul 12, 2022
fc0d7b1
Fixing the expires
Jul 12, 2022
5c32635
Merge branch 'profiling' into main
Jul 12, 2022
00aea9d
Update the 1.7.3
Jul 12, 2022
f72b253
Do not show the clear passphrase for security reason
Oct 4, 2022
6056fb3
Update the building script
Oct 17, 2022
c111b6e
Fixing the building script
Oct 17, 2022
2bb7736
Release {version:1.7.4} to hide the passphrase/password when failed l…
Oct 17, 2022
796a198
Update the building criteria
Oct 17, 2022
bcc5d51
Hide the password/passphrase when invalid for security reason
Oct 17, 2022
eb129aa
Fixing the Travis CI building failed
Oct 20, 2022
a74b0ef
Fixing the FileNotFoundError in python 2.7~3.5
Oct 20, 2022
57fa41f
Fixing the FileNotFoundError in python 2.7~3.5
Oct 20, 2022
4e029fb
Merging
Oct 20, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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

7 changes: 7 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions tests/data/profiles-sample.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
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: robey

- name: sample2
description: "Long description"
host: localhost
port: 22
#optional, if specified, the username field should not be shown on the template
username: robey
#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-----
7 changes: 7 additions & 0 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -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):

99 changes: 99 additions & 0 deletions tests/test_profiles.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
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

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']
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'

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()
del os.environ['PROFILES']

class _TestBasic_(object):
running = [True]
sshserver_port = 2200
body = 'hostname={host}&port={port}&profile={profile}&username={username}&password={password}'
headers = {'Cookie': '_xsrf=yummy'}

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

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'

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('<!-- PROFILES -->')>=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_()

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('<!-- PROFILES -->')
assert False, 'Expected the origin "index.html" but "profiles.html"'
34 changes: 29 additions & 5 deletions webssh/handler.py
Original file line number Diff line number Diff line change
@@ -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):
26 changes: 26 additions & 0 deletions webssh/settings.py
Original file line number Diff line number Diff line change
@@ -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
@@ -72,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(
@@ -87,6 +111,8 @@ def get_app_settings(options):
),
origin_policy=get_origin_setting(options)
)
settings['profiles']=get_profiles()
if not settings['profiles']: del settings['profiles']
return settings


Loading