mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2025-12-12 01:00:29 +08:00
server-side paste-as-keys
This commit is contained in:
parent
0fa0680bd7
commit
43afd9acb3
@ -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:
|
||||
async with self.__key_lock:
|
||||
try:
|
||||
key = valid_hid_key(event["key"])
|
||||
state = valid_bool(event["state"])
|
||||
except Exception:
|
||||
return
|
||||
await self.__hid.send_key_event(key, state)
|
||||
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)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
103
kvmd/keyprint.py
Normal 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)
|
||||
@ -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
|
||||
|
||||
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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 = "";
|
||||
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-led").className = "led-gray";
|
||||
$("hid-pak-led").title = "";
|
||||
$("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 = "";
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user