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)