From 0f42cb3c6d1c5d7f788590613478dd1e53ae0035 Mon Sep 17 00:00:00 2001 From: Jocay <1579649885@qq.com> Date: Thu, 13 Aug 2026 00:20:10 +0800 Subject: [PATCH] fix: improve terminal clipboard interactions --- AGENTS.md | 70 ++++++++++++++++++++++++++++++++++++++++ webssh/static/js/main.js | 53 +++++++++++++++++++++--------- 2 files changed, 107 insertions(+), 16 deletions(-) create mode 100644 AGENTS.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..a0ef898 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,70 @@ +# Repository guidance + +## Project overview + +WebSSH is a Python 3.10+ web SSH client. Tornado serves the HTTP and WebSocket +endpoints, Paramiko manages SSH connections, and xterm.js renders the terminal +in the browser. + +## Repository layout + +- `webssh/main.py`: application entry point and route wiring. +- `webssh/handler.py`: HTTP/WebSocket handlers and SSH connection setup. +- `webssh/worker.py`: asynchronous relay between Tornado and Paramiko channels. +- `webssh/settings.py`: CLI options and application/server configuration. +- `webssh/auth.py`, `webssh/storage.py`, `webssh/crypto.py`: authentication and + persistent saved-connection data. +- `webssh/templates/` and `webssh/static/`: browser UI. +- `tests/`: pytest/unittest test suite; `tests/sshserver.py` provides the local + Paramiko-based integration server. + +## Setup and development commands + +Use Python 3.10, 3.11, or 3.12. + +```bash +python -m pip install -r requirements.txt +python -m pip install -e . +python run.py # listens on 127.0.0.1:8888 +python run.py --port=8888 --debug=True --data-dir=./data +``` + +Install test tools with `python -m pip install pytest pytest-cov ruff`. Before +handing off a change, run the checks relevant to it: + +```bash +python -m pytest tests +python -m pytest tests/test_app.py # focused example +python -m pytest --cov=webssh +ruff check . +``` + +The legacy Flake8 configuration in `setup.cfg` uses a maximum line length of +79 and excludes tests and `__init__.py` files. + +## Implementation constraints + +- Do not block Tornado's IOLoop. Paramiko connection work is blocking and must + run through the existing handler `ThreadPoolExecutor` pattern. +- Preserve locking around auth data, saved connections, and host-key mutation; + these objects are accessed from multiple threads. +- New HTTP endpoints should include `MixinHandler`. Protect private endpoints + with `@tornado.web.authenticated` and retain the existing origin, XSRF, IP, + and session checks. +- Treat credentials, private keys, cookie secrets, and saved connection data as + sensitive. Never log or expose them to the browser. Persistent auth and + connection files must retain mode `0o600`. +- Keep saved-password encryption compatible with the cookie secret and do not + silently reuse a credential when host, port, or username changes. +- Keep changes compatible with all supported Python versions and avoid adding + dependencies unless the task requires them. + +## Testing expectations + +- Add or update focused tests for behavior changes and regressions. +- Use the local mock SSH server for SSH authentication and connection flows; + tests must not depend on external SSH hosts or network services. +- For frontend or WebSocket changes, cover both the server handler behavior and + the corresponding browser-side message contract when applicable. +- Do not commit generated runtime state such as `auth.json`, `connections.json`, + cookie secrets, coverage output, caches, or local data directories. diff --git a/webssh/static/js/main.js b/webssh/static/js/main.js index e4819bd..3fc53ef 100644 --- a/webssh/static/js/main.js +++ b/webssh/static/js/main.js @@ -470,8 +470,9 @@ jQuery(function($){ function read_clipboard_text(on_text, on_error) { if (navigator.clipboard && navigator.clipboard.readText) { navigator.clipboard.readText().then(on_text, on_error); + return true; } else { - on_error(); + return false; } } @@ -855,24 +856,36 @@ jQuery(function($){ return false; } - copy_text_to_clipboard(term.getSelection(), function(copied) { + var selection = term.getSelection(); + copy_text_to_clipboard(selection, function(copied) { + if (copied && term) { + term.clearSelection(); + } + if (term) { + term.focus(); + } show_terminal_toast(copied ? '已复制' : '复制失败'); }); - term.clearSelection(); return true; } - function paste_into_terminal() { - read_clipboard_text( + function paste_into_terminal(show_unavailable) { + var started = read_clipboard_text( function(text) { - if (text) { - send_data(text); + if (text && term) { + // Let xterm normalize newlines and honor bracketed-paste mode. + term.paste(text); + term.focus(); } }, function() { - show_terminal_toast('无法读取剪贴板,请使用 Ctrl+V 粘贴'); + show_terminal_toast('剪贴板权限被拒绝,请使用 Ctrl+V 粘贴'); } ); + if (!started && show_unavailable !== false) { + show_terminal_toast('浏览器不支持直接读取剪贴板,请使用 Ctrl+V'); + } + return started; } term.attachCustomKeyEventHandler(function(e) { @@ -882,15 +895,19 @@ jQuery(function($){ var key = e.key ? e.key.toLowerCase() : ''; - // Ctrl+Shift+C / Ctrl+Shift+V: copy and paste shortcuts - if (e.ctrlKey && e.shiftKey && key === 'c') { + // Ctrl+Shift+C / Cmd+C: copy the active terminal selection. + if (((e.ctrlKey && e.shiftKey) || e.metaKey) && key === 'c' && + term.hasSelection()) { e.preventDefault(); copy_terminal_selection(); return false; } - if (e.ctrlKey && e.shiftKey && key === 'v') { - e.preventDefault(); - paste_into_terminal(); + + // Do not let xterm translate Ctrl/Cmd+V into a control character. By + // returning false without preventDefault, the browser can dispatch its + // trusted paste event to xterm's hidden textarea. This works without the + // async Clipboard API and therefore also works on plain HTTP deployments. + if ((e.ctrlKey || e.metaKey) && !e.altKey && key === 'v') { return false; } // Ctrl+C with an active selection copies instead of sending SIGINT @@ -903,16 +920,20 @@ jQuery(function($){ return true; }); - // Right click: copy the selection if there is one, otherwise paste + // Right click: copy the selection if there is one, otherwise paste. When + // programmatic clipboard reads are unavailable, keep the native context + // menu so the browser's Paste command remains usable. $('#terminal').off('contextmenu.wssh').on('contextmenu.wssh', function(e) { if (!term) { return; } - e.preventDefault(); if (term.hasSelection()) { + e.preventDefault(); copy_terminal_selection(); + } else if (paste_into_terminal(false)) { + e.preventDefault(); } else { - paste_into_terminal(); + show_terminal_toast('请从右键菜单选择粘贴,或使用 Ctrl+V'); } });