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
|
||||
hostname, port, username and terminal type for quick reconnects. It does not
|
||||
persist ssh passwords, private keys, key passphrases or TOTP codes.
|
||||
hostname, port, username, ssh password and terminal type for quick reconnects.
|
||||
It does not persist private keys, key passphrases or TOTP codes.
|
||||
|
||||
Use `--auth=false` only for a trusted private deployment where another layer
|
||||
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
|
||||
|
||||
Saved connections are stored under the data directory too. WebSSH saves
|
||||
hostname, port, username and terminal type for quick reconnects. It does
|
||||
not persist ssh passwords, private keys, key passphrases or TOTP codes.
|
||||
hostname, port, username, ssh password and terminal type for quick
|
||||
reconnects. It does not persist private keys, key passphrases or TOTP
|
||||
codes.
|
||||
|
||||
Use ``--auth=false`` only for a trusted private deployment where another
|
||||
layer already protects access to WebSSH.
|
||||
|
||||
@@ -318,6 +318,33 @@ class TestWsockHandler(unittest.TestCase):
|
||||
obj.close.assert_called_with(reason='Worker closed')
|
||||
|
||||
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):
|
||||
handler = Mock(spec=IndexHandler)
|
||||
|
||||
|
||||
@@ -21,11 +21,13 @@ class TestConnectionStore(unittest.TestCase):
|
||||
'hostname': '127.0.0.1',
|
||||
'port': 22,
|
||||
'username': 'root',
|
||||
'password': 'root-secret',
|
||||
'term': 'xterm-256color',
|
||||
'auth_type': 'password'
|
||||
})
|
||||
|
||||
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([], self.store.list('other'))
|
||||
|
||||
@@ -33,6 +35,7 @@ class TestConnectionStore(unittest.TestCase):
|
||||
'hostname': '127.0.0.1',
|
||||
'port': 22,
|
||||
'username': 'root',
|
||||
'password': 'new-secret',
|
||||
'term': 'xterm',
|
||||
'auth_type': 'privatekey'
|
||||
})
|
||||
@@ -40,7 +43,7 @@ class TestConnectionStore(unittest.TestCase):
|
||||
self.assertEqual(profile['id'], updated['id'])
|
||||
self.assertEqual(1, len(self.store.list('admin')))
|
||||
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.assertFalse(self.store.delete('admin', profile['id']))
|
||||
self.assertEqual([], self.store.list('admin'))
|
||||
|
||||
|
||||
@@ -551,6 +551,7 @@ class IndexHandler(MixinHandler, tornado.web.RequestHandler):
|
||||
'hostname': hostname,
|
||||
'port': port,
|
||||
'username': username,
|
||||
'password': password,
|
||||
'term': term,
|
||||
'auth_type': 'privatekey' if privatekey else 'password'
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
--danger: #d65a4a;
|
||||
--danger-soft: #fff1ee;
|
||||
--shadow: 0 18px 48px rgba(21, 58, 58, 0.09);
|
||||
--terminal-safe-bottom: 52px;
|
||||
}
|
||||
|
||||
* {
|
||||
@@ -153,6 +152,35 @@ a:hover {
|
||||
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 {
|
||||
min-height: 44px;
|
||||
color: var(--text);
|
||||
@@ -284,7 +312,10 @@ input[type="file"].form-control {
|
||||
|
||||
#terminal.terminal-fullscreen {
|
||||
position: fixed;
|
||||
inset: 0 0 var(--terminal-safe-bottom) 0;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: 255;
|
||||
overflow: hidden;
|
||||
background: #0f1720;
|
||||
|
||||
+21
-13
@@ -43,6 +43,7 @@ jQuery(function($){
|
||||
saved_connections_select = $('#saved-connections'),
|
||||
load_connection_button = $('#load-connection'),
|
||||
delete_connection_button = $('#delete-connection'),
|
||||
show_password_checkbox = $('#show-password'),
|
||||
term_type = $('#term'),
|
||||
style = {},
|
||||
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() {
|
||||
var profile = selected_connection();
|
||||
if (!profile) {
|
||||
@@ -189,7 +198,7 @@ jQuery(function($){
|
||||
$('#hostname').val(profile.hostname);
|
||||
$('#port').val(profile.port);
|
||||
$('#username').val(profile.username);
|
||||
$('#password').val('');
|
||||
$('#password').val(profile.password || '');
|
||||
$('#privatekey').val('');
|
||||
$('#passphrase').val('');
|
||||
$('#totp').val('');
|
||||
@@ -301,20 +310,11 @@ jQuery(function($){
|
||||
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() {
|
||||
var terminal = document.getElementById('terminal'),
|
||||
rect = terminal.getBoundingClientRect(),
|
||||
width = rect.width || window.innerWidth,
|
||||
height = rect.height || (window.innerHeight - terminal_safe_bottom());
|
||||
height = rect.height || window.innerHeight;
|
||||
|
||||
return {'width': width, 'height': height};
|
||||
}
|
||||
@@ -330,8 +330,8 @@ jQuery(function($){
|
||||
}
|
||||
|
||||
var size = terminal_size(),
|
||||
cols = Math.max(2, parseInt(size.width / style.width, 10) - 1),
|
||||
rows = Math.max(1, parseInt(size.height / style.height, 10) - 1);
|
||||
cols = Math.max(2, parseInt(size.width / style.width, 10)),
|
||||
rows = Math.max(1, parseInt(size.height / style.height, 10));
|
||||
return {'cols': cols, 'rows': rows};
|
||||
}
|
||||
|
||||
@@ -995,6 +995,10 @@ jQuery(function($){
|
||||
connect();
|
||||
});
|
||||
|
||||
$(form_id).on('reset', function() {
|
||||
window.setTimeout(update_password_visibility, 0);
|
||||
});
|
||||
|
||||
load_connection_button.click(function() {
|
||||
apply_saved_connection();
|
||||
});
|
||||
@@ -1007,6 +1011,10 @@ jQuery(function($){
|
||||
delete_saved_connection();
|
||||
});
|
||||
|
||||
show_password_checkbox.change(function() {
|
||||
update_password_visibility();
|
||||
});
|
||||
|
||||
|
||||
function cross_origin_connect(event)
|
||||
{
|
||||
|
||||
+4
-1
@@ -75,6 +75,9 @@ class ConnectionStore(object):
|
||||
term = (connection.get('term') or 'xterm-256color').strip()
|
||||
port = connection.get('port') or DEFAULT_PORT
|
||||
port = int(port)
|
||||
password = connection.get('password')
|
||||
if password is None:
|
||||
password = ''
|
||||
title = '{}@{}:{}'.format(username, hostname, port)
|
||||
auth_type = connection.get('auth_type') or 'password'
|
||||
|
||||
@@ -86,6 +89,7 @@ class ConnectionStore(object):
|
||||
'hostname': hostname,
|
||||
'port': port,
|
||||
'username': username,
|
||||
'password': password,
|
||||
'term': term,
|
||||
'auth_type': auth_type
|
||||
}
|
||||
@@ -129,4 +133,3 @@ def _chmod_private(path):
|
||||
os.chmod(path, 0o600)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
@@ -81,7 +81,13 @@
|
||||
name="username" value="" required>
|
||||
</div>
|
||||
<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"
|
||||
name="password" value="">
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user