diff options
author | Devaev Maxim <[email protected]> | 2020-05-22 21:07:54 +0300 |
---|---|---|
committer | Devaev Maxim <[email protected]> | 2020-05-22 21:07:54 +0300 |
commit | 43afd9acb3a7f2c94a3515f580ec3afcee720dc2 (patch) | |
tree | e1e3031ea1f083f17751c4997f29708dfb7e5d98 | |
parent | 0fa0680bd7c28e246c70b5a5102e38a592bd0f0d (diff) |
server-side paste-as-keys
-rw-r--r-- | kvmd/apps/kvmd/api/hid.py | 37 | ||||
-rw-r--r-- | kvmd/apps/kvmd/server.py | 2 | ||||
-rw-r--r-- | kvmd/apps/vnc/server.py | 9 | ||||
-rw-r--r-- | kvmd/clients/kvmd.py | 12 | ||||
-rw-r--r-- | kvmd/keyprint.py | 103 | ||||
-rw-r--r-- | kvmd/plugins/hid/__init__.py | 10 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/__init__.py | 10 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/device.py | 4 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/keyboard.py | 2 | ||||
-rw-r--r-- | kvmd/plugins/hid/otg/mouse.py | 2 | ||||
-rw-r--r-- | kvmd/plugins/hid/serial.py | 24 | ||||
-rw-r--r-- | web/kvm/index.html | 1 | ||||
-rw-r--r-- | web/share/js/kvm/hid.js | 92 |
13 files changed, 196 insertions, 112 deletions
diff --git a/kvmd/apps/kvmd/api/hid.py b/kvmd/apps/kvmd/api/hid.py index 9d18b2d5..5283751a 100644 --- a/kvmd/apps/kvmd/api/hid.py +++ b/kvmd/apps/kvmd/api/hid.py @@ -20,6 +20,8 @@ # ========================================================================== # +import asyncio + from typing import Dict from aiohttp.web import Request @@ -29,12 +31,15 @@ from aiohttp.web import WebSocketResponse from ....plugins.hid import BaseHid from ....validators.basic import valid_bool +from ....validators.basic import valid_number from ....validators.kvm import valid_hid_key from ....validators.kvm import valid_hid_mouse_move from ....validators.kvm import valid_hid_mouse_button from ....validators.kvm import valid_hid_mouse_wheel +from .... import keyprint + from ..http import exposed_http from ..http import exposed_ws from ..http import make_json_response @@ -45,6 +50,8 @@ class HidApi: def __init__(self, hid: BaseHid) -> None: self.__hid = hid + self.__key_lock = asyncio.Lock() + # ===== @exposed_http("GET", "/hid") @@ -56,16 +63,28 @@ class HidApi: await self.__hid.reset() return make_json_response() + @exposed_http("POST", "/hid/print") + async def __print_handler(self, request: Request) -> Response: + text = await request.text() + limit = int(valid_number(request.query.get("limit", "1024"), min=0, type=int)) + if limit > 0: + text = text[:limit] + async with self.__key_lock: + for (key, state) in keyprint.text_to_keys(text): + self.__hid.send_key_event(key, state) + return make_json_response() + # ===== @exposed_ws("key") async def __ws_key_handler(self, _: WebSocketResponse, event: Dict) -> None: - try: - key = valid_hid_key(event["key"]) - state = valid_bool(event["state"]) - except Exception: - return - await self.__hid.send_key_event(key, state) + async with self.__key_lock: + try: + key = valid_hid_key(event["key"]) + state = valid_bool(event["state"]) + except Exception: + return + self.__hid.send_key_event(key, state) @exposed_ws("mouse_button") async def __ws_mouse_button_handler(self, _: WebSocketResponse, event: Dict) -> None: @@ -74,7 +93,7 @@ class HidApi: state = valid_bool(event["state"]) except Exception: return - await self.__hid.send_mouse_button_event(button, state) + self.__hid.send_mouse_button_event(button, state) @exposed_ws("mouse_move") async def __ws_mouse_move_handler(self, _: WebSocketResponse, event: Dict) -> None: @@ -83,7 +102,7 @@ class HidApi: to_y = valid_hid_mouse_move(event["to"]["y"]) except Exception: return - await self.__hid.send_mouse_move_event(to_x, to_y) + self.__hid.send_mouse_move_event(to_x, to_y) @exposed_ws("mouse_wheel") async def __ws_mouse_wheel_handler(self, _: WebSocketResponse, event: Dict) -> None: @@ -92,4 +111,4 @@ class HidApi: delta_y = valid_hid_mouse_wheel(event["delta"]["y"]) except Exception: return - await self.__hid.send_mouse_wheel_event(delta_x, delta_y) + self.__hid.send_mouse_wheel_event(delta_x, delta_y) diff --git a/kvmd/apps/kvmd/server.py b/kvmd/apps/kvmd/server.py index a965757c..3a309651 100644 --- a/kvmd/apps/kvmd/server.py +++ b/kvmd/apps/kvmd/server.py @@ -391,7 +391,7 @@ class KvmdServer(HttpServer): # pylint: disable=too-many-arguments,too-many-ins async def __remove_socket(self, ws: aiohttp.web.WebSocketResponse) -> None: async with self.__sockets_lock: - await self.__hid.clear_events() + self.__hid.clear_events() try: self.__sockets.remove(ws) remote: Optional[str] = (ws._req.remote if ws._req is not None else None) # pylint: disable=protected-access diff --git a/kvmd/apps/vnc/server.py b/kvmd/apps/vnc/server.py index e2ac697c..0b9a26f6 100644 --- a/kvmd/apps/vnc/server.py +++ b/kvmd/apps/vnc/server.py @@ -278,7 +278,14 @@ class _Client(RfbClient): # pylint: disable=too-many-instance-attributes self.__mouse_move = move async def _on_cut_event(self, text: str) -> None: - pass # print("CutEvent", text) # TODO + assert self.__authorized.done() + (user, passwd) = self.__authorized.result() + logger = get_logger(0) + logger.info("[main] Client %s: Printing %d characters ...", self._remote, len(text)) + try: + await self.__kvmd.hid.print(user, passwd, text, 0) + except Exception: + logger.exception("[main] Client %s: Can't print characters", self._remote) async def _on_set_encodings(self) -> None: assert self.__authorized.done() diff --git a/kvmd/clients/kvmd.py b/kvmd/clients/kvmd.py index 0b33eb42..678f104e 100644 --- a/kvmd/clients/kvmd.py +++ b/kvmd/clients/kvmd.py @@ -89,6 +89,17 @@ class _StreamerClientPart(_BaseClientPart): aiotools.raise_not_200(response) +class _HidClientPart(_BaseClientPart): + async def print(self, user: str, passwd: str, text: str, limit: int) -> None: + async with self._make_session(user, passwd) as session: + async with session.post( + url=self._make_url("hid/print"), + params={"limit": limit}, + data=text, + ) as response: + aiotools.raise_not_200(response) + + class _AtxClientPart(_BaseClientPart): async def get_state(self, user: str, passwd: str) -> Dict: async with self._make_session(user, passwd) as session: @@ -134,6 +145,7 @@ class KvmdClient(_BaseClientPart): self.auth = _AuthClientPart(**kwargs) self.streamer = _StreamerClientPart(**kwargs) + self.hid = _HidClientPart(**kwargs) self.atx = _AtxClientPart(**kwargs) @contextlib.asynccontextmanager diff --git a/kvmd/keyprint.py b/kvmd/keyprint.py new file mode 100644 index 00000000..bee7916a --- /dev/null +++ b/kvmd/keyprint.py @@ -0,0 +1,103 @@ +# ========================================================================== # +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 Maxim Devaev <[email protected]> # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see <https://www.gnu.org/licenses/>. # +# # +# ========================================================================== # + + +import string + +from typing import Tuple +from typing import Generator + +from . import keymap + + +# ===== +_LOWER_CHARS = { + "\n": "Enter", + "\t": "Tab", + " ": "Space", + "`": "Backquote", + "\\": "Backslash", + "[": "BracketLeft", + "]": "BracketLeft", + ",": "Comma", + ".": "Period", + "-": "Minus", + "'": "Quote", + ";": "Semicolon", + "/": "Slash", + "=": "Equal", + **{str(number): f"Digit{number}" for number in range(0, 10)}, + **{ch: f"Key{ch.upper()}" for ch in string.ascii_lowercase}, +} +assert not set(_LOWER_CHARS.values()).difference(keymap.KEYMAP) + +_UPPER_CHARS = { + "~": "Backquote", + "|": "Backslash", + "{": "BracketLeft", + "}": "BracketRight", + "<": "Comma", + ">": "Period", + "!": "Digit1", + "@": "Digit2", + "#": "Digit3", + "$": "Digit4", + "%": "Digit5", + "^": "Digit6", + "&": "Digit7", + "*": "Digit8", + "(": "Digit9", + ")": "Digit0", + "_": "Minus", + "\"": "Quote", + ":": "Semicolon", + "?": "Slash", + "+": "Equal", + **{ch: f"Key{ch}" for ch in string.ascii_uppercase}, +} +assert not set(_UPPER_CHARS.values()).difference(keymap.KEYMAP) + + +# ===== +def text_to_keys(text: str, shift_key: str="ShiftLeft") -> Generator[Tuple[str, bool], None, None]: + assert shift_key in ["ShiftLeft", "ShiftRight"] + + shifted = False + for ch in text: + upper = False + key = _LOWER_CHARS.get(ch) + if key is None: + if (key := _UPPER_CHARS.get(ch)) is None: + continue + upper = True + + if upper and not shifted: + yield (shift_key, True) + shifted = True + elif not upper and shifted: + yield (shift_key, False) + shifted = False + + yield (key, True) + yield (key, False) + + if shifted: + yield (shift_key, False) diff --git a/kvmd/plugins/hid/__init__.py b/kvmd/plugins/hid/__init__.py index bc391b03..5227d673 100644 --- a/kvmd/plugins/hid/__init__.py +++ b/kvmd/plugins/hid/__init__.py @@ -48,19 +48,19 @@ class BaseHid(BasePlugin): # ===== - async def send_key_event(self, key: str, state: bool) -> None: + def send_key_event(self, key: str, state: bool) -> None: raise NotImplementedError - async def send_mouse_button_event(self, button: str, state: bool) -> None: + def send_mouse_button_event(self, button: str, state: bool) -> None: raise NotImplementedError - async def send_mouse_move_event(self, to_x: int, to_y: int) -> None: + def send_mouse_move_event(self, to_x: int, to_y: int) -> None: raise NotImplementedError - async def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: raise NotImplementedError - async def clear_events(self) -> None: + def clear_events(self) -> None: raise NotImplementedError diff --git a/kvmd/plugins/hid/otg/__init__.py b/kvmd/plugins/hid/otg/__init__.py index 0684a674..3dc49c72 100644 --- a/kvmd/plugins/hid/otg/__init__.py +++ b/kvmd/plugins/hid/otg/__init__.py @@ -113,18 +113,18 @@ class Plugin(BaseHid): # ===== - async def send_key_event(self, key: str, state: bool) -> None: + def send_key_event(self, key: str, state: bool) -> None: self.__keyboard_proc.send_key_event(key, state) - async def send_mouse_button_event(self, button: str, state: bool) -> None: + def send_mouse_button_event(self, button: str, state: bool) -> None: self.__mouse_proc.send_button_event(button, state) - async def send_mouse_move_event(self, to_x: int, to_y: int) -> None: + def send_mouse_move_event(self, to_x: int, to_y: int) -> None: self.__mouse_proc.send_move_event(to_x, to_y) - async def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: self.__mouse_proc.send_wheel_event(delta_x, delta_y) - async def clear_events(self) -> None: + def clear_events(self) -> None: self.__keyboard_proc.send_clear_event() self.__mouse_proc.send_clear_event() diff --git a/kvmd/plugins/hid/otg/device.py b/kvmd/plugins/hid/otg/device.py index 2986ce4e..d1a2c86c 100644 --- a/kvmd/plugins/hid/otg/device.py +++ b/kvmd/plugins/hid/otg/device.py @@ -126,6 +126,10 @@ class BaseDeviceProcess(multiprocessing.Process): # pylint: disable=too-many-in def _queue_event(self, event: BaseEvent) -> None: self.__events_queue.put_nowait(event) + def _clear_queue(self) -> None: + while not self.__events_queue.empty(): + self.__events_queue.get_nowait() + def _ensure_write(self, report: bytes, reopen: bool=False, close: bool=False) -> bool: if reopen: self.__close_device() diff --git a/kvmd/plugins/hid/otg/keyboard.py b/kvmd/plugins/hid/otg/keyboard.py index fb964d8a..cb83232b 100644 --- a/kvmd/plugins/hid/otg/keyboard.py +++ b/kvmd/plugins/hid/otg/keyboard.py @@ -81,9 +81,11 @@ class KeyboardProcess(BaseDeviceProcess): self._ensure_write(b"\x00" * 8, close=True) # Release all keys and modifiers def send_clear_event(self) -> None: + self._clear_queue() self._queue_event(_ClearEvent()) def send_reset_event(self) -> None: + self._clear_queue() self._queue_event(_ResetEvent()) def send_key_event(self, key: str, state: bool) -> None: diff --git a/kvmd/plugins/hid/otg/mouse.py b/kvmd/plugins/hid/otg/mouse.py index a31d7ae5..3c41eb40 100644 --- a/kvmd/plugins/hid/otg/mouse.py +++ b/kvmd/plugins/hid/otg/mouse.py @@ -79,9 +79,11 @@ class MouseProcess(BaseDeviceProcess): self._ensure_write(report, close=True) # Release all buttons def send_clear_event(self) -> None: + self._clear_queue() self._queue_event(_ClearEvent()) def send_reset_event(self) -> None: + self._clear_queue() self._queue_event(_ResetEvent()) def send_button_event(self, button: str, state: bool) -> None: diff --git a/kvmd/plugins/hid/serial.py b/kvmd/plugins/hid/serial.py index 1725483b..f2ece19d 100644 --- a/kvmd/plugins/hid/serial.py +++ b/kvmd/plugins/hid/serial.py @@ -252,22 +252,24 @@ class Plugin(BaseHid, multiprocessing.Process): # pylint: disable=too-many-inst # ===== - async def send_key_event(self, key: str, state: bool) -> None: - await self.__queue_event(_KeyEvent(key, state)) + def send_key_event(self, key: str, state: bool) -> None: + self.__queue_event(_KeyEvent(key, state)) - async def send_mouse_button_event(self, button: str, state: bool) -> None: - await self.__queue_event(_MouseButtonEvent(button, state)) + def send_mouse_button_event(self, button: str, state: bool) -> None: + self.__queue_event(_MouseButtonEvent(button, state)) - async def send_mouse_move_event(self, to_x: int, to_y: int) -> None: - await self.__queue_event(_MouseMoveEvent(to_x, to_y)) + def send_mouse_move_event(self, to_x: int, to_y: int) -> None: + self.__queue_event(_MouseMoveEvent(to_x, to_y)) - async def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: - await self.__queue_event(_MouseWheelEvent(delta_x, delta_y)) + def send_mouse_wheel_event(self, delta_x: int, delta_y: int) -> None: + self.__queue_event(_MouseWheelEvent(delta_x, delta_y)) - async def clear_events(self) -> None: - await self.__queue_event(_ClearEvent()) + def clear_events(self) -> None: + while not self.__events_queue.empty(): + self.__events_queue.get_nowait() + self.__queue_event(_ClearEvent()) - async def __queue_event(self, event: _BaseEvent) -> None: + def __queue_event(self, event: _BaseEvent) -> None: if not self.__stop_event.is_set(): self.__events_queue.put_nowait(event) diff --git a/web/kvm/index.html b/web/kvm/index.html index bd4ea1d4..1bcbe55b 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -301,7 +301,6 @@ <li class="menu-right-items"> <a class="menu-item" href="#"> - <img data-dont-hide-menu id="hid-pak-led" class="led-gray" src="../share/svg/led-gear.svg" /> Shortcuts ↴ </a> <div data-dont-hide-menu class="menu-item-content"> diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js index 4868b0a5..8efb72c2 100644 --- a/web/share/js/kvm/hid.js +++ b/web/share/js/kvm/hid.js @@ -35,11 +35,6 @@ export function Hid() { /************************************************************************/ - var __ws = null; - - var __chars_to_codes = {}; - var __codes_delay = 50; - var __keyboard = new Keyboard(); var __mouse = new Mouse(); @@ -73,8 +68,6 @@ export function Hid() { window.addEventListener("pagehide", __releaseAll); window.addEventListener("blur", __releaseAll); - __chars_to_codes = __buildCharsToCodes(); - tools.setOnClick($("hid-pak-button"), __clickPasteAsKeysButton); tools.setOnClick($("hid-reset-button"), __clickResetButton); @@ -89,7 +82,6 @@ export function Hid() { wm.switchEnabled($("hid-pak-text"), ws); wm.switchEnabled($("hid-pak-button"), ws); wm.switchEnabled($("hid-reset-button"), ws); - __ws = ws; __keyboard.setSocket(ws); __mouse.setSocket(ws); }; @@ -125,68 +117,16 @@ export function Hid() { } else { resolve(null); } - }, __codes_delay); + }, 50); iterate(); }); }; - var __buildCharsToCodes = function() { - let chars_to_codes = { - "\n": ["Enter"], - "\t": ["Tab"], - " ": ["Space"], - "`": ["Backquote"], "~": ["ShiftLeft", "Backquote"], - "\\": ["Backslash"], "|": ["ShiftLeft", "Backslash"], - "[": ["BracketLeft"], "{": ["ShiftLeft", "BracketLeft"], - "]": ["BracketLeft"], "}": ["ShiftLeft", "BracketRight"], - ",": ["Comma"], "<": ["ShiftLeft", "Comma"], - ".": ["Period"], ">": ["ShiftLeft", "Period"], - "1": ["Digit1"], "!": ["ShiftLeft", "Digit1"], - "2": ["Digit2"], "@": ["ShiftLeft", "Digit2"], - "3": ["Digit3"], "#": ["ShiftLeft", "Digit3"], - "4": ["Digit4"], "$": ["ShiftLeft", "Digit4"], - "5": ["Digit5"], "%": ["ShiftLeft", "Digit5"], - "6": ["Digit6"], "^": ["ShiftLeft", "Digit6"], - "7": ["Digit7"], "&": ["ShiftLeft", "Digit7"], - "8": ["Digit8"], "*": ["ShiftLeft", "Digit8"], - "9": ["Digit9"], "(": ["ShiftLeft", "Digit9"], - "0": ["Digit0"], ")": ["ShiftLeft", "Digit0"], - "-": ["Minus"], "_": ["ShiftLeft", "Minus"], - "'": ["Quote"], "\"": ["ShiftLeft", "Quote"], - ";": ["Semicolon"], ":": ["ShiftLeft", "Semicolon"], - "/": ["Slash"], "?": ["ShiftLeft", "Slash"], - "=": ["Equal"], "+": ["ShiftLeft", "Equal"], - }; - - for (let ch = "a".charCodeAt(0); ch <= "z".charCodeAt(0); ++ch) { - let low = String.fromCharCode(ch); - let up = low.toUpperCase(); - let code = "Key" + up; - chars_to_codes[low] = [code]; - chars_to_codes[up] = ["ShiftLeft", code]; - } - - return chars_to_codes; - }; - var __clickPasteAsKeysButton = function() { let text = $("hid-pak-text").value.replace(/[^\x00-\x7F]/g, ""); // eslint-disable-line no-control-regex if (text) { - let clipboard_codes = []; - let codes_count = 0; - for (let ch of text) { - let codes = __chars_to_codes[ch]; - if (codes) { - codes_count += codes.length; - clipboard_codes.push(codes); - } - } - let time = __codes_delay * codes_count * 2 / 1000; - let confirm_msg = ` - You are going to automatically type ${codes_count} characters from the system clipboard. - It will take ${time} seconds.<br> - <br> + You're goint to paste ${text.length} characters.<br> Are you sure you want to continue? `; @@ -194,27 +134,21 @@ export function Hid() { if (ok) { wm.switchEnabled($("hid-pak-text"), false); wm.switchEnabled($("hid-pak-button"), false); - $("hid-pak-led").className = "led-yellow-rotating-fast"; - $("hid-pak-led").title = "Autotyping..."; tools.debug("HID: paste-as-keys:", text); - let index = 0; - let iterate = function() { - __emitShortcut(clipboard_codes[index]).then(function() { - ++index; - if (index < clipboard_codes.length && __ws) { - iterate(); - } else { - $("hid-pak-text").value = ""; - wm.switchEnabled($("hid-pak-text"), true); - wm.switchEnabled($("hid-pak-button"), true); - $("hid-pak-led").className = "led-gray"; - $("hid-pak-led").title = ""; + let http = tools.makeRequest("POST", "/api/hid/print?limit=0", function() { + if (http.readyState === 4) { + wm.switchEnabled($("hid-pak-text"), true); + wm.switchEnabled($("hid-pak-button"), true); + $("hid-pak-text").value = ""; + if (http.status === 413) { + wm.error("Too many text for paste!"); + } else if (http.status !== 200) { + wm.error("HID paste error:<br>", http.responseText); } - }); - }; - iterate(); + } + }, text, "text/plain"); } else { $("hid-pak-text").value = ""; } |