From f5369b7ab9b94ca78fd398cc3cf4305e229e7148 Mon Sep 17 00:00:00 2001 From: jocayn <1579649885@qq.com> Date: Sat, 13 Jun 2026 13:10:16 +0800 Subject: [PATCH] Save SSH passwords with connections --- README.md | 4 ++-- README.rst | 5 +++-- tests/test_handler.py | 27 +++++++++++++++++++++++++++ tests/test_storage.py | 5 ++++- webssh/handler.py | 1 + webssh/static/css/app.css | 35 +++++++++++++++++++++++++++++++++-- webssh/static/js/main.js | 34 +++++++++++++++++++++------------- webssh/storage.py | 5 ++++- webssh/templates/index.html | 8 +++++++- 9 files changed, 102 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 5a17e5a..a441217 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/README.rst b/README.rst index d6a3ccf..d8e71ef 100644 --- a/README.rst +++ b/README.rst @@ -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. diff --git a/tests/test_handler.py b/tests/test_handler.py index 5eabfa5..d90eef3 100644 --- a/tests/test_handler.py +++ b/tests/test_handler.py @@ -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) diff --git a/tests/test_storage.py b/tests/test_storage.py index bc255ee..e3336ff 100644 --- a/tests/test_storage.py +++ b/tests/test_storage.py @@ -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')) - diff --git a/webssh/handler.py b/webssh/handler.py index 89db9ec..158013b 100644 --- a/webssh/handler.py +++ b/webssh/handler.py @@ -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' } diff --git a/webssh/static/css/app.css b/webssh/static/css/app.css index 4d17c6b..ec9d5e6 100644 --- a/webssh/static/css/app.css +++ b/webssh/static/css/app.css @@ -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; diff --git a/webssh/static/js/main.js b/webssh/static/js/main.js index df29786..5d40e9c 100644 --- a/webssh/static/js/main.js +++ b/webssh/static/js/main.js @@ -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) { diff --git a/webssh/storage.py b/webssh/storage.py index bdbd996..9910f24 100644 --- a/webssh/storage.py +++ b/webssh/storage.py @@ -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 - diff --git a/webssh/templates/index.html b/webssh/templates/index.html index 3c72ccd..861200a 100644 --- a/webssh/templates/index.html +++ b/webssh/templates/index.html @@ -81,7 +81,13 @@ name="username" value="" required>
- +
+ + +