server-side paste-as-keys

This commit is contained in:
Devaev Maxim 2020-05-22 21:07:54 +03:00
parent 0fa0680bd7
commit 43afd9acb3
13 changed files with 196 additions and 112 deletions

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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

103
kvmd/keyprint.py Normal file
View File

@ -0,0 +1,103 @@
# ========================================================================== #
# #
# KVMD - The main Pi-KVM daemon. #
# #
# Copyright (C) 2018 Maxim Devaev <mdevaev@gmail.com> #
# #
# 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)

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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:

View File

@ -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:

View File

@ -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)

View File

@ -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 &#8628;
</a>
<div data-dont-hide-menu class="menu-item-content">

View File

@ -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 = "";
}