Save SSH passwords with connections
This commit is contained in:
@@ -63,8 +63,8 @@ WEBSSH_AUTH_USERNAME=admin WEBSSH_AUTH_PASSWORD='strong-password' wssh
|
|||||||
```
|
```
|
||||||
|
|
||||||
Saved connections are stored under the data directory too. WebSSH saves
|
Saved connections are stored under the data directory too. WebSSH saves
|
||||||
hostname, port, username and terminal type for quick reconnects. It does not
|
hostname, port, username, ssh password and terminal type for quick reconnects.
|
||||||
persist ssh passwords, private keys, key passphrases or TOTP codes.
|
It does not persist private keys, key passphrases or TOTP codes.
|
||||||
|
|
||||||
Use `--auth=false` only for a trusted private deployment where another layer
|
Use `--auth=false` only for a trusted private deployment where another layer
|
||||||
already protects access to WebSSH.
|
already protects access to WebSSH.
|
||||||
|
|||||||
+3
-2
@@ -67,8 +67,9 @@ You can also provide credentials with environment variables:
|
|||||||
WEBSSH_AUTH_USERNAME=admin WEBSSH_AUTH_PASSWORD='strong-password' wssh
|
WEBSSH_AUTH_USERNAME=admin WEBSSH_AUTH_PASSWORD='strong-password' wssh
|
||||||
|
|
||||||
Saved connections are stored under the data directory too. WebSSH saves
|
Saved connections are stored under the data directory too. WebSSH saves
|
||||||
hostname, port, username and terminal type for quick reconnects. It does
|
hostname, port, username, ssh password and terminal type for quick
|
||||||
not persist ssh passwords, private keys, key passphrases or TOTP codes.
|
reconnects. It does not persist private keys, key passphrases or TOTP
|
||||||
|
codes.
|
||||||
|
|
||||||
Use ``--auth=false`` only for a trusted private deployment where another
|
Use ``--auth=false`` only for a trusted private deployment where another
|
||||||
layer already protects access to WebSSH.
|
layer already protects access to WebSSH.
|
||||||
|
|||||||
@@ -318,6 +318,33 @@ class TestWsockHandler(unittest.TestCase):
|
|||||||
obj.close.assert_called_with(reason='Worker closed')
|
obj.close.assert_called_with(reason='Worker closed')
|
||||||
|
|
||||||
class TestIndexHandler(unittest.TestCase):
|
class TestIndexHandler(unittest.TestCase):
|
||||||
|
def test_get_args_keeps_password_for_saved_connection(self):
|
||||||
|
obj = Mock(spec=IndexHandler)
|
||||||
|
obj.get_hostname.return_value = '127.0.0.1'
|
||||||
|
obj.get_port.return_value = 22
|
||||||
|
obj.get_value.return_value = 'root'
|
||||||
|
obj.get_privatekey.return_value = ('', '')
|
||||||
|
obj.policy = paramiko.WarningPolicy()
|
||||||
|
obj.ssh_client = Mock()
|
||||||
|
|
||||||
|
values = {
|
||||||
|
'password': 'root-secret',
|
||||||
|
'passphrase': '',
|
||||||
|
'totp': '',
|
||||||
|
'term': 'xterm-256color'
|
||||||
|
}
|
||||||
|
obj.get_argument.side_effect = lambda name, default=u'': values.get(
|
||||||
|
name, default
|
||||||
|
)
|
||||||
|
|
||||||
|
args = IndexHandler.get_args(obj)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
('127.0.0.1', 22, 'root', 'root-secret'),
|
||||||
|
args[:4]
|
||||||
|
)
|
||||||
|
self.assertEqual('root-secret', obj.connection_info['password'])
|
||||||
|
|
||||||
def test_null_in_encoding(self):
|
def test_null_in_encoding(self):
|
||||||
handler = Mock(spec=IndexHandler)
|
handler = Mock(spec=IndexHandler)
|
||||||
|
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ class TestConnectionStore(unittest.TestCase):
|
|||||||
'hostname': '127.0.0.1',
|
'hostname': '127.0.0.1',
|
||||||
'port': 22,
|
'port': 22,
|
||||||
'username': 'root',
|
'username': 'root',
|
||||||
|
'password': 'root-secret',
|
||||||
'term': 'xterm-256color',
|
'term': 'xterm-256color',
|
||||||
'auth_type': 'password'
|
'auth_type': 'password'
|
||||||
})
|
})
|
||||||
|
|
||||||
self.assertEqual(profile['title'], 'root@127.0.0.1:22')
|
self.assertEqual(profile['title'], 'root@127.0.0.1:22')
|
||||||
|
self.assertEqual(profile['password'], 'root-secret')
|
||||||
self.assertEqual([profile], self.store.list('admin'))
|
self.assertEqual([profile], self.store.list('admin'))
|
||||||
self.assertEqual([], self.store.list('other'))
|
self.assertEqual([], self.store.list('other'))
|
||||||
|
|
||||||
@@ -33,6 +35,7 @@ class TestConnectionStore(unittest.TestCase):
|
|||||||
'hostname': '127.0.0.1',
|
'hostname': '127.0.0.1',
|
||||||
'port': 22,
|
'port': 22,
|
||||||
'username': 'root',
|
'username': 'root',
|
||||||
|
'password': 'new-secret',
|
||||||
'term': 'xterm',
|
'term': 'xterm',
|
||||||
'auth_type': 'privatekey'
|
'auth_type': 'privatekey'
|
||||||
})
|
})
|
||||||
@@ -40,7 +43,7 @@ class TestConnectionStore(unittest.TestCase):
|
|||||||
self.assertEqual(profile['id'], updated['id'])
|
self.assertEqual(profile['id'], updated['id'])
|
||||||
self.assertEqual(1, len(self.store.list('admin')))
|
self.assertEqual(1, len(self.store.list('admin')))
|
||||||
self.assertEqual('xterm', self.store.list('admin')[0]['term'])
|
self.assertEqual('xterm', self.store.list('admin')[0]['term'])
|
||||||
|
self.assertEqual('new-secret', self.store.list('admin')[0]['password'])
|
||||||
self.assertTrue(self.store.delete('admin', profile['id']))
|
self.assertTrue(self.store.delete('admin', profile['id']))
|
||||||
self.assertFalse(self.store.delete('admin', profile['id']))
|
self.assertFalse(self.store.delete('admin', profile['id']))
|
||||||
self.assertEqual([], self.store.list('admin'))
|
self.assertEqual([], self.store.list('admin'))
|
||||||
|
|
||||||
|
|||||||
@@ -551,6 +551,7 @@ class IndexHandler(MixinHandler, tornado.web.RequestHandler):
|
|||||||
'hostname': hostname,
|
'hostname': hostname,
|
||||||
'port': port,
|
'port': port,
|
||||||
'username': username,
|
'username': username,
|
||||||
|
'password': password,
|
||||||
'term': term,
|
'term': term,
|
||||||
'auth_type': 'privatekey' if privatekey else 'password'
|
'auth_type': 'privatekey' if privatekey else 'password'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,6 @@
|
|||||||
--danger: #d65a4a;
|
--danger: #d65a4a;
|
||||||
--danger-soft: #fff1ee;
|
--danger-soft: #fff1ee;
|
||||||
--shadow: 0 18px 48px rgba(21, 58, 58, 0.09);
|
--shadow: 0 18px 48px rgba(21, 58, 58, 0.09);
|
||||||
--terminal-safe-bottom: 52px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
@@ -153,6 +152,35 @@ a:hover {
|
|||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.field-heading {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field-heading label {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-toggle {
|
||||||
|
display: inline-flex !important;
|
||||||
|
gap: 6px;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--muted) !important;
|
||||||
|
font-size: 12px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inline-toggle input {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
accent-color: var(--brand);
|
||||||
|
}
|
||||||
|
|
||||||
.form-control {
|
.form-control {
|
||||||
min-height: 44px;
|
min-height: 44px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -284,7 +312,10 @@ input[type="file"].form-control {
|
|||||||
|
|
||||||
#terminal.terminal-fullscreen {
|
#terminal.terminal-fullscreen {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0 0 var(--terminal-safe-bottom) 0;
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
z-index: 255;
|
z-index: 255;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #0f1720;
|
background: #0f1720;
|
||||||
|
|||||||
+21
-13
@@ -43,6 +43,7 @@ jQuery(function($){
|
|||||||
saved_connections_select = $('#saved-connections'),
|
saved_connections_select = $('#saved-connections'),
|
||||||
load_connection_button = $('#load-connection'),
|
load_connection_button = $('#load-connection'),
|
||||||
delete_connection_button = $('#delete-connection'),
|
delete_connection_button = $('#delete-connection'),
|
||||||
|
show_password_checkbox = $('#show-password'),
|
||||||
term_type = $('#term'),
|
term_type = $('#term'),
|
||||||
style = {},
|
style = {},
|
||||||
default_title = 'WebSSH',
|
default_title = 'WebSSH',
|
||||||
@@ -180,6 +181,14 @@ jQuery(function($){
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function update_password_visibility() {
|
||||||
|
$('#password').attr(
|
||||||
|
'type',
|
||||||
|
show_password_checkbox.prop('checked') ? 'text' : 'password'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
function apply_saved_connection() {
|
function apply_saved_connection() {
|
||||||
var profile = selected_connection();
|
var profile = selected_connection();
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
@@ -189,7 +198,7 @@ jQuery(function($){
|
|||||||
$('#hostname').val(profile.hostname);
|
$('#hostname').val(profile.hostname);
|
||||||
$('#port').val(profile.port);
|
$('#port').val(profile.port);
|
||||||
$('#username').val(profile.username);
|
$('#username').val(profile.username);
|
||||||
$('#password').val('');
|
$('#password').val(profile.password || '');
|
||||||
$('#privatekey').val('');
|
$('#privatekey').val('');
|
||||||
$('#passphrase').val('');
|
$('#passphrase').val('');
|
||||||
$('#totp').val('');
|
$('#totp').val('');
|
||||||
@@ -301,20 +310,11 @@ jQuery(function($){
|
|||||||
resize_terminal(term);
|
resize_terminal(term);
|
||||||
}
|
}
|
||||||
|
|
||||||
function terminal_safe_bottom() {
|
|
||||||
var value = window.getComputedStyle(document.documentElement)
|
|
||||||
.getPropertyValue('--terminal-safe-bottom'),
|
|
||||||
parsed = window.parseInt(value, 10);
|
|
||||||
|
|
||||||
return parsed > 0 ? parsed : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function terminal_size() {
|
function terminal_size() {
|
||||||
var terminal = document.getElementById('terminal'),
|
var terminal = document.getElementById('terminal'),
|
||||||
rect = terminal.getBoundingClientRect(),
|
rect = terminal.getBoundingClientRect(),
|
||||||
width = rect.width || window.innerWidth,
|
width = rect.width || window.innerWidth,
|
||||||
height = rect.height || (window.innerHeight - terminal_safe_bottom());
|
height = rect.height || window.innerHeight;
|
||||||
|
|
||||||
return {'width': width, 'height': height};
|
return {'width': width, 'height': height};
|
||||||
}
|
}
|
||||||
@@ -330,8 +330,8 @@ jQuery(function($){
|
|||||||
}
|
}
|
||||||
|
|
||||||
var size = terminal_size(),
|
var size = terminal_size(),
|
||||||
cols = Math.max(2, parseInt(size.width / style.width, 10) - 1),
|
cols = Math.max(2, parseInt(size.width / style.width, 10)),
|
||||||
rows = Math.max(1, parseInt(size.height / style.height, 10) - 1);
|
rows = Math.max(1, parseInt(size.height / style.height, 10));
|
||||||
return {'cols': cols, 'rows': rows};
|
return {'cols': cols, 'rows': rows};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -995,6 +995,10 @@ jQuery(function($){
|
|||||||
connect();
|
connect();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(form_id).on('reset', function() {
|
||||||
|
window.setTimeout(update_password_visibility, 0);
|
||||||
|
});
|
||||||
|
|
||||||
load_connection_button.click(function() {
|
load_connection_button.click(function() {
|
||||||
apply_saved_connection();
|
apply_saved_connection();
|
||||||
});
|
});
|
||||||
@@ -1007,6 +1011,10 @@ jQuery(function($){
|
|||||||
delete_saved_connection();
|
delete_saved_connection();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
show_password_checkbox.change(function() {
|
||||||
|
update_password_visibility();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
function cross_origin_connect(event)
|
function cross_origin_connect(event)
|
||||||
{
|
{
|
||||||
|
|||||||
+4
-1
@@ -75,6 +75,9 @@ class ConnectionStore(object):
|
|||||||
term = (connection.get('term') or 'xterm-256color').strip()
|
term = (connection.get('term') or 'xterm-256color').strip()
|
||||||
port = connection.get('port') or DEFAULT_PORT
|
port = connection.get('port') or DEFAULT_PORT
|
||||||
port = int(port)
|
port = int(port)
|
||||||
|
password = connection.get('password')
|
||||||
|
if password is None:
|
||||||
|
password = ''
|
||||||
title = '{}@{}:{}'.format(username, hostname, port)
|
title = '{}@{}:{}'.format(username, hostname, port)
|
||||||
auth_type = connection.get('auth_type') or 'password'
|
auth_type = connection.get('auth_type') or 'password'
|
||||||
|
|
||||||
@@ -86,6 +89,7 @@ class ConnectionStore(object):
|
|||||||
'hostname': hostname,
|
'hostname': hostname,
|
||||||
'port': port,
|
'port': port,
|
||||||
'username': username,
|
'username': username,
|
||||||
|
'password': password,
|
||||||
'term': term,
|
'term': term,
|
||||||
'auth_type': auth_type
|
'auth_type': auth_type
|
||||||
}
|
}
|
||||||
@@ -129,4 +133,3 @@ def _chmod_private(path):
|
|||||||
os.chmod(path, 0o600)
|
os.chmod(path, 0o600)
|
||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@@ -81,7 +81,13 @@
|
|||||||
name="username" value="" required>
|
name="username" value="" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="field">
|
<div class="field">
|
||||||
<label for="password">SSH 密码</label>
|
<div class="field-heading">
|
||||||
|
<label for="password">SSH 密码</label>
|
||||||
|
<label class="inline-toggle" for="show-password">
|
||||||
|
<input type="checkbox" id="show-password">
|
||||||
|
<span>显示</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
<input class="form-control" type="password" id="password"
|
<input class="form-control" type="password" id="password"
|
||||||
name="password" value="">
|
name="password" value="">
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user