Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37c508297f | ||
|
|
13bf07fecf | ||
|
|
849b821501 | ||
|
|
00a27018d9 | ||
|
|
95238b781f | ||
|
|
3ea7937b5b | ||
|
|
80d9e30bd4 | ||
|
|
bde2df2b44 | ||
|
|
76a73017db | ||
|
|
db06ddc054 | ||
|
|
e0a048fbcc | ||
|
|
9d736bb91f | ||
|
|
914609b758 | ||
|
|
af81c92695 | ||
|
|
a528e9f3b2 | ||
|
|
3b3ce96678 | ||
|
|
42316ba137 | ||
|
|
d34baa67be | ||
|
|
491724beb5 | ||
|
|
b54b87ca6f | ||
|
|
270ba455a8 | ||
|
|
f3b312046f | ||
|
|
436b080deb |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.12"
|
version = "2.8.35"
|
||||||
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
description = "TESM -- Verwaltung und Ueberwachung von PoE-Endgeraeten und Aruba-Switchen"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ def _register_blueprints(app: Flask) -> None:
|
|||||||
settings,
|
settings,
|
||||||
switches,
|
switches,
|
||||||
hostkeys,
|
hostkeys,
|
||||||
|
screen,
|
||||||
terminal,
|
terminal,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ def _register_blueprints(app: Flask) -> None:
|
|||||||
app.register_blueprint(license_bp.bp)
|
app.register_blueprint(license_bp.bp)
|
||||||
app.register_blueprint(backup.bp)
|
app.register_blueprint(backup.bp)
|
||||||
terminal.init_app(app)
|
terminal.init_app(app)
|
||||||
|
screen.init_app(app)
|
||||||
|
|
||||||
# Die Statusuebersicht ist die Startseite; ``index`` bleibt als Endpunkt
|
# Die Statusuebersicht ist die Startseite; ``index`` bleibt als Endpunkt
|
||||||
# bestehen, weil der Kern (Weiterleitungen, Fehlerseiten) darauf verweist.
|
# bestehen, weil der Kern (Weiterleitungen, Fehlerseiten) darauf verweist.
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from flask import Blueprint, abort, render_template, request
|
from flask import Blueprint, abort, render_template, request, url_for
|
||||||
from tesm_core.security.sessions import fragment
|
from tesm_core.security.sessions import fragment
|
||||||
from tesm_core.extension import core, db
|
from tesm_core.extension import core, db
|
||||||
|
|
||||||
@@ -49,6 +49,13 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
|||||||
# Einmal je Seitenaufbau, nicht je Kachel: der Port ist fuer alle derselbe,
|
# Einmal je Seitenaufbau, nicht je Kachel: der Port ist fuer alle derselbe,
|
||||||
# und ohne das Recht wird gar nicht erst nachgesehen.
|
# und ohne das Recht wird gar nicht erst nachgesehen.
|
||||||
vnc_erlaubt = detailed and current_user.has("device_vnc.execute")
|
vnc_erlaubt = detailed and current_user.has("device_vnc.execute")
|
||||||
|
# Das Fenster setzt eine frische Passwortbestaetigung voraus -- dieselbe
|
||||||
|
# Schwelle wie die Geraeteseite. Fehlt sie, fuehrt der Knopf dorthin: dort
|
||||||
|
# wird danach gefragt, und ein Fenster, das sich mit "bitte erst
|
||||||
|
# bestaetigen" oeffnet, waere ein Klick fuer nichts.
|
||||||
|
from tesm_core.auth.service import reauth_is_fresh
|
||||||
|
|
||||||
|
vnc_fenster = vnc_erlaubt and reauth_is_fresh()
|
||||||
vnc_port = (
|
vnc_port = (
|
||||||
int(core().settings.get(conn, "vnc_port") or vnc_dienst.STANDARD_PORT)
|
int(core().settings.get(conn, "vnc_port") or vnc_dienst.STANDARD_PORT)
|
||||||
if vnc_erlaubt and core().settings.get(conn, "vnc_enabled")
|
if vnc_erlaubt and core().settings.get(conn, "vnc_enabled")
|
||||||
@@ -84,26 +91,27 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
|||||||
maintenance_active=_maintenance_active(),
|
maintenance_active=_maintenance_active(),
|
||||||
darf=current_user.has,
|
darf=current_user.has,
|
||||||
),
|
),
|
||||||
# Die fertige Adresse, nicht nur ein Schalter: der Knopf
|
# Der Knopf oeffnet die Sitzung in TESM selbst. Ein
|
||||||
# auf der Kachel soll die Verbindung oeffnen und nicht
|
# vnc://-Link setzte einen eingerichteten Viewer auf dem
|
||||||
# erst eine Datei ablegen, die jemand anklicken muss.
|
# Arbeitsplatz voraus -- und tat dort, wo keiner
|
||||||
|
# registriert ist, schlicht nichts.
|
||||||
#
|
#
|
||||||
# Beides -- ob dort ein Server antwortet und wohin gefragt
|
# Ob dort ein Server antwortet, steht aus dem letzten
|
||||||
# wurde -- steht aus dem letzten Durchlauf in der
|
# Durchlauf in der Datenbank. Die Uebersicht zeigt viele
|
||||||
# Datenbank. Die Uebersicht zeigt viele Kacheln und laedt
|
# Kacheln und laedt sich selbst nach; ein
|
||||||
# sich selbst nach; ein Verbindungsversuch oder eine
|
# Verbindungsversuch je Kachel und Aufbau waere genau die
|
||||||
# Namensaufloesung je Kachel und Aufbau waere genau die
|
|
||||||
# Art Hintergrundlast, die niemand bestellt hat.
|
# Art Hintergrundlast, die niemand bestellt hat.
|
||||||
"vnc": (
|
"vnc": bool(vnc_port and row["vnc_open"]),
|
||||||
vnc_dienst.Zugang(
|
"vnc_quelle": (
|
||||||
ziel=str(row["vnc_target"]), port=vnc_port, erreichbar=True
|
url_for("screen.session_json", device_id=int(row["id"]))
|
||||||
).adresse
|
if vnc_port and row["vnc_open"]
|
||||||
if vnc_port and row["vnc_open"] and row["vnc_target"]
|
|
||||||
else ""
|
else ""
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
tiles.append(entry)
|
tiles.append(entry)
|
||||||
|
# Merker fuer die Vorlage: nur mit frischer Bestaetigung gibt es das
|
||||||
|
# Fenster; sonst zeigt die Kachel auf die Seite.
|
||||||
|
|
||||||
sections = [
|
sections = [
|
||||||
{
|
{
|
||||||
@@ -148,6 +156,7 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
|||||||
"counts": counts,
|
"counts": counts,
|
||||||
"sections": sections,
|
"sections": sections,
|
||||||
"detailed": detailed,
|
"detailed": detailed,
|
||||||
|
"vnc_fenster": vnc_fenster,
|
||||||
"interval": int(core().settings.get(conn, "monitor_interval_seconds") or 300),
|
"interval": int(core().settings.get(conn, "monitor_interval_seconds") or 300),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -318,12 +318,16 @@ def _restart_as_job(extension: Any, conn: Any, device: Any, method: Any): # typ
|
|||||||
# Der Auftrag laeuft im Hintergrund und meldet sein Ergebnis erst, wenn das
|
# Der Auftrag laeuft im Hintergrund und meldet sein Ergebnis erst, wenn das
|
||||||
# Geraet zurueck ist. Der Zustand muss aber sofort umschlagen -- sonst sieht
|
# Geraet zurueck ist. Der Zustand muss aber sofort umschlagen -- sonst sieht
|
||||||
# die Uebersicht aus wie eine, in der nichts passiert ist.
|
# die Uebersicht aus wie eine, in der nichts passiert ist.
|
||||||
|
live = extension.registry.get("live_log")
|
||||||
with transaction():
|
with transaction():
|
||||||
monitor.mark_restarting(conn, int(device["id"]))
|
monitor.mark_restarting(
|
||||||
|
conn, int(device["id"]), live=live, trigger="manuell", method=method.key
|
||||||
|
)
|
||||||
|
|
||||||
maintenance_service.start_background(
|
maintenance_service.start_background(
|
||||||
maintenance_service.run_reboot,
|
maintenance_service.run_reboot,
|
||||||
database=extension.database,
|
database=extension.database,
|
||||||
|
live=live,
|
||||||
job_id=handle.id,
|
job_id=handle.id,
|
||||||
device=dict(device),
|
device=dict(device),
|
||||||
username=username,
|
username=username,
|
||||||
@@ -374,13 +378,13 @@ def detail(device_id: int):
|
|||||||
|
|
||||||
|
|
||||||
def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None:
|
def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None:
|
||||||
"""Der Bildschirmzugriff dieses Clients -- frisch gefragt, nicht geraten.
|
"""Der Bildschirmzugriff dieses Clients -- aus dem, was bekannt ist.
|
||||||
|
|
||||||
Die Kacheln der Statusuebersicht nehmen den Stand aus dem letzten
|
Diese Seite fragt das Geraet **nicht**. Erreichbarkeit kommt aus dem
|
||||||
Ueberwachungsdurchlauf; hier geht es um **ein** Geraet, also fragt die Seite
|
Ueberwachungsdurchlauf, die Anmeldeverfahren aus der letzten
|
||||||
selbst nach (ein halbe Sekunde Zeitlimit, und nur wenn der Client gerade
|
ausdruecklichen Pruefung. Der Grund steht in :func:`vnc.zugang_gemerkt`:
|
||||||
erreichbar ist). Das Ergebnis wird gleich festgehalten -- dann stimmt auch
|
jede Messung ist eine angefangene Protokollverhandlung, und ein RealVNC
|
||||||
die Kachel, ohne auf den naechsten Durchlauf zu warten.
|
Server zaehlt so etwas mit.
|
||||||
"""
|
"""
|
||||||
extension = core()
|
extension = core()
|
||||||
if not current_user.has("device_vnc.execute"):
|
if not current_user.has("device_vnc.execute"):
|
||||||
@@ -388,12 +392,7 @@ def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None:
|
|||||||
if not extension.settings.get(conn, "vnc_enabled"):
|
if not extension.settings.get(conn, "vnc_enabled"):
|
||||||
return None
|
return None
|
||||||
port = int(extension.settings.get(conn, "vnc_port") or vnc.STANDARD_PORT)
|
port = int(extension.settings.get(conn, "vnc_port") or vnc.STANDARD_PORT)
|
||||||
online = str(device.get("state") or "") == "online" and not device.get("deleted_at")
|
return vnc.zugang_gemerkt(device, port=port)
|
||||||
zugang = vnc.zugang(device, port=port, pruefen=online)
|
|
||||||
if online:
|
|
||||||
with transaction():
|
|
||||||
monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar, zugang.ziel)
|
|
||||||
return zugang
|
|
||||||
|
|
||||||
|
|
||||||
@bp.get("/<int:device_id>/vnc")
|
@bp.get("/<int:device_id>/vnc")
|
||||||
@@ -416,15 +415,21 @@ def vnc_connection(device_id: int):
|
|||||||
ziel = vnc.ziel(device)
|
ziel = vnc.ziel(device)
|
||||||
if not ziel:
|
if not ziel:
|
||||||
abort(404)
|
abort(404)
|
||||||
with transaction():
|
# Kein Eintrag in der Aenderungskette: eine Verbindungsdatei ist derselbe
|
||||||
_audit(
|
# Vorgang wie die Sitzung im Browser, nur fuer einen Viewer auf dem
|
||||||
conn,
|
# Arbeitsplatz -- und sie aendert nichts. Wer zusieht, steht im laufenden
|
||||||
"device.vnc_opened",
|
# Protokoll; das Aenderungsprotokoll bleibt die Liste der Eingriffe.
|
||||||
str(device["name"]),
|
live = core().registry.get("live_log")
|
||||||
f"Verbindungsdatei für {ziel}:{port}",
|
if live is not None:
|
||||||
|
live.info(
|
||||||
|
"vnc", f"{current_user.username} lädt die Verbindungsdatei für {device['name']}"
|
||||||
)
|
)
|
||||||
return Response(
|
return Response(
|
||||||
vnc.verbindungsdatei(device, port=port),
|
vnc.verbindungsdatei(
|
||||||
|
device,
|
||||||
|
port=port,
|
||||||
|
warnung_aus=bool(extension.settings.get(conn, "vnc_warn_unencrypted_off")),
|
||||||
|
),
|
||||||
mimetype="application/octet-stream",
|
mimetype="application/octet-stream",
|
||||||
headers={
|
headers={
|
||||||
"Content-Disposition": f'attachment; filename="{vnc.dateiname(device)}"',
|
"Content-Disposition": f'attachment; filename="{vnc.dateiname(device)}"',
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from tesm_core.auth.service import client_ip
|
|||||||
from tesm_core.db import Database
|
from tesm_core.db import Database
|
||||||
from tesm_core.extension import core, db, transaction
|
from tesm_core.extension import core, db, transaction
|
||||||
|
|
||||||
from ..services import kea
|
from ..services import inventory, kea
|
||||||
from ..permissions import DHCP_PAGE
|
from ..permissions import DHCP_PAGE
|
||||||
|
|
||||||
bp = Blueprint("dhcp", __name__, url_prefix="/dhcp")
|
bp = Blueprint("dhcp", __name__, url_prefix="/dhcp")
|
||||||
@@ -73,6 +73,12 @@ def overview():
|
|||||||
# Zustand aus den laufenden Leases -- wie im Windows-DHCP.
|
# Zustand aus den laufenden Leases -- wie im Windows-DHCP.
|
||||||
reservations = kea.annotate_reservations(reservations, leases)
|
reservations = kea.annotate_reservations(reservations, leases)
|
||||||
reserved_ips = {str(entry["ip"]) for entry in reservations}
|
reserved_ips = {str(entry["ip"]) for entry in reservations}
|
||||||
|
# Wozu es schon einen Client gibt, ist nicht mehr zu uebernehmen -- auch
|
||||||
|
# wenn dessen Adresse (noch) keine Reservierung hat.
|
||||||
|
bekannte_macs = {
|
||||||
|
inventory.normalize_mac(str(zeile["mac"]))
|
||||||
|
for zeile in Database.all(conn, "SELECT mac FROM devices WHERE deleted_at IS NULL")
|
||||||
|
}
|
||||||
|
|
||||||
return render_template(
|
return render_template(
|
||||||
"tesm/dhcp.html",
|
"tesm/dhcp.html",
|
||||||
@@ -90,6 +96,7 @@ def overview():
|
|||||||
manual_reservations=Database.all(conn, "SELECT * FROM dhcp_reservations ORDER BY ip"),
|
manual_reservations=Database.all(conn, "SELECT * FROM dhcp_reservations ORDER BY ip"),
|
||||||
leases=leases,
|
leases=leases,
|
||||||
reserved_ips=reserved_ips,
|
reserved_ips=reserved_ips,
|
||||||
|
bekannte_macs=bekannte_macs,
|
||||||
networks=networks,
|
networks=networks,
|
||||||
default_gateway=kea.default_gateway(sysops) if sysops else "",
|
default_gateway=kea.default_gateway(sysops) if sysops else "",
|
||||||
service=kea.service_state(sysops) if sysops else {},
|
service=kea.service_state(sysops) if sysops else {},
|
||||||
@@ -447,6 +454,91 @@ def add_reservation():
|
|||||||
return _write_and_redirect()
|
return _write_and_redirect()
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/lease/adopt")
|
||||||
|
# Zwei Rechte, mit Absicht: die Uebernahme ist eine eigene Funktion, das
|
||||||
|
# Ergebnis aber ein Client. Wer keine Clients anlegen darf, legt auch hier
|
||||||
|
# keine an -- sonst waere diese Schaltflaeche eine Hintertuer an der
|
||||||
|
# Geraeteverwaltung vorbei.
|
||||||
|
@permission_required("dhcp_lease_adopt.execute", "devices.create")
|
||||||
|
def adopt_lease():
|
||||||
|
"""Macht aus einer dynamisch vergebenen Adresse einen ueberwachten Client.
|
||||||
|
|
||||||
|
Die Angaben kommen **nicht** aus dem Formular, sondern aus der Lease-Datei:
|
||||||
|
geschickt wird nur die MAC, alles Weitere liest TESM frisch nach. Damit
|
||||||
|
heisst "Uebernahme" auch Uebernahme -- und eine Lease, die zwischen
|
||||||
|
Seitenaufbau und Klick abgelaufen oder umgezogen ist, faellt auf, statt
|
||||||
|
stillschweigend einen falschen Datensatz anzulegen.
|
||||||
|
"""
|
||||||
|
extension = core()
|
||||||
|
conn = db()
|
||||||
|
sysops = extension.sysops
|
||||||
|
gesucht = inventory.normalize_mac(request.form.get("mac") or "")
|
||||||
|
leases = kea.read_leases(sysops) if sysops else []
|
||||||
|
lease = next((eintrag for eintrag in leases if eintrag.mac == gesucht), None)
|
||||||
|
if lease is None:
|
||||||
|
flash(
|
||||||
|
"Zu dieser MAC liegt keine Lease mehr vor. Die Liste war nicht mehr aktuell.",
|
||||||
|
"warning",
|
||||||
|
)
|
||||||
|
return redirect(url_for("dhcp.overview"))
|
||||||
|
|
||||||
|
# Nur dynamische. Eine Adresse, die schon reserviert ist, gehoert bereits
|
||||||
|
# zu einem gepflegten Datensatz -- da gibt es nichts zu uebernehmen.
|
||||||
|
networks = kea.detect_networks(sysops) if sysops else []
|
||||||
|
reserviert = {
|
||||||
|
str(eintrag["ip"])
|
||||||
|
for eintrag in kea.collect_reservations(
|
||||||
|
conn, only_networks=kea.configured_networks(conn, networks)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if lease.address in reserviert:
|
||||||
|
flash(
|
||||||
|
f"{lease.address} ist bereits reserviert – diese Lease ist nicht dynamisch.",
|
||||||
|
"warning",
|
||||||
|
)
|
||||||
|
return redirect(url_for("dhcp.overview"))
|
||||||
|
|
||||||
|
# Der Hostname aus der Lease ist der Name, den das Geraet selbst nennt.
|
||||||
|
# Fehlt er, steht die Adresse da: ein Name wie "Gerät 7" sagt weniger.
|
||||||
|
name = (lease.hostname or "").strip() or f"Client {lease.address}"
|
||||||
|
try:
|
||||||
|
with transaction():
|
||||||
|
device_id = inventory.create_device(
|
||||||
|
conn,
|
||||||
|
name=name,
|
||||||
|
mac=lease.mac,
|
||||||
|
ip=lease.address,
|
||||||
|
switch_id=None,
|
||||||
|
port="",
|
||||||
|
credential_id=None,
|
||||||
|
ssh_port=None,
|
||||||
|
# Ohne Switch und Port gibt es keinen PoE-Neustart. Mit
|
||||||
|
# eingeschaltetem Automatismus schriebe die Ueberwachung bei
|
||||||
|
# jedem Ausfall einen Fehlschlag ins Protokoll, den niemand
|
||||||
|
# abstellen kann. Einschalten kann man ihn, sobald der Port
|
||||||
|
# eingetragen ist.
|
||||||
|
auto_restart=False,
|
||||||
|
actor=current_user.username,
|
||||||
|
)
|
||||||
|
except inventory.ValidationError as exc:
|
||||||
|
flash(str(exc), "danger")
|
||||||
|
return redirect(url_for("dhcp.overview"))
|
||||||
|
|
||||||
|
_audit(
|
||||||
|
"device.created",
|
||||||
|
name,
|
||||||
|
f"aus dynamischer Lease {lease.address} ({lease.mac}) übernommen",
|
||||||
|
severity="notice",
|
||||||
|
)
|
||||||
|
flash(
|
||||||
|
f"{name} wurde als Client übernommen. Switch, Port und Zugangsdaten fehlen noch – "
|
||||||
|
"ohne sie gibt es keinen PoE-Neustart und keine Wartung. Die feste Adresse entsteht "
|
||||||
|
"beim nächsten Schreiben der DHCP-Konfiguration.",
|
||||||
|
"success",
|
||||||
|
)
|
||||||
|
return redirect(url_for("devices.detail", device_id=device_id))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/reservation/<int:reservation_id>/delete")
|
@bp.post("/reservation/<int:reservation_id>/delete")
|
||||||
@permission_required("dhcp_reservations.delete")
|
@permission_required("dhcp_reservations.delete")
|
||||||
def delete_reservation(reservation_id: int):
|
def delete_reservation(reservation_id: int):
|
||||||
|
|||||||
@@ -66,4 +66,5 @@ def restarts():
|
|||||||
method=method,
|
method=method,
|
||||||
methods=tuple(monitor.METHOD_LABELS.items()),
|
methods=tuple(monitor.METHOD_LABELS.items()),
|
||||||
method_labels=monitor.METHOD_LABELS,
|
method_labels=monitor.METHOD_LABELS,
|
||||||
|
trigger_labels=monitor.TRIGGER_LABELS,
|
||||||
)
|
)
|
||||||
@@ -18,7 +18,7 @@ from __future__ import annotations
|
|||||||
from flask import Blueprint, abort, flash, jsonify, redirect, render_template, request, url_for
|
from flask import Blueprint, abort, flash, jsonify, redirect, render_template, request, url_for
|
||||||
from tesm_core.security.sessions import fragment
|
from tesm_core.security.sessions import fragment
|
||||||
from tesm_core import audit
|
from tesm_core import audit
|
||||||
from tesm_core.auth.models import current_user, permission_required
|
from tesm_core.auth.models import current_user, permission_required, reauth_required
|
||||||
from tesm_core.auth.service import client_ip
|
from tesm_core.auth.service import client_ip
|
||||||
from tesm_core.db import Database
|
from tesm_core.db import Database
|
||||||
from tesm_core.extension import core, db, transaction
|
from tesm_core.extension import core, db, transaction
|
||||||
@@ -77,18 +77,20 @@ def _query_devices(conn) -> list[dict]: # type: ignore[no-untyped-def]
|
|||||||
|
|
||||||
@bp.get("/")
|
@bp.get("/")
|
||||||
@permission_required(*SERVICING_PAGE, mode="any")
|
@permission_required(*SERVICING_PAGE, mode="any")
|
||||||
def overview():
|
def overview(): # noqa: C901 - eine Seite, drei Aufgaben
|
||||||
conn = db()
|
conn = db()
|
||||||
return render_template(
|
return render_template(
|
||||||
"tesm/maintenance.html",
|
"tesm/maintenance.html",
|
||||||
devices=_eligible_devices(conn),
|
devices=_eligible_devices(conn),
|
||||||
jobs=service.list_jobs(conn),
|
jobs=service.list_jobs(conn),
|
||||||
|
auftragslage=service.letzter_auftrag_je_geraet(conn),
|
||||||
category_labels=dict(inventory.CREDENTIAL_CATEGORIES),
|
category_labels=dict(inventory.CREDENTIAL_CATEGORIES),
|
||||||
category_effects=inventory.CATEGORY_EFFECTS,
|
category_effects=inventory.CATEGORY_EFFECTS,
|
||||||
update_category=inventory.MAINTENANCE_CATEGORY,
|
update_category=inventory.MAINTENANCE_CATEGORY,
|
||||||
terminal_enabled=bool(core().settings.get(conn, "terminal_clients_enabled")),
|
terminal_enabled=bool(core().settings.get(conn, "terminal_clients_enabled")),
|
||||||
windows_tool_missing=_windows_tool_missing(conn),
|
windows_tool_missing=_windows_tool_missing(conn),
|
||||||
windows_package=windowsops.REQUIRED_PACKAGE,
|
windows_package=windowsops.REQUIRED_PACKAGE,
|
||||||
|
befehlsvorlagen=service.BEFEHLSVORLAGEN,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -144,12 +146,19 @@ def install_windows_tool():
|
|||||||
def status():
|
def status():
|
||||||
"""Zustand der Auftraege -- aus der Datenbank, nicht aus dem Prozessspeicher."""
|
"""Zustand der Auftraege -- aus der Datenbank, nicht aus dem Prozessspeicher."""
|
||||||
conn = db()
|
conn = db()
|
||||||
jobs = service.list_jobs(conn, limit=30)
|
jobs = service.laufende_und_letzte(conn, limit=30)
|
||||||
running = any(job["state"] in {"pending", "running"} for job in jobs)
|
running = any(job["state"] in {"pending", "running"} for job in jobs)
|
||||||
return jsonify(
|
return jsonify(
|
||||||
{
|
{
|
||||||
"running": running,
|
"running": running,
|
||||||
"poll_ms": 2000 if running else 10000,
|
"poll_ms": 2000 if running else 10000,
|
||||||
|
# Der Zustand der Geraete gehoert dazu: die Seite steht sonst mit
|
||||||
|
# dem Stand ihres Aufbaus da, und wer nach einem Neustart auf die
|
||||||
|
# Rueckkehr wartet, sieht "unbekannt", bis er von Hand neu laedt.
|
||||||
|
"devices": [
|
||||||
|
{"id": int(row["id"]), "state": str(row["state"] or "unknown")}
|
||||||
|
for row in _eligible_devices(conn)
|
||||||
|
],
|
||||||
"jobs": [
|
"jobs": [
|
||||||
{
|
{
|
||||||
"id": job["id"],
|
"id": job["id"],
|
||||||
@@ -159,6 +168,16 @@ def status():
|
|||||||
"progress": job["progress"],
|
"progress": job["progress"],
|
||||||
"message": job["message"],
|
"message": job["message"],
|
||||||
"updated_at": job["updated_at"],
|
"updated_at": job["updated_at"],
|
||||||
|
# Die letzten Zeilen -- nur von dem, was gerade laeuft.
|
||||||
|
# Mitlesen heisst mitlesen: wer nach der Wartung wissen
|
||||||
|
# will, was war, oeffnet den Auftrag. Alles mitzuschicken
|
||||||
|
# hiesse, alle zwei Sekunden das gesamte Protokoll aller
|
||||||
|
# Geraete zu uebertragen.
|
||||||
|
"output": (
|
||||||
|
service.letzte_zeilen(str(job["output"] or ""))
|
||||||
|
if service.eben_fertig(dict(job))
|
||||||
|
else ""
|
||||||
|
),
|
||||||
}
|
}
|
||||||
for job in jobs
|
for job in jobs
|
||||||
],
|
],
|
||||||
@@ -187,7 +206,7 @@ def job_output(job_id: str):
|
|||||||
return render_template("tesm/_maintenance_output.html", job=job)
|
return render_template("tesm/_maintenance_output.html", job=job)
|
||||||
|
|
||||||
|
|
||||||
def _start(kind: str) -> None:
|
def _start(kind: str, befehl: str = "") -> None:
|
||||||
extension = core()
|
extension = core()
|
||||||
conn = db()
|
conn = db()
|
||||||
device_ids = [int(value) for value in request.form.getlist("device_ids") if value.isdigit()]
|
device_ids = [int(value) for value in request.form.getlist("device_ids") if value.isdigit()]
|
||||||
@@ -233,12 +252,27 @@ def _start(kind: str) -> None:
|
|||||||
actor_id=current_user.id,
|
actor_id=current_user.id,
|
||||||
action=kind,
|
action=kind,
|
||||||
target=str(device["name"]),
|
target=str(device["name"]),
|
||||||
detail=f"Auftrag {handle.id}",
|
# Beim Befehl gehoert sein Wortlaut hierher und nicht nur die
|
||||||
|
# Auftragsnummer: was gelaufen ist, muss aus dem Protokoll
|
||||||
|
# hervorgehen, auch wenn der Auftrag laengst aus dem Verlauf
|
||||||
|
# gerollt ist.
|
||||||
|
detail=f"Auftrag {handle.id}" + (f": {befehl}" if befehl else ""),
|
||||||
ip=client_ip(),
|
ip=client_ip(),
|
||||||
severity="notice",
|
severity="warning" if befehl else "notice",
|
||||||
)
|
)
|
||||||
|
|
||||||
if kind == service.JOB_KIND_UPDATE:
|
if kind == service.JOB_KIND_COMMAND:
|
||||||
|
service.start_background(
|
||||||
|
service.run_command,
|
||||||
|
database=extension.database,
|
||||||
|
job_id=handle.id,
|
||||||
|
device=dict(device),
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
befehl=befehl,
|
||||||
|
timeout=int(extension.settings.get(conn, "maintenance_command_timeout") or 900),
|
||||||
|
)
|
||||||
|
elif kind == service.JOB_KIND_UPDATE:
|
||||||
service.start_background(
|
service.start_background(
|
||||||
service.run_update,
|
service.run_update,
|
||||||
database=extension.database,
|
database=extension.database,
|
||||||
@@ -249,27 +283,35 @@ def _start(kind: str) -> None:
|
|||||||
timeout=int(extension.settings.get(conn, "maintenance_command_timeout") or 900),
|
timeout=int(extension.settings.get(conn, "maintenance_command_timeout") or 900),
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
|
weg = (
|
||||||
|
service.METHOD_RPC
|
||||||
|
if str(device["category"]) == inventory.NON_SSH_CATEGORY
|
||||||
|
else service.METHOD_SSH
|
||||||
|
)
|
||||||
# Sofort sichtbar, nicht erst wenn das Geraet zurueck ist.
|
# Sofort sichtbar, nicht erst wenn das Geraet zurueck ist.
|
||||||
with transaction() as verbindung:
|
with transaction() as verbindung:
|
||||||
monitor.mark_restarting(verbindung, int(device["id"]))
|
monitor.mark_restarting(
|
||||||
|
verbindung,
|
||||||
|
int(device["id"]),
|
||||||
|
live=extension.registry.get("live_log"),
|
||||||
|
trigger="maintenance",
|
||||||
|
method=weg,
|
||||||
|
)
|
||||||
service.start_background(
|
service.start_background(
|
||||||
service.run_reboot,
|
service.run_reboot,
|
||||||
database=extension.database,
|
database=extension.database,
|
||||||
|
live=extension.registry.get("live_log"),
|
||||||
job_id=handle.id,
|
job_id=handle.id,
|
||||||
device=dict(device),
|
device=dict(device),
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
wait_seconds=int(extension.settings.get(conn, "maintenance_reboot_wait") or 300),
|
wait_seconds=int(extension.settings.get(conn, "maintenance_reboot_wait") or 300),
|
||||||
actor=current_user.username,
|
actor=current_user.username,
|
||||||
# Hier steht in ``category`` die Kategorie der *Zugangsdaten*
|
# Der Weg steht oben: in ``category`` liegt die Kategorie der
|
||||||
# (siehe _query_devices) -- daraus folgt der Weg. Uebergeben
|
# *Zugangsdaten* (siehe _query_devices), daraus folgt er.
|
||||||
# statt in run_reboot erraten: dort bedeutet derselbe
|
# Uebergeben statt in run_reboot erraten -- dort bedeutet
|
||||||
# Schluessel je Herkunft etwas anderes.
|
# derselbe Schluessel je Herkunft etwas anderes.
|
||||||
method=(
|
method=weg,
|
||||||
service.METHOD_RPC
|
|
||||||
if str(device["category"]) == inventory.NON_SSH_CATEGORY
|
|
||||||
else service.METHOD_SSH
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
started += 1
|
started += 1
|
||||||
|
|
||||||
@@ -289,3 +331,22 @@ def start_update():
|
|||||||
def start_reboot():
|
def start_reboot():
|
||||||
_start(service.JOB_KIND_REBOOT)
|
_start(service.JOB_KIND_REBOOT)
|
||||||
return redirect(url_for("maintenance.overview"))
|
return redirect(url_for("maintenance.overview"))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/command")
|
||||||
|
@permission_required("servicing_command.execute")
|
||||||
|
@reauth_required
|
||||||
|
def start_command():
|
||||||
|
"""Eine Befehlszeile auf allen ausgewaehlten Clients.
|
||||||
|
|
||||||
|
Dieselbe Schwelle wie die Konsole: eigenes Recht **und** eine frische
|
||||||
|
Passwortbestaetigung. Wer hier etwas eintippt, tut es auf einem Dutzend
|
||||||
|
Maschinen gleichzeitig, und niemand sieht dabei zu.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
befehl = service.befehl_pruefen(request.form.get("befehl", ""))
|
||||||
|
except ValueError as exc:
|
||||||
|
flash(str(exc), "danger")
|
||||||
|
return redirect(url_for("maintenance.overview"))
|
||||||
|
_start(service.JOB_KIND_COMMAND, befehl=befehl)
|
||||||
|
return redirect(url_for("maintenance.overview"))
|
||||||
@@ -0,0 +1,484 @@
|
|||||||
|
"""Bildschirm eines Clients im Browser -- noVNC ueber eine WebSocket-Strecke.
|
||||||
|
|
||||||
|
Warum ueberhaupt im Browser und nicht mit dem Viewer auf dem Arbeitsplatz: der
|
||||||
|
Viewer muss installiert, gepflegt und mit Zugangsdaten versorgt werden, und vom
|
||||||
|
Telefon aus gibt es ihn gar nicht. Hier genuegt die Anmeldung an TESM -- die
|
||||||
|
Rechte, die Protokollierung und die Frage "darf dieser Mensch auf diesen
|
||||||
|
Bildschirm?" haengen ohnehin schon dort.
|
||||||
|
|
||||||
|
**Die Strecke reicht durch, sie versteht nichts.** Der Server oeffnet eine
|
||||||
|
gewoehnliche TCP-Verbindung zum VNC-Server und schaufelt Bytes in beide
|
||||||
|
Richtungen; das RFB-Protokoll samt Authentifizierung handelt der Browser
|
||||||
|
unmittelbar mit dem Geraet aus. Das ist Absicht: eine Gegenstelle, die das
|
||||||
|
Protokoll auspackt, muesste die Verschluesselung aufbrechen und wieder
|
||||||
|
aufbauen -- mehr Code an der empfindlichsten Stelle, ohne dass jemand etwas
|
||||||
|
davon haette.
|
||||||
|
|
||||||
|
**Was daraus folgt** und nicht verschwiegen werden soll: weil der Browser die
|
||||||
|
Anmeldung fuehrt, muss er die Zugangsdaten kennen. Hinterlegte Zugangsdaten
|
||||||
|
gibt TESM deshalb nur an einen Menschen heraus, der Geheimnisse ohnehin sehen
|
||||||
|
darf (``credentials.secrets``) -- jede Herausgabe steht im Aenderungsprotokoll.
|
||||||
|
Wer das Recht nicht hat, tippt sie in der Seite selbst ein; sie gehen dann
|
||||||
|
durch den Browser, aber nie durch unsere Datenbank.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
abort,
|
||||||
|
flash,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
url_for,
|
||||||
|
)
|
||||||
|
from tesm_core import audit
|
||||||
|
from tesm_core.auth.models import current_user, permission_required, reauth_required
|
||||||
|
from tesm_core.auth.service import client_ip, reauth_is_fresh
|
||||||
|
from tesm_core.extension import core, db
|
||||||
|
|
||||||
|
from ..services import inventory, vnc
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
bp = Blueprint("screen", __name__)
|
||||||
|
|
||||||
|
#: Wie lange auf den VNC-Server gewartet wird, bis die Strecke steht.
|
||||||
|
VERBINDUNGSLIMIT = 6.0
|
||||||
|
|
||||||
|
#: Groesse der Haeppchen, in denen vom Geraet gelesen wird. Ein Bildschirm
|
||||||
|
#: liefert in Schueben; kleiner hiesse mehr Runden, groesser mehr Wartezeit,
|
||||||
|
#: bis ein Schub weitergereicht wird.
|
||||||
|
BLOCK = 32 * 1024
|
||||||
|
|
||||||
|
#: Wie oft nachgesehen wird, ob die Sitzung noch etwas tut. Kurz genug, dass
|
||||||
|
#: das Trennen puenktlich ist, lang genug, dass das Nachsehen nichts kostet.
|
||||||
|
TAKT = 1.0
|
||||||
|
|
||||||
|
|
||||||
|
def _geraet(conn: Any, device_id: int) -> dict[str, Any] | None:
|
||||||
|
device = inventory.get_device(conn, device_id)
|
||||||
|
if device is None or device.get("deleted_at"):
|
||||||
|
return None
|
||||||
|
return device
|
||||||
|
|
||||||
|
|
||||||
|
def _erklaerung(fehler: OSError) -> str:
|
||||||
|
"""Die Fehlernummer in einem Satz, der weiterhilft.
|
||||||
|
|
||||||
|
"No route to host" und "Connection refused" sehen im Protokoll gleich
|
||||||
|
harmlos aus und bedeuten das Gegenteil voneinander: einmal kommt man gar
|
||||||
|
nicht hin, einmal kommt man hin und niemand hoert zu. Und beides hat mit
|
||||||
|
einem gelungenen Ping nichts zu tun -- der prueft eine andere Schicht.
|
||||||
|
"""
|
||||||
|
import errno as fehlernummern
|
||||||
|
|
||||||
|
nummer = getattr(fehler, "errno", None)
|
||||||
|
if nummer == fehlernummern.EHOSTUNREACH:
|
||||||
|
return (
|
||||||
|
f"{fehler} – kein Weg dorthin. Eine Firewall oder eine Route steht "
|
||||||
|
"dazwischen; ein gelungener Ping sagt darüber nichts, der prüft eine "
|
||||||
|
"andere Schicht"
|
||||||
|
)
|
||||||
|
if nummer == fehlernummern.ECONNREFUSED:
|
||||||
|
return f"{fehler} – das Gerät ist erreichbar, aber auf diesem Port hört niemand"
|
||||||
|
if nummer == fehlernummern.ETIMEDOUT:
|
||||||
|
return f"{fehler} – keine Antwort; Paketfilter verwerfen meist stillschweigend"
|
||||||
|
return str(fehler)
|
||||||
|
|
||||||
|
|
||||||
|
def _port(conn: Any) -> int:
|
||||||
|
extension = core()
|
||||||
|
if not extension.settings.get(conn, "vnc_enabled"):
|
||||||
|
return 0
|
||||||
|
return int(extension.settings.get(conn, "vnc_port") or vnc.STANDARD_PORT)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/monitoring/clients/<int:device_id>/screen")
|
||||||
|
@permission_required("device_vnc.execute")
|
||||||
|
@reauth_required
|
||||||
|
def screen(device_id: int):
|
||||||
|
"""Die Seite mit der Leinwand.
|
||||||
|
|
||||||
|
Sie beruehrt das Geraet nicht: die Sitzung selbst ist der einzige Anlass,
|
||||||
|
dort anzuklopfen. Was ueber die Anmeldeverfahren bekannt ist, steht in der
|
||||||
|
Datenbank -- aus der letzten ausdruecklichen Pruefung (Knopf "Verfahren
|
||||||
|
pruefen"). Dass diese Seite frueher selbst gemessen hat, war ein Fehler:
|
||||||
|
jede Messung ist eine angefangene und abgebrochene Protokollverhandlung,
|
||||||
|
und ein RealVNC Server zaehlt so etwas -- das Oeffnen der Seite hat der
|
||||||
|
Sitzung also selbst im Weg gestanden.
|
||||||
|
"""
|
||||||
|
conn = db()
|
||||||
|
device = _geraet(conn, device_id)
|
||||||
|
if device is None:
|
||||||
|
abort(404)
|
||||||
|
port = _port(conn)
|
||||||
|
if not port:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
zugang = vnc.zugang_gemerkt(device, port=port)
|
||||||
|
return render_template(
|
||||||
|
"tesm/screen.html",
|
||||||
|
device=device,
|
||||||
|
zugang=zugang,
|
||||||
|
quelle=url_for("screen.session_json", device_id=device_id),
|
||||||
|
darf_geheimnis=bool(current_user.has("credentials.secrets")),
|
||||||
|
verfahren_name=vnc.verfahren_name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _zugangsdaten(conn: Any, device: dict[str, Any]) -> tuple[str, str]:
|
||||||
|
"""Hinterlegte Zugangsdaten -- nur fuer den, der Geheimnisse sehen darf.
|
||||||
|
|
||||||
|
Weil der Browser die Anmeldung fuehrt, muessen sie in die Seite. Wer
|
||||||
|
Geheimnisse nicht sehen darf, bekommt sie auch hier nicht: sonst waere die
|
||||||
|
Bildschirmsitzung der Umweg, auf dem jeder das Passwort liest. Jede
|
||||||
|
Herausgabe steht im Aenderungsprotokoll -- das ist kein Zusehen, sondern
|
||||||
|
ein Zugriff auf etwas, das sonst niemand sieht.
|
||||||
|
"""
|
||||||
|
if not device.get("credential_id") or not current_user.has("credentials.secrets"):
|
||||||
|
return "", ""
|
||||||
|
try:
|
||||||
|
benutzer, passwort = inventory.credential_secret(
|
||||||
|
conn, core().keystore, int(device["credential_id"])
|
||||||
|
)
|
||||||
|
except inventory.ValidationError:
|
||||||
|
return "", ""
|
||||||
|
if passwort:
|
||||||
|
with core().database.transaction(conn):
|
||||||
|
audit.log(
|
||||||
|
conn,
|
||||||
|
actor=current_user.username,
|
||||||
|
actor_id=current_user.id,
|
||||||
|
action="device.vnc_credentials_used",
|
||||||
|
target=str(device["name"]),
|
||||||
|
detail=(
|
||||||
|
f"Hinterlegte Zugangsdaten ({benutzer}) an die Bildschirmsitzung "
|
||||||
|
"übergeben"
|
||||||
|
),
|
||||||
|
ip=client_ip(),
|
||||||
|
severity="warning",
|
||||||
|
)
|
||||||
|
return benutzer, passwort
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/monitoring/clients/<int:device_id>/screen/probe")
|
||||||
|
@permission_required("device_vnc.execute")
|
||||||
|
@reauth_required
|
||||||
|
def probe(device_id: int):
|
||||||
|
"""Fragt das Geraet ausdruecklich, wie es sich authentifizieren will.
|
||||||
|
|
||||||
|
Ein Knopf und keine Nebenwirkung des Seitenaufbaus: die Messung bricht eine
|
||||||
|
Protokollverhandlung ab, und RealVNC zaehlt das. Sie gehoert in die Hand
|
||||||
|
dessen, der gerade sucht -- nicht in jeden Aufruf.
|
||||||
|
"""
|
||||||
|
conn = db()
|
||||||
|
device = _geraet(conn, device_id)
|
||||||
|
if device is None:
|
||||||
|
abort(404)
|
||||||
|
port = _port(conn)
|
||||||
|
if not port:
|
||||||
|
abort(404)
|
||||||
|
|
||||||
|
from ..services import monitor
|
||||||
|
|
||||||
|
zugang = vnc.zugang(device, port=port)
|
||||||
|
with core().database.transaction(conn):
|
||||||
|
monitor.merke_vnc(conn, int(device["id"]), zugang.erreichbar, zugang.ziel)
|
||||||
|
monitor.merke_verfahren(conn, int(device["id"]), zugang.verfahren, zugang.absage)
|
||||||
|
if zugang.absage:
|
||||||
|
flash(f"Der Server antwortet: „{zugang.absage}“", "warning")
|
||||||
|
elif zugang.verfahren:
|
||||||
|
flash(
|
||||||
|
"Angebotene Anmeldeverfahren: "
|
||||||
|
+ ", ".join(vnc.verfahren_name(v) for v in zugang.verfahren),
|
||||||
|
"info",
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
flash(f"{zugang.ziel}:{port} hat nicht geantwortet.", "danger")
|
||||||
|
return redirect(url_for("screen.screen", device_id=device_id))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/monitoring/clients/<int:device_id>/screen/session")
|
||||||
|
@permission_required("device_vnc.execute")
|
||||||
|
def session_json(device_id: int):
|
||||||
|
"""Alles, was der Client fuer eine Sitzung braucht -- als Auskunft.
|
||||||
|
|
||||||
|
Warum nicht in die Seite geschrieben: dieselbe Sitzung laeuft auch im
|
||||||
|
Fenster der Statusuebersicht, und dort stuenden sonst die Zugangsdaten
|
||||||
|
**jedes** Geraets im Quelltext der Seite. So holt sie sich nur, wer eine
|
||||||
|
Sitzung wirklich oeffnet -- und nur fuer das eine Geraet.
|
||||||
|
|
||||||
|
Die frische Passwortbestaetigung wird hier von Hand geprueft statt ueber
|
||||||
|
den Dekorator: der wuerde auf eine Seite umleiten, und eine Umleitung ist
|
||||||
|
fuer einen Abruf im Hintergrund keine Auskunft. 409 mit einem Hinweis ist
|
||||||
|
eine.
|
||||||
|
"""
|
||||||
|
conn = db()
|
||||||
|
device = _geraet(conn, device_id)
|
||||||
|
if device is None:
|
||||||
|
abort(404)
|
||||||
|
port = _port(conn)
|
||||||
|
if not port:
|
||||||
|
abort(404)
|
||||||
|
if not reauth_is_fresh():
|
||||||
|
return {"reauth": True}, 409
|
||||||
|
|
||||||
|
zugang = vnc.zugang_gemerkt(device, port=port)
|
||||||
|
benutzer, passwort = _zugangsdaten(conn, device)
|
||||||
|
return {
|
||||||
|
"name": str(device["name"]),
|
||||||
|
"socket": f"/ws/screen/{device_id}",
|
||||||
|
"ziel": f"{zugang.ziel}:{zugang.port}",
|
||||||
|
"verfahren": list(zugang.verfahren),
|
||||||
|
"bewaehrt": zugang.bewaehrt,
|
||||||
|
"hinderungsgrund": zugang.hinderungsgrund,
|
||||||
|
"meldeweg": url_for("screen.merke_verfahren", device_id=device_id),
|
||||||
|
"vorgaben": {"username": benutzer, "password": passwort},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/monitoring/clients/<int:device_id>/screen/method")
|
||||||
|
@permission_required("device_vnc.execute")
|
||||||
|
def merke_verfahren(device_id: int):
|
||||||
|
"""Der Browser meldet, womit die Sitzung zustande kam.
|
||||||
|
|
||||||
|
Nur er weiss es: die Strecke reicht Bytes durch und liest das Protokoll
|
||||||
|
nicht. Beim naechsten Mal faengt die Seite damit an, statt wieder bei der
|
||||||
|
ersten Wahl des Servers -- die hier schon einmal abgerissen ist und deren
|
||||||
|
Absage sich ein sperrender RealVNC Server Minuten Zeit laesst.
|
||||||
|
"""
|
||||||
|
conn = db()
|
||||||
|
device = _geraet(conn, device_id)
|
||||||
|
if device is None:
|
||||||
|
abort(404)
|
||||||
|
try:
|
||||||
|
verfahren = int(request.json.get("verfahren", 0)) if request.json else 0
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
verfahren = 0
|
||||||
|
if not 0 < verfahren < 256:
|
||||||
|
return {"ok": False}, 400
|
||||||
|
|
||||||
|
from ..services import monitor
|
||||||
|
|
||||||
|
with core().database.transaction(conn):
|
||||||
|
monitor.merke_verfahren_das_traegt(conn, int(device["id"]), verfahren)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
def init_app(app) -> None: # type: ignore[no-untyped-def]
|
||||||
|
"""Meldet Seite und -- falls verfuegbar -- die WebSocket-Strecke an."""
|
||||||
|
app.register_blueprint(bp)
|
||||||
|
|
||||||
|
try:
|
||||||
|
from flask_sock import Sock
|
||||||
|
except ImportError: # pragma: no cover - optionale Abhaengigkeit
|
||||||
|
logger.info("flask-sock fehlt – die Bildschirmsitzung bleibt abgeschaltet.")
|
||||||
|
return
|
||||||
|
|
||||||
|
sock = Sock(app)
|
||||||
|
|
||||||
|
@sock.route("/ws/screen/<int:device_id>")
|
||||||
|
def screen_socket(ws, device_id: int): # type: ignore[no-untyped-def]
|
||||||
|
_strecke(ws, device_id)
|
||||||
|
|
||||||
|
@sock.route("/ws/probe")
|
||||||
|
def probe_socket(ws): # type: ignore[no-untyped-def]
|
||||||
|
"""Sagt "da", und mehr nicht.
|
||||||
|
|
||||||
|
Wozu: bricht eine Bildschirmsitzung ab, gibt es zwei sehr verschiedene
|
||||||
|
Ursachen -- der VNC-Server mag nicht, oder die WebSocket-Verbindung
|
||||||
|
kommt gar nicht erst durch (ein Proxy zwischen Browser und TESM, der
|
||||||
|
"Upgrade" nicht weiterreicht, ist der haeufigste Fall). Von aussen
|
||||||
|
sehen beide gleich aus: ein Abbruch ohne Grund. Diese Probe trennt
|
||||||
|
sie, ohne dass jemand ein Protokoll lesen muss.
|
||||||
|
"""
|
||||||
|
if not current_user.is_authenticated:
|
||||||
|
ws.close(1008, "Nicht angemeldet.")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
ws.send("da")
|
||||||
|
ws.receive(timeout=2)
|
||||||
|
except Exception: # noqa: BLE001 - der Browser ist weg, das ist keine Stoerung
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _strecke(ws, device_id: int) -> None: # type: ignore[no-untyped-def]
|
||||||
|
"""Bytes hin, Bytes her -- und sonst nichts.
|
||||||
|
|
||||||
|
Die Pruefungen stehen hier ausdruecklich noch einmal: ein WebSocket geht an
|
||||||
|
den Dekoratoren der HTTP-Route vorbei.
|
||||||
|
"""
|
||||||
|
extension = core()
|
||||||
|
conn = db()
|
||||||
|
|
||||||
|
if not current_user.is_authenticated or not current_user.has("device_vnc.execute"):
|
||||||
|
ws.close(1008, "Keine Berechtigung für den Bildschirm dieses Clients.")
|
||||||
|
return
|
||||||
|
if not reauth_is_fresh():
|
||||||
|
ws.close(1008, "Die Passwortbestätigung ist abgelaufen.")
|
||||||
|
return
|
||||||
|
port = _port(conn)
|
||||||
|
if not port:
|
||||||
|
ws.close(1008, "Der Bildschirmzugriff ist in dieser Installation abgeschaltet.")
|
||||||
|
return
|
||||||
|
device = _geraet(conn, device_id)
|
||||||
|
if device is None:
|
||||||
|
ws.close(1008, "Client nicht gefunden.")
|
||||||
|
return
|
||||||
|
host = vnc.ziel(device)
|
||||||
|
if not host:
|
||||||
|
ws.close(1008, "Für diesen Client ist kein Ziel bekannt.")
|
||||||
|
return
|
||||||
|
|
||||||
|
gegenstelle = None
|
||||||
|
ausweich = str(device.get("ip") or "").strip()
|
||||||
|
for versuch, ziel_host in enumerate([host] + ([ausweich] if ausweich != host else [])):
|
||||||
|
try:
|
||||||
|
gegenstelle = socket.create_connection((ziel_host, port), timeout=VERBINDUNGSLIMIT)
|
||||||
|
if versuch:
|
||||||
|
# Der Name fuehrte ins Leere, die Adresse nicht. Das ist eine
|
||||||
|
# Auskunft ueber das Netz, keine Nebensaechlichkeit: irgendwo
|
||||||
|
# zeigt ein Eintrag woandershin.
|
||||||
|
if live_log := extension.registry.get("live_log"):
|
||||||
|
live_log.warning(
|
||||||
|
"vnc",
|
||||||
|
f"{host} war nicht erreichbar – Sitzung läuft über {ziel_host}",
|
||||||
|
)
|
||||||
|
host = ziel_host
|
||||||
|
break
|
||||||
|
except OSError as exc:
|
||||||
|
letzter = exc
|
||||||
|
if gegenstelle is None:
|
||||||
|
ws.close(1011, f"{host}:{port} antwortet nicht ({_erklaerung(letzter)}).")
|
||||||
|
return
|
||||||
|
# Das Zeitlimit galt dem *Aufbau* -- als Abbruchgrund taugt es nicht.
|
||||||
|
#
|
||||||
|
# ``create_connection`` setzt es an den Socket, und dort gilt es fuer jede
|
||||||
|
# weitere Leseoperation. Ein Bildschirm, auf dem sich gerade nichts tut,
|
||||||
|
# schickt sekundenlang nichts; nach sechs Sekunden Stille brach das Lesen
|
||||||
|
# mit einer Zeitueberschreitung ab, und die Sitzung war weg. Genau so
|
||||||
|
# beobachtet: Bild steht, nach fuenf bis zehn Sekunden Schluss.
|
||||||
|
#
|
||||||
|
# Stattdessen ein kurzer Takt: das Lesen laeuft immer wieder in ein
|
||||||
|
# Zeitlimit, und *das* ist kein Abbruch, sondern der Anlass nachzusehen,
|
||||||
|
# ob die Sitzung ueberhaupt noch etwas tut.
|
||||||
|
# Ohne das sammelt der Kern kleine Pakete, bis genug beisammen ist
|
||||||
|
# (Nagle) -- bei einem Bildschirm sind Tastendruck und Mausbewegung genau
|
||||||
|
# solche kleinen Pakete, und das Warten darauf faellt als Ruckeln auf.
|
||||||
|
gegenstelle.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||||
|
gegenstelle.settimeout(TAKT)
|
||||||
|
leerlauf = int(extension.settings.get(conn, "vnc_idle_timeout") or 0)
|
||||||
|
|
||||||
|
# Ins **laufende** Protokoll, nicht in die Aenderungskette: eine
|
||||||
|
# Bildschirmsitzung aendert nichts an der Anlage. Das Aenderungsprotokoll
|
||||||
|
# soll beantworten, wer was umgestellt hat -- wer wann zugesehen hat,
|
||||||
|
# gehoert nicht in dieselbe Liste und macht sie in einem Betrieb mit
|
||||||
|
# ein paar Dutzend Geraeten unlesbar. Sichtbar ist die Sitzung trotzdem,
|
||||||
|
# solange sie laeuft.
|
||||||
|
begonnen = time.monotonic()
|
||||||
|
live = extension.registry.get("live_log")
|
||||||
|
if live is not None:
|
||||||
|
live.info("vnc", f"{current_user.username} öffnet den Bildschirm von {device['name']}")
|
||||||
|
|
||||||
|
laeuft = threading.Event()
|
||||||
|
laeuft.set()
|
||||||
|
#: Wann zuletzt etwas floss -- in *irgendeine* Richtung. Eine Liste, weil
|
||||||
|
#: beide Richtungen daran schreiben und eine Zuweisung in Python nicht in
|
||||||
|
#: die andere hineinlaufen kann.
|
||||||
|
zuletzt = [time.monotonic()]
|
||||||
|
#: Grund des Endes, falls wir selbst getrennt haben.
|
||||||
|
schluss = [""]
|
||||||
|
#: Kam ueberhaupt schon ein Byte vom Geraet?
|
||||||
|
gehoert = [False]
|
||||||
|
|
||||||
|
def vom_geraet() -> None:
|
||||||
|
"""Alles, was der VNC-Server schickt, geht als Binaerrahmen weiter."""
|
||||||
|
try:
|
||||||
|
while laeuft.is_set():
|
||||||
|
try:
|
||||||
|
daten = gegenstelle.recv(BLOCK)
|
||||||
|
except socket.timeout:
|
||||||
|
# Kein Fehler, nur Stille -- und die bedeutet zweierlei,
|
||||||
|
# je nachdem, ob die Sitzung schon stand.
|
||||||
|
#
|
||||||
|
# **Vor dem ersten Byte** ist Stille ein Befund: der Port
|
||||||
|
# nimmt an, aber es kommt nichts. Genau so verhaelt sich
|
||||||
|
# ein RealVNC Server, der die Adresse gerade aussperrt. Wer
|
||||||
|
# das nicht erfaehrt, sieht ewig "Verbindung wird
|
||||||
|
# aufgebaut" -- und sucht den Fehler bei uns.
|
||||||
|
if not gehoert[0] and time.monotonic() - begonnen > VERBINDUNGSLIMIT:
|
||||||
|
schluss[0] = (
|
||||||
|
f"{host}:{port} nimmt die Verbindung an, schickt aber nichts. "
|
||||||
|
"So verhält sich ein VNC-Server, der die Adresse gerade "
|
||||||
|
"aussperrt (RealVNC: BlacklistTimeout) – ein Neustart des "
|
||||||
|
"VNC-Dienstes auf dem Gerät räumt das ab."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
# **Danach** ist Stille der Normalfall: ein Bildschirm, auf
|
||||||
|
# dem sich nichts tut, schickt nichts.
|
||||||
|
if gehoert[0] and leerlauf and time.monotonic() - zuletzt[0] > leerlauf:
|
||||||
|
schluss[0] = (
|
||||||
|
f"Sitzung nach {leerlauf} Sekunden ohne Bewegung getrennt"
|
||||||
|
)
|
||||||
|
break
|
||||||
|
continue
|
||||||
|
if not daten:
|
||||||
|
if not gehoert[0]:
|
||||||
|
schluss[0] = (
|
||||||
|
f"{host}:{port} hat die Verbindung sofort wieder geschlossen."
|
||||||
|
)
|
||||||
|
break
|
||||||
|
gehoert[0] = True
|
||||||
|
zuletzt[0] = time.monotonic()
|
||||||
|
ws.send(daten)
|
||||||
|
except Exception: # noqa: BLE001 - Abbruch ist der Normalfall, nicht die Stoerung
|
||||||
|
pass
|
||||||
|
finally:
|
||||||
|
laeuft.clear()
|
||||||
|
try:
|
||||||
|
if schluss[0]:
|
||||||
|
ws.close(1000 if gehoert[0] else 1011, schluss[0])
|
||||||
|
else:
|
||||||
|
ws.close()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
leser = threading.Thread(target=vom_geraet, name=f"vnc-{device_id}", daemon=True)
|
||||||
|
leser.start()
|
||||||
|
|
||||||
|
try:
|
||||||
|
while laeuft.is_set():
|
||||||
|
nachricht = ws.receive()
|
||||||
|
if nachricht is None:
|
||||||
|
break
|
||||||
|
if isinstance(nachricht, str):
|
||||||
|
# Der Browser-Client schickt ausschliesslich Binaerrahmen. Text
|
||||||
|
# waere entweder ein Irrtum oder ein Versuch -- beides endet
|
||||||
|
# hier und nicht auf dem Geraet.
|
||||||
|
continue
|
||||||
|
zuletzt[0] = time.monotonic()
|
||||||
|
gegenstelle.sendall(nachricht)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
logger.debug("Bildschirmsitzung beendet (%s)", device["name"], exc_info=True)
|
||||||
|
finally:
|
||||||
|
laeuft.clear()
|
||||||
|
try:
|
||||||
|
gegenstelle.shutdown(socket.SHUT_RDWR)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
gegenstelle.close()
|
||||||
|
dauer = int(time.monotonic() - begonnen)
|
||||||
|
if live is not None:
|
||||||
|
live.info(
|
||||||
|
"vnc",
|
||||||
|
f"Bildschirmsitzung {device['name']} beendet ({dauer}s)"
|
||||||
|
+ (f" – {schluss[0]}" if schluss[0] else ""),
|
||||||
|
)
|
||||||
@@ -105,6 +105,8 @@ def system():
|
|||||||
"dashboard_public",
|
"dashboard_public",
|
||||||
"vnc_enabled",
|
"vnc_enabled",
|
||||||
"vnc_port",
|
"vnc_port",
|
||||||
|
"vnc_idle_timeout",
|
||||||
|
"vnc_warn_unencrypted_off",
|
||||||
"terminal_switches_enabled",
|
"terminal_switches_enabled",
|
||||||
"terminal_clients_enabled",
|
"terminal_clients_enabled",
|
||||||
"terminal_idle_timeout",
|
"terminal_idle_timeout",
|
||||||
@@ -147,6 +149,8 @@ def system_save():
|
|||||||
"dashboard_public",
|
"dashboard_public",
|
||||||
"vnc_enabled",
|
"vnc_enabled",
|
||||||
"vnc_port",
|
"vnc_port",
|
||||||
|
"vnc_idle_timeout",
|
||||||
|
"vnc_warn_unencrypted_off",
|
||||||
"terminal_switches_enabled",
|
"terminal_switches_enabled",
|
||||||
"terminal_clients_enabled",
|
"terminal_clients_enabled",
|
||||||
"terminal_idle_timeout",
|
"terminal_idle_timeout",
|
||||||
|
|||||||
@@ -76,6 +76,16 @@ CLIENT_COMMANDS: tuple[tuple[str, str], ...] = (
|
|||||||
("journalctl -p err -n 50 --no-pager", "Letzte Fehler im Journal"),
|
("journalctl -p err -n 50 --no-pager", "Letzte Fehler im Journal"),
|
||||||
("apt list --upgradable 2>/dev/null", "Verfügbare Aktualisierungen"),
|
("apt list --upgradable 2>/dev/null", "Verfügbare Aktualisierungen"),
|
||||||
("uname -a", "Kernel und Architektur"),
|
("uname -a", "Kernel und Architektur"),
|
||||||
|
# Die beiden schreiben -- deshalb stehen sie am Ende und sagen es im Namen.
|
||||||
|
# Eine Vorlage fuellt nur das Eingabefeld; abgeschickt wird von Hand.
|
||||||
|
(
|
||||||
|
'echo "Encryption=AlwaysOff" | sudo tee -a /etc/vnc/config.d/common.custom',
|
||||||
|
"VNC: Streckenverschlüsselung abschalten (schreibt)",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"sudo systemctl restart vncserver-x11-serviced",
|
||||||
|
"VNC-Dienst neu starten (schreibt)",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ NAVIGATION = NavTree(
|
|||||||
children=(
|
children=(
|
||||||
NavItem(
|
NavItem(
|
||||||
key="logs_live",
|
key="logs_live",
|
||||||
label="Laufend",
|
label="Live",
|
||||||
icon="terminal",
|
icon="terminal",
|
||||||
endpoint="logs.live",
|
endpoint="logs.live",
|
||||||
permissions=("logs_live.read",),
|
permissions=("logs_live.read",),
|
||||||
|
|||||||
@@ -187,6 +187,24 @@ SERVICING_TOOL = Resource(
|
|||||||
action_hints={A.EXECUTE: "Installation auslösen."},
|
action_hints={A.EXECUTE: "Installation auslösen."},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: Ein freier Befehl auf vielen Geraeten ist das Maechtigste, was diese
|
||||||
|
#: Anwendung kann -- und das Einzige, dessen Wirkung sie nicht kennt. Deshalb
|
||||||
|
#: ein eigenes Recht, das niemand nebenbei bekommt: wer Pakete aktualisieren
|
||||||
|
#: oder neu starten darf, tut etwas Umrissenes; wer hier etwas eintippt, tut
|
||||||
|
#: alles, was ein Konto auf dem Geraet tun kann. Die Vorlagen nehmen dem den
|
||||||
|
#: Alltag ab, nicht die Verantwortung.
|
||||||
|
SERVICING_COMMAND = Resource(
|
||||||
|
key="servicing_command",
|
||||||
|
label="Befehl auf mehreren Geräten",
|
||||||
|
actions=(A.READ, A.EXECUTE),
|
||||||
|
hint="Eine Befehlszeile über SSH, auf allen ausgewählten Clients",
|
||||||
|
action_hints={
|
||||||
|
A.READ: "Die Karte sehen – und damit, was zuletzt lief.",
|
||||||
|
A.EXECUTE: "Befehl absenden. Verlangt eine frische Passwortbestätigung; "
|
||||||
|
"jeder Auftrag steht mit seinem Wortlaut im Änderungsprotokoll.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
#: Die Wartungsseite zeigt alle drei Aufgaben. Sie oeffnet, wer **eine** davon
|
#: Die Wartungsseite zeigt alle drei Aufgaben. Sie oeffnet, wer **eine** davon
|
||||||
#: sehen darf -- und zeigt dann nur die. Als Konstante, damit die vier Routen
|
#: sehen darf -- und zeigt dann nur die. Als Konstante, damit die vier Routen
|
||||||
#: dieselbe Antwort geben; einzeln getippt liefen sie irgendwann auseinander.
|
#: dieselbe Antwort geben; einzeln getippt liefen sie irgendwann auseinander.
|
||||||
@@ -194,6 +212,7 @@ SERVICING_PAGE: tuple[str, ...] = (
|
|||||||
"servicing_update.read",
|
"servicing_update.read",
|
||||||
"servicing_reboot.read",
|
"servicing_reboot.read",
|
||||||
"servicing_tool.read",
|
"servicing_tool.read",
|
||||||
|
"servicing_command.read",
|
||||||
)
|
)
|
||||||
|
|
||||||
#: Elf Funktionen hingen an ``dhcp.edit``: Grundeinstellungen, Subnetze,
|
#: Elf Funktionen hingen an ``dhcp.edit``: Grundeinstellungen, Subnetze,
|
||||||
@@ -245,6 +264,31 @@ DHCP_SERVICE = Resource(
|
|||||||
action_hints={A.EXECUTE: "Dienstzustand ändern."},
|
action_hints={A.EXECUTE: "Dienstzustand ändern."},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: Eine dynamische Lease in einen Client verwandeln.
|
||||||
|
#:
|
||||||
|
#: Eigene Zeile, weil es eine eigene Entscheidung ist: hier wird aus einer
|
||||||
|
#: Beobachtung ("dieses Geraet hat sich eine Adresse geholt") ein gepflegter
|
||||||
|
#: Datensatz, den die Ueberwachung ab sofort anpingt und der beim naechsten
|
||||||
|
#: Schreiben der Konfiguration eine feste Adresse bekommt. Wer Leases sehen
|
||||||
|
#: darf, soll deshalb nicht automatisch welche uebernehmen duerfen.
|
||||||
|
#:
|
||||||
|
#: **Und es reicht allein nicht.** Die Uebernahme legt einen Client an, also
|
||||||
|
#: verlangt die Route zusaetzlich "Clients: Anlegen". Sonst waere dies eine
|
||||||
|
#: Hintertuer an der Geraeteverwaltung vorbei -- dieselbe Wirkung, anderes
|
||||||
|
#: Formular. Beide Haken zusammen, oder die Schaltflaeche erscheint nicht.
|
||||||
|
DHCP_LEASE_ADOPT = Resource(
|
||||||
|
key="dhcp_lease_adopt",
|
||||||
|
parent="dhcp_subnets",
|
||||||
|
label="DHCP: Lease als Client übernehmen",
|
||||||
|
actions=(A.READ, A.EXECUTE),
|
||||||
|
hint="Aus einer dynamisch vergebenen Adresse einen überwachten Client machen",
|
||||||
|
action_hints={
|
||||||
|
A.READ: "Die Schaltfläche an dynamischen Leases sehen.",
|
||||||
|
A.EXECUTE: "Client aus der Lease anlegen. Verlangt zusätzlich "
|
||||||
|
"'Clients: Anlegen' – es entsteht ein Gerätedatensatz.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
#: Die DHCP-Seite zeigt alle Abschnitte; jeder Abschnitt haengt an seiner
|
#: Die DHCP-Seite zeigt alle Abschnitte; jeder Abschnitt haengt an seiner
|
||||||
#: eigenen Zeile. Geoeffnet wird sie, wer einen davon sehen darf.
|
#: eigenen Zeile. Geoeffnet wird sie, wer einen davon sehen darf.
|
||||||
DHCP_PAGE: tuple[str, ...] = (
|
DHCP_PAGE: tuple[str, ...] = (
|
||||||
@@ -354,14 +398,21 @@ MAINTENANCE_AREA = Area(
|
|||||||
icon="wrench",
|
icon="wrench",
|
||||||
hint="Paketpflege und Neustarts. Ohne diesen Zugang bleibt der Bereich "
|
hint="Paketpflege und Neustarts. Ohne diesen Zugang bleibt der Bereich "
|
||||||
"verborgen – auch wenn einzelne Rechte darunter gesetzt sind.",
|
"verborgen – auch wenn einzelne Rechte darunter gesetzt sind.",
|
||||||
resources=(SERVICING_UPDATE, SERVICING_REBOOT, SERVICING_TOOL),
|
resources=(SERVICING_UPDATE, SERVICING_REBOOT, SERVICING_COMMAND, SERVICING_TOOL),
|
||||||
)
|
)
|
||||||
|
|
||||||
NETWORK_AREA = Area(
|
NETWORK_AREA = Area(
|
||||||
key="network",
|
key="network",
|
||||||
label="Netzwerkdienste",
|
label="Netzwerkdienste",
|
||||||
icon="network",
|
icon="network",
|
||||||
resources=(DHCP_SUBNETS, DHCP_OPTIONS, DHCP_RESERVATIONS, DHCP_CONFIG, DHCP_SERVICE),
|
resources=(
|
||||||
|
DHCP_SUBNETS,
|
||||||
|
DHCP_OPTIONS,
|
||||||
|
DHCP_RESERVATIONS,
|
||||||
|
DHCP_LEASE_ADOPT,
|
||||||
|
DHCP_CONFIG,
|
||||||
|
DHCP_SERVICE,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
FILES_AREA = Area(
|
FILES_AREA = Area(
|
||||||
|
|||||||
@@ -515,6 +515,43 @@ ALTER TABLE device_status ADD COLUMN vnc_target TEXT NOT NULL DEFAULT '';
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Version 12 ----------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Welche Sicherheitsverfahren der VNC-Server angeboten hat. Daran entscheidet
|
||||||
|
# sich, ob eine Sitzung im Browser ueberhaupt zustande kommen kann -- und wer
|
||||||
|
# das erst erfaehrt, wenn er vor einer schwarzen Flaeche sitzt, sucht den
|
||||||
|
# Fehler bei uns.
|
||||||
|
V12 = """
|
||||||
|
ALTER TABLE device_status ADD COLUMN vnc_types TEXT NOT NULL DEFAULT '';
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Version 13 ----------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Die Absage eines VNC-Servers und der Zeitpunkt der Verfahrensmessung. Beide
|
||||||
|
# gehoeren getrennt vom Ueberwachungsdurchlauf gefuehrt: der fragt nur noch,
|
||||||
|
# ob der Port annimmt (eine abgebrochene Protokollverhandlung sperrt bei
|
||||||
|
# RealVNC die eigene Adresse aus), die Verfahren misst die Seite -- und auch
|
||||||
|
# die nur, wenn die letzte Messung alt ist.
|
||||||
|
V13 = """
|
||||||
|
ALTER TABLE device_status ADD COLUMN vnc_reason TEXT NOT NULL DEFAULT '';
|
||||||
|
ALTER TABLE device_status ADD COLUMN vnc_types_at TEXT;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Version 14 ----------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Welches Anmeldeverfahren zuletzt wirklich getragen hat. Der Server nennt
|
||||||
|
# seine Reihenfolge, und die gilt -- aber wenn seine erste Wahl schon einmal
|
||||||
|
# abgerissen ist und eine andere zum Bild gefuehrt hat, ist das ein Befund.
|
||||||
|
# Ihn zu vergessen kostet bei jedem Oeffnen einen Fehlversuch, und RealVNC
|
||||||
|
# laesst sich mit der Absage Zeit: gemessen zwei bis drei Minuten, bis das
|
||||||
|
# Bild stand.
|
||||||
|
V14 = """
|
||||||
|
ALTER TABLE device_status ADD COLUMN vnc_method INTEGER NOT NULL DEFAULT 0;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
def uebergang_standardgruppen(conn) -> None: # type: ignore[no-untyped-def]
|
def uebergang_standardgruppen(conn) -> None: # type: ignore[no-untyped-def]
|
||||||
"""Aus sieben Standardgruppen werden drei -- ohne jemandem etwas wegzunehmen.
|
"""Aus sieben Standardgruppen werden drei -- ohne jemandem etwas wegzunehmen.
|
||||||
|
|
||||||
@@ -559,4 +596,7 @@ TESM_MIGRATIONS: tuple[Migration, ...] = (
|
|||||||
Migration(version=9, name="standardgruppen-aufraeumen", fn=uebergang_standardgruppen),
|
Migration(version=9, name="standardgruppen-aufraeumen", fn=uebergang_standardgruppen),
|
||||||
Migration(version=10, name="vnc-erreichbarkeit", sql=V10),
|
Migration(version=10, name="vnc-erreichbarkeit", sql=V10),
|
||||||
Migration(version=11, name="vnc-ziel", sql=V11),
|
Migration(version=11, name="vnc-ziel", sql=V11),
|
||||||
|
Migration(version=12, name="vnc-verfahren", sql=V12),
|
||||||
|
Migration(version=13, name="vnc-absage", sql=V13),
|
||||||
|
Migration(version=14, name="vnc-verfahren-das-traegt", sql=V14),
|
||||||
)
|
)
|
||||||
@@ -543,7 +543,10 @@ def list_devices(
|
|||||||
COALESCE(st.state,'unknown') AS state, st.latency_ms, st.last_check_at,
|
COALESCE(st.state,'unknown') AS state, st.latency_ms, st.last_check_at,
|
||||||
st.last_ok_at, st.last_change_at, st.last_restart_at, st.restart_count,
|
st.last_ok_at, st.last_change_at, st.last_restart_at, st.restart_count,
|
||||||
st.fail_count, COALESCE(st.vnc_open, 0) AS vnc_open, st.vnc_checked_at,
|
st.fail_count, COALESCE(st.vnc_open, 0) AS vnc_open, st.vnc_checked_at,
|
||||||
COALESCE(st.vnc_target, '') AS vnc_target
|
COALESCE(st.vnc_target, '') AS vnc_target,
|
||||||
|
COALESCE(st.vnc_types, '') AS vnc_types,
|
||||||
|
COALESCE(st.vnc_reason, '') AS vnc_reason, st.vnc_types_at,
|
||||||
|
COALESCE(st.vnc_method, 0) AS vnc_method
|
||||||
FROM devices d
|
FROM devices d
|
||||||
LEFT JOIN switches s ON s.id = d.switch_id AND s.deleted_at IS NULL
|
LEFT JOIN switches s ON s.id = d.switch_id AND s.deleted_at IS NULL
|
||||||
LEFT JOIN credentials c ON c.id = d.credential_id AND c.deleted_at IS NULL
|
LEFT JOIN credentials c ON c.id = d.credential_id AND c.deleted_at IS NULL
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ import sqlite3
|
|||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timedelta, timezone
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable
|
||||||
|
|
||||||
from tesm_core import audit
|
from tesm_core import audit
|
||||||
@@ -44,6 +44,51 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
JOB_KIND_UPDATE = "maintenance.update"
|
JOB_KIND_UPDATE = "maintenance.update"
|
||||||
JOB_KIND_REBOOT = "maintenance.reboot"
|
JOB_KIND_REBOOT = "maintenance.reboot"
|
||||||
|
JOB_KIND_COMMAND = "maintenance.command"
|
||||||
|
|
||||||
|
#: Vorlagen fuer den Massenbefehl: Name, Befehl, Erklaerung.
|
||||||
|
#:
|
||||||
|
#: ``{sudo}`` wird zur Laufzeit ersetzt -- mit ``sudo -n``, wo es ohne Passwort
|
||||||
|
#: geht, sonst mit ``sudo -S`` und dem hinterlegten Passwort ueber stdin. Wer
|
||||||
|
#: eine Vorlage waehlt, sieht den Befehl im Feld und kann ihn aendern: eine
|
||||||
|
#: Vorlage ist ein Vorschlag, kein Automat.
|
||||||
|
BEFEHLSVORLAGEN: tuple[tuple[str, str, str], ...] = (
|
||||||
|
(
|
||||||
|
"VNC: Streckenverschlüsselung abschalten",
|
||||||
|
'echo "Encryption=AlwaysOff" | {sudo} tee -a /etc/vnc/config.d/common.custom',
|
||||||
|
"Danach bietet RealVNC auch RA2ne und „ohne Authentifizierung“ an – nur damit "
|
||||||
|
"kommt eine Sitzung im Browser zustande. Die Strecke ist dann unverschlüsselt; "
|
||||||
|
"im Netz zwischen Server und Gerät ist sie es ohnehin.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"VNC-Dienst neu starten",
|
||||||
|
"{sudo} systemctl restart vncserver-x11-serviced",
|
||||||
|
"Räumt auch eine Sperre ab, die der Server nach zu vielen abgebrochenen "
|
||||||
|
"Verbindungsversuchen gesetzt hat.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"VNC: aktuelle Einstellungen zeigen",
|
||||||
|
"cat /etc/vnc/config.d/common.custom 2>/dev/null || echo '(keine eigene Konfiguration)'",
|
||||||
|
"Liest nur – gut, um vor dem Ändern zu sehen, was schon dasteht.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Zeit und Zeitzone",
|
||||||
|
"timedatectl",
|
||||||
|
"Liest nur.",
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"Belegung der Dateisysteme",
|
||||||
|
"df -h",
|
||||||
|
"Liest nur.",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
#: Was in einer Befehlszeile nichts zu suchen hat. Nicht als Schutz vor einem
|
||||||
|
#: Menschen mit diesem Recht -- er darf ohnehin alles, was sein Konto auf dem
|
||||||
|
#: Geraet darf --, sondern gegen Unfaelle: ein Zeilenumbruch aus der
|
||||||
|
#: Zwischenablage macht aus einem Befehl unbemerkt zwei.
|
||||||
|
VERBOTENE_ZEICHEN = ("\n", "\r", "\x00")
|
||||||
|
MAX_BEFEHLSLAENGE = 500
|
||||||
|
|
||||||
MAX_OUTPUT_LINES = 400
|
MAX_OUTPUT_LINES = 400
|
||||||
CONNECT_TIMEOUT = 25
|
CONNECT_TIMEOUT = 25
|
||||||
@@ -231,16 +276,89 @@ def update_job(
|
|||||||
|
|
||||||
|
|
||||||
def list_jobs(
|
def list_jobs(
|
||||||
conn: sqlite3.Connection, *, kinds: tuple[str, ...] = (JOB_KIND_UPDATE, JOB_KIND_REBOOT), limit: int = 60
|
conn: sqlite3.Connection,
|
||||||
|
*,
|
||||||
|
kinds: tuple[str, ...] = (JOB_KIND_UPDATE, JOB_KIND_REBOOT, JOB_KIND_COMMAND),
|
||||||
|
limit: int = 60,
|
||||||
) -> list[dict[str, Any]]:
|
) -> list[dict[str, Any]]:
|
||||||
placeholders = ",".join("?" * len(kinds))
|
placeholders = ",".join("?" * len(kinds))
|
||||||
return Database.all(
|
return Database.all(
|
||||||
conn,
|
conn,
|
||||||
f"SELECT * FROM jobs WHERE kind IN ({placeholders}) ORDER BY created_at DESC LIMIT ?",
|
# ``rowid`` als zweites Ordnungsmerkmal: ``created_at`` hat
|
||||||
|
# Sekundenaufloesung, und ein Massenbefehl legt vierzig Auftraege in
|
||||||
|
# derselben Sekunde an. Ohne das entschied bei Gleichstand die
|
||||||
|
# Speicherreihenfolge -- die Liste sah bei jedem Aufruf anders aus.
|
||||||
|
f"SELECT * FROM jobs WHERE kind IN ({placeholders}) "
|
||||||
|
"ORDER BY created_at DESC, rowid DESC LIMIT ?",
|
||||||
(*kinds, limit),
|
(*kinds, limit),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def laufende_und_letzte(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
*,
|
||||||
|
kinds: tuple[str, ...] = (JOB_KIND_UPDATE, JOB_KIND_REBOOT, JOB_KIND_COMMAND),
|
||||||
|
limit: int = 30,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
"""Alles, was laeuft oder eben fertig wurde -- und dazu die letzten Auftraege.
|
||||||
|
|
||||||
|
Fuer die Statusabfrage der Wartungsseite. Eine feste Obergrenze allein
|
||||||
|
reicht dort nicht, und das war ein echter Fehler: ein Massenbefehl auf
|
||||||
|
vierzig Geraete legt vierzig Auftraege an, ``LIMIT 30`` schnitt zehn davon
|
||||||
|
ab -- und weil alle denselben Zeitstempel auf die Sekunde tragen, waren es
|
||||||
|
zehn willkuerliche. Auf der Seite sah das aus, als haetten die letzten
|
||||||
|
Geraete den Befehl nie bekommen: sie liefen, ihre Ausgabe kam nur nie an.
|
||||||
|
|
||||||
|
Was laeuft, faellt hier deshalb nie heraus, und was gerade fertig wurde,
|
||||||
|
behaelt seine letzten Zeilen so lange wie ``eben_fertig`` es zusagt.
|
||||||
|
"""
|
||||||
|
platzhalter = ",".join("?" * len(kinds))
|
||||||
|
schwelle = (
|
||||||
|
datetime.now(timezone.utc) - timedelta(seconds=EBEN_FERTIG_SEKUNDEN)
|
||||||
|
).isoformat(timespec="seconds")
|
||||||
|
gesammelt: dict[str, dict[str, Any]] = {}
|
||||||
|
for sql, params in (
|
||||||
|
(
|
||||||
|
f"SELECT * FROM jobs WHERE kind IN ({platzhalter}) "
|
||||||
|
"AND state IN ('pending','running') ORDER BY created_at DESC, rowid DESC",
|
||||||
|
(*kinds,),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
f"SELECT * FROM jobs WHERE kind IN ({platzhalter}) "
|
||||||
|
"AND COALESCE(finished_at, updated_at) >= ? ORDER BY created_at DESC, rowid DESC",
|
||||||
|
(*kinds, schwelle),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
f"SELECT * FROM jobs WHERE kind IN ({platzhalter}) "
|
||||||
|
"ORDER BY created_at DESC, rowid DESC LIMIT ?",
|
||||||
|
(*kinds, limit),
|
||||||
|
),
|
||||||
|
):
|
||||||
|
for job in Database.all(conn, sql, params):
|
||||||
|
gesammelt.setdefault(str(job["id"]), job)
|
||||||
|
return sorted(
|
||||||
|
gesammelt.values(), key=lambda job: str(job["created_at"] or ""), reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def letzter_auftrag_je_geraet(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
*,
|
||||||
|
kinds: tuple[str, ...] = (JOB_KIND_UPDATE, JOB_KIND_REBOOT, JOB_KIND_COMMAND),
|
||||||
|
limit: int = 400,
|
||||||
|
) -> dict[str, dict[str, Any]]:
|
||||||
|
"""Zu jedem Geraetenamen sein neuester Auftrag.
|
||||||
|
|
||||||
|
Damit die Wartungsseite schon beim Aufbau zeigt, was wo laeuft -- und nicht
|
||||||
|
erst, wenn die erste Statusabfrage zurueck ist. Der Schluessel ist der
|
||||||
|
Name, denn genau den traegt ein Auftrag als ``subject``.
|
||||||
|
"""
|
||||||
|
stand: dict[str, dict[str, Any]] = {}
|
||||||
|
for job in list_jobs(conn, kinds=kinds, limit=limit):
|
||||||
|
stand.setdefault(str(job["subject"]), job)
|
||||||
|
return stand
|
||||||
|
|
||||||
|
|
||||||
def get_job(conn: sqlite3.Connection, job_id: str) -> dict[str, Any] | None:
|
def get_job(conn: sqlite3.Connection, job_id: str) -> dict[str, Any] | None:
|
||||||
return Database.one(conn, "SELECT * FROM jobs WHERE id=?", (job_id,))
|
return Database.one(conn, "SELECT * FROM jobs WHERE id=?", (job_id,))
|
||||||
|
|
||||||
@@ -447,6 +565,149 @@ def run_update(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: So lange gilt ein Auftrag als "eben fertig geworden" und schickt seine
|
||||||
|
#: letzten Zeilen noch mit. Ohne das verschwaende die Ausgabe genau in dem
|
||||||
|
#: Augenblick, in dem sie interessant wird: wenn der Befehl durch ist.
|
||||||
|
EBEN_FERTIG_SEKUNDEN = 120
|
||||||
|
|
||||||
|
#: So viele Zeilen gehen ins Mitlesen. Genug, um zu sehen, was gerade
|
||||||
|
#: passiert; wenig genug, dass eine Abfrage alle zwei Sekunden nichts kostet.
|
||||||
|
MITLESEN_ZEILEN = 40
|
||||||
|
|
||||||
|
|
||||||
|
def letzte_zeilen(ausgabe: str, anzahl: int = MITLESEN_ZEILEN) -> str:
|
||||||
|
"""Das Ende einer Ausgabe -- das ist der Teil, der gerade entsteht."""
|
||||||
|
zeilen = [zeile for zeile in ausgabe.splitlines() if zeile.strip()]
|
||||||
|
return "\n".join(zeilen[-anzahl:])
|
||||||
|
|
||||||
|
|
||||||
|
def eben_fertig(job: dict[str, Any], sekunden: int = EBEN_FERTIG_SEKUNDEN) -> bool:
|
||||||
|
"""Ist dieser Auftrag gerade eben fertig geworden?"""
|
||||||
|
if job.get("state") in {"pending", "running"}:
|
||||||
|
return True
|
||||||
|
stempel = str(job.get("finished_at") or job.get("updated_at") or "")
|
||||||
|
if not stempel:
|
||||||
|
return False
|
||||||
|
try:
|
||||||
|
moment = datetime.fromisoformat(stempel)
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
if moment.tzinfo is None:
|
||||||
|
moment = moment.replace(tzinfo=timezone.utc)
|
||||||
|
return (datetime.now(timezone.utc) - moment).total_seconds() < sekunden
|
||||||
|
|
||||||
|
|
||||||
|
def befehl_pruefen(befehl: str) -> str:
|
||||||
|
"""Gibt den bereinigten Befehl zurueck -- oder wirft mit Klartext.
|
||||||
|
|
||||||
|
Eine eigene Funktion, weil genau diese Frage sich pruefen lassen muss,
|
||||||
|
ohne ein Geraet zu brauchen.
|
||||||
|
"""
|
||||||
|
sauber = (befehl or "").strip()
|
||||||
|
if not sauber:
|
||||||
|
raise ValueError("Es wurde kein Befehl eingegeben.")
|
||||||
|
if len(sauber) > MAX_BEFEHLSLAENGE:
|
||||||
|
raise ValueError(
|
||||||
|
f"Der Befehl ist zu lang (mehr als {MAX_BEFEHLSLAENGE} Zeichen). "
|
||||||
|
"Für längere Abläufe ist ein Skript auf dem Gerät der bessere Weg."
|
||||||
|
)
|
||||||
|
for zeichen in VERBOTENE_ZEICHEN:
|
||||||
|
if zeichen in sauber:
|
||||||
|
raise ValueError(
|
||||||
|
"Der Befehl enthält einen Zeilenumbruch. Hier läuft genau eine "
|
||||||
|
"Befehlszeile – alles danach würde stillschweigend mitlaufen."
|
||||||
|
)
|
||||||
|
return sauber
|
||||||
|
|
||||||
|
|
||||||
|
def run_command(
|
||||||
|
database: Any,
|
||||||
|
*,
|
||||||
|
job_id: str,
|
||||||
|
device: dict[str, Any],
|
||||||
|
username: str,
|
||||||
|
password: str,
|
||||||
|
befehl: str,
|
||||||
|
timeout: int,
|
||||||
|
) -> None:
|
||||||
|
"""Fuehrt **eine** Befehlszeile auf einem Geraet aus. Eigener Thread.
|
||||||
|
|
||||||
|
Bewusst ohne Deutung des Ergebnisses: was der Befehl tut, weiss der
|
||||||
|
Mensch, der ihn geschrieben hat. Festgehalten wird, was zurueckkam und mit
|
||||||
|
welchem Rueckgabewert -- mehr kann ein Werkzeug hier nicht beitragen, und
|
||||||
|
weniger waere zu wenig.
|
||||||
|
"""
|
||||||
|
name = str(device.get("name") or device.get("ip"))
|
||||||
|
|
||||||
|
def emit(line: str) -> None:
|
||||||
|
stamp = datetime.now().strftime("%H:%M:%S")
|
||||||
|
update_job(database, job_id, append_output=f"{stamp} {line}\n")
|
||||||
|
|
||||||
|
update_job(
|
||||||
|
database, job_id, state="running", progress=5, message="Verbindung wird aufgebaut"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
with database.session() as conn:
|
||||||
|
transport = open_transport(
|
||||||
|
conn,
|
||||||
|
host=str(device["ip"]),
|
||||||
|
port=int(device.get("ssh_port") or 22),
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
)
|
||||||
|
except SshError as exc:
|
||||||
|
emit(f"FEHLER: {exc}")
|
||||||
|
update_job(database, job_id, state="failed", message=str(exc), finished=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
status = 1
|
||||||
|
try:
|
||||||
|
sudo, sudo_stdin = _sudo_mode(transport, password, emit)
|
||||||
|
# Nur wer {sudo} benutzt, bekommt es auch: ein Befehl ohne diese Marke
|
||||||
|
# laeuft als der angemeldete Benutzer, und das ist die richtige
|
||||||
|
# Vorgabe -- Rechte holt man sich hier ausdruecklich.
|
||||||
|
ausgefuehrt = befehl.replace("{sudo}", sudo) if "{sudo}" in befehl else befehl
|
||||||
|
emit(f"$ {ausgefuehrt}")
|
||||||
|
update_job(database, job_id, progress=40, message="Befehl läuft")
|
||||||
|
status = _run_remote_command(
|
||||||
|
transport,
|
||||||
|
ausgefuehrt,
|
||||||
|
timeout=timeout,
|
||||||
|
on_line=emit,
|
||||||
|
stdin_text=sudo_stdin if "{sudo}" in befehl else "",
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001 - der Auftrag soll das Ergebnis tragen
|
||||||
|
emit(f"FEHLER: {exc}")
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
transport.close()
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
pass
|
||||||
|
|
||||||
|
emit(COMPLETION_MARKER)
|
||||||
|
update_job(
|
||||||
|
database,
|
||||||
|
job_id,
|
||||||
|
state="succeeded" if status == 0 else "failed",
|
||||||
|
progress=100,
|
||||||
|
message=(
|
||||||
|
"Befehl ausgeführt."
|
||||||
|
if status == 0
|
||||||
|
else f"Der Befehl endete mit Rückgabewert {status}."
|
||||||
|
),
|
||||||
|
finished=True,
|
||||||
|
)
|
||||||
|
with database.session() as conn, database.transaction(conn):
|
||||||
|
audit.log(
|
||||||
|
conn,
|
||||||
|
actor="system",
|
||||||
|
action="maintenance.command_finished",
|
||||||
|
target=name,
|
||||||
|
detail=f"Rückgabewert {status}",
|
||||||
|
severity="notice" if status == 0 else "warning",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _reboot_windows(
|
def _reboot_windows(
|
||||||
database: Any,
|
database: Any,
|
||||||
*,
|
*,
|
||||||
@@ -457,6 +718,7 @@ def _reboot_windows(
|
|||||||
actor: str,
|
actor: str,
|
||||||
emit: Callable[[str], None],
|
emit: Callable[[str], None],
|
||||||
elapsed: Callable[[], int],
|
elapsed: Callable[[], int],
|
||||||
|
live: Any = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""Neustart eines Windows-Geraets ueber RPC. ``True`` = weiter warten.
|
"""Neustart eines Windows-Geraets ueber RPC. ``True`` = weiter warten.
|
||||||
|
|
||||||
@@ -487,6 +749,7 @@ def _reboot_windows(
|
|||||||
duration_ms=elapsed(),
|
duration_ms=elapsed(),
|
||||||
detail=detail,
|
detail=detail,
|
||||||
method=METHOD_RPC,
|
method=METHOD_RPC,
|
||||||
|
live=live,
|
||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -501,6 +764,7 @@ def run_reboot(
|
|||||||
wait_seconds: int,
|
wait_seconds: int,
|
||||||
actor: str = "system",
|
actor: str = "system",
|
||||||
method: str = METHOD_SSH,
|
method: str = METHOD_SSH,
|
||||||
|
live: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Loest einen Neustart aus und wartet auf die Rueckkehr.
|
"""Loest einen Neustart aus und wartet auf die Rueckkehr.
|
||||||
|
|
||||||
@@ -553,6 +817,7 @@ def run_reboot(
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
emit=emit,
|
emit=emit,
|
||||||
elapsed=elapsed,
|
elapsed=elapsed,
|
||||||
|
live=live,
|
||||||
):
|
):
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
@@ -576,6 +841,7 @@ def run_reboot(
|
|||||||
duration_ms=elapsed(),
|
duration_ms=elapsed(),
|
||||||
detail=str(exc),
|
detail=str(exc),
|
||||||
method=method,
|
method=method,
|
||||||
|
live=live,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -618,6 +884,7 @@ def run_reboot(
|
|||||||
duration_ms=elapsed(),
|
duration_ms=elapsed(),
|
||||||
detail=problem,
|
detail=problem,
|
||||||
method=method,
|
method=method,
|
||||||
|
live=live,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -658,6 +925,7 @@ def run_reboot(
|
|||||||
duration_ms=elapsed(),
|
duration_ms=elapsed(),
|
||||||
detail="Neustart über SSH, Gerät ist zurückgekehrt.",
|
detail="Neustart über SSH, Gerät ist zurückgekehrt.",
|
||||||
method=method,
|
method=method,
|
||||||
|
live=live,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
time.sleep(REBOOT_POLL_SECONDS)
|
time.sleep(REBOOT_POLL_SECONDS)
|
||||||
@@ -682,7 +950,8 @@ def run_reboot(
|
|||||||
f"{wait_seconds}s nicht wieder geantwortet."
|
f"{wait_seconds}s nicht wieder geantwortet."
|
||||||
),
|
),
|
||||||
method=method,
|
method=method,
|
||||||
)
|
live=live,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _log_restart(
|
def _log_restart(
|
||||||
@@ -694,6 +963,7 @@ def _log_restart(
|
|||||||
duration_ms: int,
|
duration_ms: int,
|
||||||
detail: str,
|
detail: str,
|
||||||
method: str = METHOD_SSH,
|
method: str = METHOD_SSH,
|
||||||
|
live: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Traegt den Neustart in die geraeteweite Neustart-Historie ein.
|
"""Traegt den Neustart in die geraeteweite Neustart-Historie ein.
|
||||||
|
|
||||||
@@ -716,6 +986,7 @@ def _log_restart(
|
|||||||
duration_ms=duration_ms,
|
duration_ms=duration_ms,
|
||||||
detail=detail,
|
detail=detail,
|
||||||
method=method,
|
method=method,
|
||||||
|
live=live,
|
||||||
)
|
)
|
||||||
except Exception: # noqa: BLE001 - Protokollieren darf den Auftrag nie kippen
|
except Exception: # noqa: BLE001 - Protokollieren darf den Auftrag nie kippen
|
||||||
logger.exception("Neustart konnte nicht protokolliert werden (Gerät %s)", device_id)
|
logger.exception("Neustart konnte nicht protokolliert werden (Gerät %s)", device_id)
|
||||||
|
|||||||
@@ -156,6 +156,16 @@ class LiveLog:
|
|||||||
def info(self, scope: str, message: str) -> None:
|
def info(self, scope: str, message: str) -> None:
|
||||||
self.write("INFO", scope, message)
|
self.write("INFO", scope, message)
|
||||||
|
|
||||||
|
def notice(self, scope: str, message: str) -> None:
|
||||||
|
"""Etwas ist passiert, und es ist in Ordnung -- aber es gehoert gesehen.
|
||||||
|
|
||||||
|
Zwischen "laeuft" und "stimmt etwas nicht" fehlte eine Stufe. Ein
|
||||||
|
Geraet, das neu gestartet wird, ist kein Fehler; es einfach als INFO
|
||||||
|
zwischen zweihundert Erreichbarkeitsmeldungen zu stellen heisst aber,
|
||||||
|
dass es niemand findet.
|
||||||
|
"""
|
||||||
|
self.write("HINWEIS", scope, message)
|
||||||
|
|
||||||
def warning(self, scope: str, message: str) -> None:
|
def warning(self, scope: str, message: str) -> None:
|
||||||
self.write("WARNUNG", scope, message)
|
self.write("WARNUNG", scope, message)
|
||||||
|
|
||||||
@@ -278,6 +288,39 @@ METHOD_LABELS = {
|
|||||||
METHOD_RPC: "Neustart über RPC",
|
METHOD_RPC: "Neustart über RPC",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#: Wer den Neustart ausgeloest hat -- in Worten statt in Schluesseln.
|
||||||
|
#:
|
||||||
|
#: Die Schluessel stehen so in der Datenbank und bleiben dort; eine Spalte
|
||||||
|
#: umzuschreiben, um eine Anzeige zu verbessern, waere der falsche Weg. Die
|
||||||
|
#: Uebersetzung gehoert an genau eine Stelle, sonst heisst derselbe Vorgang im
|
||||||
|
#: Protokoll "auto" und auf der Seite "automatisch".
|
||||||
|
TRIGGER_LABELS = {
|
||||||
|
"auto": "automatisch",
|
||||||
|
"manuell": "manuell",
|
||||||
|
"maintenance": "aus der Wartung",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def trigger_label(trigger: str) -> str:
|
||||||
|
"""Der Ausloeser als Wort -- unbekannte Schluessel bleiben, wie sie sind."""
|
||||||
|
return TRIGGER_LABELS.get(str(trigger or ""), str(trigger or "unbekannt"))
|
||||||
|
|
||||||
|
|
||||||
|
def _geraet_kurz(conn: sqlite3.Connection, device_id: int) -> str:
|
||||||
|
"""``Name (IP)`` fuer das laufende Protokoll.
|
||||||
|
|
||||||
|
Die Neustart-Funktionen bekommen nur eine Kennung; im Protokoll steht aber
|
||||||
|
eine Zeile, die ein Mensch liest -- und "Geraet 17 wird neu gestartet"
|
||||||
|
hilft niemandem.
|
||||||
|
"""
|
||||||
|
row = conn.execute("SELECT name, ip FROM devices WHERE id=?", (int(device_id),)).fetchone()
|
||||||
|
if row is None:
|
||||||
|
return f"Gerät {device_id}"
|
||||||
|
name = str(row["name"] or "").strip() or f"Gerät {device_id}"
|
||||||
|
ip = str(row["ip"] or "").strip()
|
||||||
|
return f"{name} ({ip})" if ip else name
|
||||||
|
|
||||||
|
|
||||||
#: Zustand zwischen ausgeloestem Neustart und Rueckkehr.
|
#: Zustand zwischen ausgeloestem Neustart und Rueckkehr.
|
||||||
#:
|
#:
|
||||||
#: Ohne ihn blieb die Kachel auf "online" stehen: der Zustand wechselt erst
|
#: Ohne ihn blieb die Kachel auf "online" stehen: der Zustand wechselt erst
|
||||||
@@ -303,6 +346,7 @@ def record_restart(
|
|||||||
duration_ms: int,
|
duration_ms: int,
|
||||||
detail: str,
|
detail: str,
|
||||||
method: str = METHOD_POE,
|
method: str = METHOD_POE,
|
||||||
|
live: Any = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Schreibt einen Neustart ins Protokoll -- unabhaengig davon, wie er lief.
|
"""Schreibt einen Neustart ins Protokoll -- unabhaengig davon, wie er lief.
|
||||||
|
|
||||||
@@ -311,6 +355,12 @@ def record_restart(
|
|||||||
nur in ``method``. Vorher gab es nur die PoE-Haelfte, und ein per Wartung
|
nur in ``method``. Vorher gab es nur die PoE-Haelfte, und ein per Wartung
|
||||||
neu gestartetes Geraet tauchte in der Neustart-Historie ueberhaupt nicht
|
neu gestartetes Geraet tauchte in der Neustart-Historie ueberhaupt nicht
|
||||||
auf.
|
auf.
|
||||||
|
|
||||||
|
Mit ``live`` steht das Ergebnis auch im laufenden Protokoll. Diese
|
||||||
|
Funktion ist der Trichter, durch den **jeder** Neustart laeuft -- PoE
|
||||||
|
automatisch wie von Hand, SSH und RPC aus der Wartung. Die Zeile hier zu
|
||||||
|
schreiben statt bei jeder Quelle einzeln ist der Unterschied zwischen
|
||||||
|
"alle Wege sind abgedeckt" und "alle Wege, an die jemand gedacht hat".
|
||||||
"""
|
"""
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO restart_events "
|
"INSERT INTO restart_events "
|
||||||
@@ -318,6 +368,17 @@ def record_restart(
|
|||||||
"VALUES (?,?,?,?,?,?,?,?,?)",
|
"VALUES (?,?,?,?,?,?,?,?,?)",
|
||||||
(device_id, _now(), trigger, actor, action, result, duration_ms, detail[:8000], method),
|
(device_id, _now(), trigger, actor, action, result, duration_ms, detail[:8000], method),
|
||||||
)
|
)
|
||||||
|
if live is not None:
|
||||||
|
wer = _geraet_kurz(conn, device_id)
|
||||||
|
weg = METHOD_LABELS.get(method, method)
|
||||||
|
if result == "ok":
|
||||||
|
live.info("neustart", f"{wer}: Neustart über {weg} erfolgreich")
|
||||||
|
elif result == "skipped":
|
||||||
|
# Warnstufe: ein Geraet, das haette neu gestartet werden sollen und
|
||||||
|
# es nicht wurde, ist kein Hinweis am Rande.
|
||||||
|
live.warning("neustart", f"{wer}: Neustart nicht ausgeführt – {detail}")
|
||||||
|
else:
|
||||||
|
live.error("neustart", f"{wer}: Neustart fehlgeschlagen – {detail}")
|
||||||
if result == "ok":
|
if result == "ok":
|
||||||
# Hier faellt nur noch der Zaehler und der Zeitstempel an. Den Zustand
|
# Hier faellt nur noch der Zaehler und der Zeitstempel an. Den Zustand
|
||||||
# setzt ``mark_restarting`` schon beim Ausloesen -- dieses Ergebnis
|
# setzt ``mark_restarting`` schon beim Ausloesen -- dieses Ergebnis
|
||||||
@@ -330,7 +391,14 @@ def record_restart(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def mark_restarting(conn: sqlite3.Connection, device_id: int) -> None:
|
def mark_restarting(
|
||||||
|
conn: sqlite3.Connection,
|
||||||
|
device_id: int,
|
||||||
|
*,
|
||||||
|
live: Any = None,
|
||||||
|
trigger: str = "",
|
||||||
|
method: str = METHOD_POE,
|
||||||
|
) -> None:
|
||||||
"""Setzt den Zustand auf "startet neu" -- beim **Ausloesen**, nicht am Ende.
|
"""Setzt den Zustand auf "startet neu" -- beim **Ausloesen**, nicht am Ende.
|
||||||
|
|
||||||
Ein Neustart ueber SSH oder RPC laeuft als Auftrag: Kommando schicken, dann
|
Ein Neustart ueber SSH oder RPC laeuft als Auftrag: Kommando schicken, dann
|
||||||
@@ -342,6 +410,12 @@ def mark_restarting(conn: sqlite3.Connection, device_id: int) -> None:
|
|||||||
Absicht. Und ohne ``last_restart_at``: das Kulanzfenster laeuft ab dem
|
Absicht. Und ohne ``last_restart_at``: das Kulanzfenster laeuft ab dem
|
||||||
Zeitpunkt, an dem der Neustart tatsaechlich ausgeloest wurde
|
Zeitpunkt, an dem der Neustart tatsaechlich ausgeloest wurde
|
||||||
(``record_restart``).
|
(``record_restart``).
|
||||||
|
|
||||||
|
Mit ``live`` geht der Beginn ins laufende Protokoll -- als Hinweis, nicht
|
||||||
|
als Warnung: ein Neustart ist kein Stoerfall, aber er erklaert die Luecke,
|
||||||
|
die gleich in der Erreichbarkeit steht. Dass die Zeile hier entsteht und
|
||||||
|
nicht bei den Aufrufern, ist Absicht: dies ist der Punkt, an dem ein
|
||||||
|
Neustart **ausgeloest** wird, gleich von welcher Quelle.
|
||||||
"""
|
"""
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -355,6 +429,12 @@ def mark_restarting(conn: sqlite3.Connection, device_id: int) -> None:
|
|||||||
""",
|
""",
|
||||||
(device_id, STATE_RESTARTING, _now(), _now()),
|
(device_id, STATE_RESTARTING, _now(), _now()),
|
||||||
)
|
)
|
||||||
|
if live is not None:
|
||||||
|
weg = METHOD_LABELS.get(method, method)
|
||||||
|
woher = f" ({trigger_label(trigger)})" if trigger else ""
|
||||||
|
live.notice(
|
||||||
|
"neustart", f"{_geraet_kurz(conn, device_id)} wird neu gestartet – {weg}{woher}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------------------------------- #
|
# --------------------------------------------------------------------------- #
|
||||||
@@ -402,7 +482,9 @@ def restart_device_poe(
|
|||||||
# wie fuer den automatischen Neustart, denn beide laufen hier durch.
|
# wie fuer den automatischen Neustart, denn beide laufen hier durch.
|
||||||
if port and switch_ip:
|
if port and switch_ip:
|
||||||
with write():
|
with write():
|
||||||
mark_restarting(conn, int(device["id"]))
|
mark_restarting(
|
||||||
|
conn, int(device["id"]), live=live, trigger=trigger, method=METHOD_POE
|
||||||
|
)
|
||||||
|
|
||||||
if not port or not switch_ip:
|
if not port or not switch_ip:
|
||||||
message = (
|
message = (
|
||||||
@@ -418,6 +500,7 @@ def restart_device_poe(
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
duration_ms=0,
|
duration_ms=0,
|
||||||
detail=message,
|
detail=message,
|
||||||
|
live=live,
|
||||||
)
|
)
|
||||||
return {"ok": False, "message": message, "transcript": ""}
|
return {"ok": False, "message": message, "transcript": ""}
|
||||||
|
|
||||||
@@ -462,9 +545,8 @@ def restart_device_poe(
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
duration_ms=duration,
|
duration_ms=duration,
|
||||||
detail=str(exc),
|
detail=str(exc),
|
||||||
|
live=live,
|
||||||
)
|
)
|
||||||
if live:
|
|
||||||
live.error("poe", f"{name}: {exc}")
|
|
||||||
return {"ok": False, "message": str(exc), "transcript": ""}
|
return {"ok": False, "message": str(exc), "transcript": ""}
|
||||||
|
|
||||||
duration = int((time.monotonic() - started) * 1000)
|
duration = int((time.monotonic() - started) * 1000)
|
||||||
@@ -478,12 +560,8 @@ def restart_device_poe(
|
|||||||
actor=actor,
|
actor=actor,
|
||||||
duration_ms=duration,
|
duration_ms=duration,
|
||||||
detail=(outcome.error or "; ".join(outcome.steps))[:8000],
|
detail=(outcome.error or "; ".join(outcome.steps))[:8000],
|
||||||
|
live=live,
|
||||||
)
|
)
|
||||||
if live:
|
|
||||||
if outcome.ok:
|
|
||||||
live.info("poe", f"{name}: PoE-Neustart auf {device.get('switch_hostname')} {port} erfolgreich")
|
|
||||||
else:
|
|
||||||
live.error("poe", f"{name}: PoE-Neustart fehlgeschlagen -- {outcome.error}")
|
|
||||||
return {
|
return {
|
||||||
"ok": outcome.ok,
|
"ok": outcome.ok,
|
||||||
"message": outcome.error or "PoE-Neustart durchgeführt.",
|
"message": outcome.error or "PoE-Neustart durchgeführt.",
|
||||||
@@ -507,23 +585,48 @@ class SweepReport:
|
|||||||
duration_ms: int = 0
|
duration_ms: int = 0
|
||||||
|
|
||||||
|
|
||||||
def merke_vnc(
|
def merke_vnc(conn: sqlite3.Connection, device_id: int, offen: bool, ziel: str = "") -> None:
|
||||||
conn: sqlite3.Connection, device_id: int, offen: bool, ziel: str = ""
|
|
||||||
) -> None:
|
|
||||||
"""Haelt fest, ob dort ein VNC-Server antwortet -- und unter welchem Ziel.
|
"""Haelt fest, ob dort ein VNC-Server antwortet -- und unter welchem Ziel.
|
||||||
|
|
||||||
Das Ziel gehoert dazu, weil die Statusuebersicht die Verbindung unmittelbar
|
Das Ziel gehoert dazu, weil die Statusuebersicht die Verbindung unmittelbar
|
||||||
verlinkt. Es aus dem Namen neu herzuleiten hiesse, je Kachel und
|
verlinkt. Es aus dem Namen neu herzuleiten hiesse, je Kachel und
|
||||||
Seitenaufbau eine Namensaufloesung zu versuchen; hier steht es einmal
|
Seitenaufbau eine Namensaufloesung zu versuchen; hier steht es einmal
|
||||||
geschrieben, von dem, der ohnehin gefragt hat.
|
geschrieben, von dem, der ohnehin gefragt hat.
|
||||||
|
|
||||||
|
Die Anmeldeverfahren bleiben unberuehrt: sie kommen aus einer anderen
|
||||||
|
Quelle (:func:`merke_verfahren`) und haetten hier nur den Wert "nicht
|
||||||
|
gefragt" -- der wuerde eine gute Messung ueberschreiben.
|
||||||
"""
|
"""
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE device_status SET vnc_open=?, vnc_checked_at=?, vnc_target=? "
|
"UPDATE device_status SET vnc_open=?, vnc_checked_at=?, vnc_target=? WHERE device_id=?",
|
||||||
"WHERE device_id=?",
|
|
||||||
(1 if offen else 0, _now(), ziel, device_id),
|
(1 if offen else 0, _now(), ziel, device_id),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def merke_verfahren(
|
||||||
|
conn: sqlite3.Connection, device_id: int, verfahren: tuple[int, ...], absage: str = ""
|
||||||
|
) -> None:
|
||||||
|
"""Haelt fest, wie sich der VNC-Server authentifizieren will.
|
||||||
|
|
||||||
|
Mit Zeitstempel, denn diese Messung kostet eine angefangene
|
||||||
|
Protokollverhandlung -- und die soll sich nicht bei jedem Seitenaufbau
|
||||||
|
wiederholen.
|
||||||
|
"""
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE device_status SET vnc_types=?, vnc_reason=?, vnc_types_at=? WHERE device_id=?",
|
||||||
|
(",".join(str(v) for v in verfahren), absage, _now(), device_id),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def merke_verfahren_das_traegt(
|
||||||
|
conn: sqlite3.Connection, device_id: int, verfahren: int
|
||||||
|
) -> None:
|
||||||
|
"""Haelt fest, womit die letzte Sitzung wirklich zustande kam."""
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE device_status SET vnc_method=? WHERE device_id=?", (int(verfahren), device_id)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_sweep(
|
def run_sweep(
|
||||||
*,
|
*,
|
||||||
database: Any,
|
database: Any,
|
||||||
@@ -543,6 +646,10 @@ def run_sweep(
|
|||||||
erreichbare Geraet: nimmt dort ein VNC-Server Verbindungen an? Sie gehoert
|
erreichbare Geraet: nimmt dort ein VNC-Server Verbindungen an? Sie gehoert
|
||||||
hierher und nicht in den Seitenaufbau -- einmal je Intervall statt einmal
|
hierher und nicht in den Seitenaufbau -- einmal je Intervall statt einmal
|
||||||
je Aufruf, und nur fuer Geraete, die ohnehin gerade antworten.
|
je Aufruf, und nur fuer Geraete, die ohnehin gerade antworten.
|
||||||
|
|
||||||
|
**Nur die Frage nach dem Port**, kein Wort Protokoll: ein RealVNC Server
|
||||||
|
zaehlt abgebrochene Verhandlungen und sperrt die Adresse eine Weile aus.
|
||||||
|
Welche Anmeldeverfahren er anbietet, misst die Seite, die es braucht.
|
||||||
"""
|
"""
|
||||||
from . import vnc as vnc_dienst # spaet: vnc benutzt tcp_reachable von hier
|
from . import vnc as vnc_dienst # spaet: vnc benutzt tcp_reachable von hier
|
||||||
|
|
||||||
@@ -558,15 +665,23 @@ def run_sweep(
|
|||||||
|
|
||||||
def pruefen(
|
def pruefen(
|
||||||
device: dict[str, Any],
|
device: dict[str, Any],
|
||||||
) -> tuple[dict[str, Any], PingResult, tuple[bool, str] | None]:
|
) -> tuple[dict[str, Any], PingResult, vnc_dienst.Zugang | None]:
|
||||||
ergebnis = ping(str(device["ip"]))
|
ergebnis = ping(str(device["ip"]))
|
||||||
if not vnc_port or not ergebnis.ok:
|
if not vnc_port or not ergebnis.ok:
|
||||||
# Kein Ergebnis heisst "nicht gefragt" -- und laesst den zuletzt
|
# Kein Ergebnis heisst "nicht gefragt" -- und laesst den zuletzt
|
||||||
# bekannten Stand stehen. Ein Geraet, das gerade neu startet, soll
|
# bekannten Stand stehen. Ein Geraet, das gerade neu startet, soll
|
||||||
# seinen Knopf nicht verlieren.
|
# seinen Knopf nicht verlieren.
|
||||||
return device, ergebnis, None
|
return device, ergebnis, None
|
||||||
ziel = vnc_dienst.ziel(device)
|
try:
|
||||||
return device, ergebnis, (vnc_dienst.erreichbar(ziel, vnc_port), ziel)
|
return device, ergebnis, vnc_dienst.zugang_kurz(device, port=vnc_port)
|
||||||
|
except Exception: # noqa: BLE001
|
||||||
|
# Die Ueberwachung ist die Kernaufgabe, der Bildschirmzugriff eine
|
||||||
|
# Zugabe. Was hier schiefgeht -- ein Name, den der Resolver nicht
|
||||||
|
# verdaut, ein Netz, das seltsam antwortet --, darf niemals den
|
||||||
|
# ganzen Durchlauf kosten: sonst stuenden alle Geraete auf
|
||||||
|
# "unbekannt", und niemand brächte das mit VNC in Verbindung.
|
||||||
|
logger.warning("VNC-Pruefung fuer %s uebersprungen", device.get("name"), exc_info=True)
|
||||||
|
return device, ergebnis, None
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=min(MAX_PARALLEL_CHECKS, len(devices))) as pool:
|
with ThreadPoolExecutor(max_workers=min(MAX_PARALLEL_CHECKS, len(devices))) as pool:
|
||||||
results = list(pool.map(pruefen, devices))
|
results = list(pool.map(pruefen, devices))
|
||||||
@@ -578,7 +693,7 @@ def run_sweep(
|
|||||||
report.checked += 1
|
report.checked += 1
|
||||||
previous, current = record_status(conn, int(device["id"]), result)
|
previous, current = record_status(conn, int(device["id"]), result)
|
||||||
if vnc_offen is not None:
|
if vnc_offen is not None:
|
||||||
merke_vnc(conn, int(device["id"]), vnc_offen[0], vnc_offen[1])
|
merke_vnc(conn, int(device["id"]), vnc_offen.erreichbar, vnc_offen.ziel)
|
||||||
if result.ok:
|
if result.ok:
|
||||||
report.online += 1
|
report.online += 1
|
||||||
else:
|
else:
|
||||||
@@ -604,10 +719,22 @@ def run_sweep(
|
|||||||
name = str(device["name"])
|
name = str(device["name"])
|
||||||
if not license_allows_auto_restart:
|
if not license_allows_auto_restart:
|
||||||
report.skipped_unlicensed += 1
|
report.skipped_unlicensed += 1
|
||||||
live.warning(
|
# Als Zeile in der Neustart-Historie, nicht nur als Protokollsatz:
|
||||||
"poe",
|
# wer auf der Seite "Neustarts" nachsieht, warum ein Geraet nicht
|
||||||
f"{name}: automatischer PoE-Neustart übersprungen – keine gültige Lizenz.",
|
# zurueckkam, soll dort auch lesen, dass TESM es versucht haette.
|
||||||
)
|
# Ein leerer Verlauf sieht sonst aus wie "es war nichts noetig".
|
||||||
|
with database.session() as conn, database.transaction(conn):
|
||||||
|
record_restart(
|
||||||
|
conn,
|
||||||
|
device_id=int(device["id"]),
|
||||||
|
action="cycle",
|
||||||
|
result="skipped",
|
||||||
|
trigger="auto",
|
||||||
|
actor="system",
|
||||||
|
duration_ms=0,
|
||||||
|
detail="keine gültige Lizenz für den automatischen PoE-Neustart",
|
||||||
|
live=live,
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
report.restarts_triggered += 1
|
report.restarts_triggered += 1
|
||||||
# Bewusst **keine** umschliessende Transaktion: der Neustart oeffnet eine
|
# Bewusst **keine** umschliessende Transaktion: der Neustart oeffnet eine
|
||||||
|
|||||||
@@ -57,6 +57,49 @@ class Zugang:
|
|||||||
ziel: str
|
ziel: str
|
||||||
port: int
|
port: int
|
||||||
erreichbar: bool
|
erreichbar: bool
|
||||||
|
#: Die Sicherheitsverfahren, die der Server angeboten hat (leer: nicht
|
||||||
|
#: gemessen). Daraus entsteht der Satz, warum eine Sitzung im Browser
|
||||||
|
#: gehen wird oder nicht.
|
||||||
|
verfahren: tuple[int, ...] = ()
|
||||||
|
#: Der Text, mit dem der Server die Verbindung abgelehnt hat -- er nennt
|
||||||
|
#: dann kein Verfahren, sondern eine 0 und einen Grund.
|
||||||
|
absage: str = ""
|
||||||
|
#: Das Verfahren, mit dem zuletzt wirklich eine Sitzung zustande kam
|
||||||
|
#: (0: noch keine). Damit faengt der naechste Versuch an.
|
||||||
|
bewaehrt: int = 0
|
||||||
|
|
||||||
|
@property
|
||||||
|
def browsertauglich(self) -> bool:
|
||||||
|
"""Ist mindestens ein angebotenes Verfahren eines, das noVNC kann?"""
|
||||||
|
return bool(set(self.verfahren) & BROWSER_VERFAHREN)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def hinderungsgrund(self) -> str:
|
||||||
|
"""Warum eine Sitzung im Browser nicht zustande kaeme -- im Klartext."""
|
||||||
|
if not self.erreichbar:
|
||||||
|
return f"Auf {self.ziel}:{self.port} antwortet kein VNC-Server."
|
||||||
|
if self.absage:
|
||||||
|
return (
|
||||||
|
f"Der VNC-Server auf {self.ziel} weist die Verbindung ab: "
|
||||||
|
f"„{self.absage}“. Das ist seine Auskunft, nicht unsere – "
|
||||||
|
"typisch sind eine Sperre nach zu vielen Fehlversuchen "
|
||||||
|
"(RealVNC: BlacklistTimeout), eine Adressfilterung oder eine "
|
||||||
|
"belegte Sitzung."
|
||||||
|
)
|
||||||
|
if not self.verfahren:
|
||||||
|
return ""
|
||||||
|
if self.browsertauglich:
|
||||||
|
return ""
|
||||||
|
angeboten = ", ".join(verfahren_name(v) for v in self.verfahren)
|
||||||
|
return (
|
||||||
|
f"Der VNC-Server bietet nur {angeboten} an. Der Browser-Client kann "
|
||||||
|
"davon keines: herstellereigene Verfahren sind nicht offengelegt, "
|
||||||
|
"und RA2 (5) verschlüsselt die ganze Strecke mit einem Verfahren, "
|
||||||
|
"das er nicht mitbringt. Auf dem Gerät hilft eine Zeile: "
|
||||||
|
"Encryption=AlwaysOff (dann bietet der Server RA2ne an) oder "
|
||||||
|
"Authentication=VncAuth samt vncpasswd (dann das klassische "
|
||||||
|
"VNC-Passwort)."
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def adresse(self) -> str:
|
def adresse(self) -> str:
|
||||||
@@ -89,7 +132,9 @@ def aufloesbar(name: str) -> bool:
|
|||||||
try:
|
try:
|
||||||
socket.getaddrinfo(name, None)
|
socket.getaddrinfo(name, None)
|
||||||
ergebnis = True
|
ergebnis = True
|
||||||
except OSError:
|
except Exception: # noqa: BLE001
|
||||||
|
# Nicht nur OSError: ein Name mit Sonderzeichen wirft UnicodeError,
|
||||||
|
# und der ist kein OSError. Aufloesbar ist er so oder so nicht.
|
||||||
ergebnis = False
|
ergebnis = False
|
||||||
_AUFLOESUNG[name] = (jetzt, ergebnis)
|
_AUFLOESUNG[name] = (jetzt, ergebnis)
|
||||||
return ergebnis
|
return ergebnis
|
||||||
@@ -122,19 +167,216 @@ def erreichbar(host: str, port: int = STANDARD_PORT) -> bool:
|
|||||||
return tcp_reachable(host, port, timeout=ZEITLIMIT)
|
return tcp_reachable(host, port, timeout=ZEITLIMIT)
|
||||||
|
|
||||||
|
|
||||||
def zugang(device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: bool = True) -> Zugang:
|
#: Was der Browser-Client (noVNC 1.6) aushandeln kann. Aus dessen
|
||||||
"""Ziel und Erreichbarkeit in einem Stueck -- so nimmt es die Vorlage."""
|
#: ``_isSupportedSecurityType`` uebernommen -- steht hier, weil die Oberflaeche
|
||||||
|
#: **vor** dem Verbinden sagen koennen soll, ob es klappen wird.
|
||||||
|
BROWSER_VERFAHREN: frozenset[int] = frozenset({1, 2, 6, 16, 19, 22, 30, 113})
|
||||||
|
|
||||||
|
#: Die Namen dazu. Was nicht drinsteht, ist herstellereigen -- die Nummern
|
||||||
|
#: 7 bis 15 gehoeren RealVNC, 3 und 4 ebenfalls.
|
||||||
|
VERFAHREN_NAMEN: dict[int, str] = {
|
||||||
|
1: "ohne Authentifizierung",
|
||||||
|
2: "VNC-Passwort",
|
||||||
|
5: "RA2 (RSA-AES, verschlüsselt)",
|
||||||
|
6: "RA2ne (RSA-AES, unverschlüsselt)",
|
||||||
|
16: "Tight",
|
||||||
|
18: "TLS",
|
||||||
|
19: "VeNCrypt",
|
||||||
|
22: "xvp",
|
||||||
|
30: "Apple ARD",
|
||||||
|
113: "MS-Logon II",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def verfahren_name(nummer: int) -> str:
|
||||||
|
"""Lesbarer Name eines Sicherheitsverfahrens."""
|
||||||
|
if nummer in VERFAHREN_NAMEN:
|
||||||
|
return f"{nummer} ({VERFAHREN_NAMEN[nummer]})"
|
||||||
|
if 3 <= nummer <= 15:
|
||||||
|
return f"{nummer} (RealVNC-eigen)"
|
||||||
|
return str(nummer)
|
||||||
|
|
||||||
|
|
||||||
|
def _absage(sock: Any) -> str:
|
||||||
|
"""Der Grund, den ein Server einer abgelehnten Verbindung mitgibt.
|
||||||
|
|
||||||
|
Auf eine 0 -- ob als Anzahl (ab 3.7) oder als bestimmtes Verfahren (3.3) --
|
||||||
|
folgt eine Laenge und ein Text. Genau dieser Satz ist die Auskunft, die
|
||||||
|
sonst niemand bekommt: "zu viele Fehlversuche", "Verbindung von dieser
|
||||||
|
Adresse nicht erlaubt", "kein gemeinsames Verfahren". Ohne ihn steht da
|
||||||
|
nur, dass es nicht geht.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
roh = sock.recv(4)
|
||||||
|
if len(roh) < 4:
|
||||||
|
return ""
|
||||||
|
laenge = int.from_bytes(roh, "big")
|
||||||
|
if not 0 < laenge <= 1024:
|
||||||
|
return ""
|
||||||
|
text = b""
|
||||||
|
while len(text) < laenge:
|
||||||
|
stueck = sock.recv(laenge - len(text))
|
||||||
|
if not stueck:
|
||||||
|
break
|
||||||
|
text += stueck
|
||||||
|
return text.decode("utf-8", "replace").strip()
|
||||||
|
except OSError:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def sicherheitsverfahren(
|
||||||
|
host: str, port: int = STANDARD_PORT
|
||||||
|
) -> tuple[str, tuple[int, ...], str]:
|
||||||
|
"""Fragt den Server, **wie** er sich authentifizieren will.
|
||||||
|
|
||||||
|
Der Anfang einer VNC-Verbindung ist fuer jeden Server derselbe: er nennt
|
||||||
|
seine Protokollfassung, der Client die seine, dann kommt die Liste der
|
||||||
|
Sicherheitsverfahren. Weiter gehen wir nicht -- die Verbindung wird sofort
|
||||||
|
geschlossen, es wird nichts angemeldet und nichts geraten.
|
||||||
|
|
||||||
|
Das ist die Angabe, an der eine Bildschirmsitzung im Browser scheitert oder
|
||||||
|
gelingt, und sie hier zu messen ist der Unterschied zwischen einer
|
||||||
|
schwarzen Flaeche und dem Satz "dieser Server bietet nur Verfahren an, die
|
||||||
|
der Browser nicht kennt".
|
||||||
|
|
||||||
|
Gibt ``(Fassung, Verfahren, Absage)`` zurueck. Die Absage ist der Text, den
|
||||||
|
ein Server mitschickt, wenn er die Verbindung **ablehnt** -- er meldet dann
|
||||||
|
kein Verfahren, sondern eine 0 und einen Grund. Bei einem Fehler
|
||||||
|
``("", (), "")``.
|
||||||
|
"""
|
||||||
|
import socket
|
||||||
|
|
||||||
|
try:
|
||||||
|
with socket.create_connection((host, int(port)), timeout=ZEITLIMIT) as sock:
|
||||||
|
sock.settimeout(ZEITLIMIT)
|
||||||
|
gruss = sock.recv(12)
|
||||||
|
if not gruss.startswith(b"RFB "):
|
||||||
|
return "", (), ""
|
||||||
|
fassung = gruss[4:11].decode("ascii", "replace")
|
||||||
|
# Wir antworten mit 3.8 -- der Fassung, die auch der Browser
|
||||||
|
# spricht. Ein Server, der mehr kann, faellt darauf zurueck und
|
||||||
|
# nennt trotzdem seine Verfahren.
|
||||||
|
# Antworten wird mit *seiner* Fassung, hoechstens 3.8. Wer einem
|
||||||
|
# 3.3-Server mit 3.8 antwortet, bekommt keine Liste, sondern eine
|
||||||
|
# 32-Bit-Zahl -- und liest sie als "keine Verfahren".
|
||||||
|
eigene = "003.003" if fassung == "003.003" else "003.008"
|
||||||
|
sock.sendall(b"RFB " + eigene.encode("ascii") + bytes([10]))
|
||||||
|
|
||||||
|
if eigene == "003.003":
|
||||||
|
# RFB 3.3: der Server bestimmt allein und schickt eine
|
||||||
|
# 32-Bit-Zahl. Das ist kein Sonderfall aus der Vorzeit --
|
||||||
|
# RealVNC faellt darauf zurueck, sobald keine Anmeldung
|
||||||
|
# verlangt wird.
|
||||||
|
rohwert = b""
|
||||||
|
while len(rohwert) < 4:
|
||||||
|
stueck = sock.recv(4 - len(rohwert))
|
||||||
|
if not stueck:
|
||||||
|
break
|
||||||
|
rohwert += stueck
|
||||||
|
if len(rohwert) < 4:
|
||||||
|
return fassung, (), ""
|
||||||
|
bestimmt = int.from_bytes(rohwert, "big")
|
||||||
|
if bestimmt == 0:
|
||||||
|
return fassung, (), _absage(sock)
|
||||||
|
return fassung, (bestimmt,), ""
|
||||||
|
|
||||||
|
anzahl = sock.recv(1)
|
||||||
|
if not anzahl:
|
||||||
|
return fassung, (), ""
|
||||||
|
wie_viele = anzahl[0]
|
||||||
|
if wie_viele == 0:
|
||||||
|
# Null heisst: der Server lehnt ab und schickt den Grund.
|
||||||
|
return fassung, (), _absage(sock)
|
||||||
|
liste = b""
|
||||||
|
while len(liste) < wie_viele:
|
||||||
|
stueck = sock.recv(wie_viele - len(liste))
|
||||||
|
if not stueck:
|
||||||
|
break
|
||||||
|
liste += stueck
|
||||||
|
return fassung, tuple(liste), ""
|
||||||
|
except OSError:
|
||||||
|
return "", (), ""
|
||||||
|
|
||||||
|
|
||||||
|
def zugang_kurz(device: Mapping[str, Any], *, port: int = STANDARD_PORT) -> Zugang:
|
||||||
|
"""Nur die Frage, ob dort jemand annimmt -- ohne ein Wort Protokoll.
|
||||||
|
|
||||||
|
Das ist die Fassung fuer den Ueberwachungsdurchlauf. Er laeuft alle paar
|
||||||
|
Minuten ueber jedes Geraet, und eine angefangene und sofort wieder
|
||||||
|
abgebrochene Protokollverhandlung ist nicht nur unhoeflich: ein RealVNC
|
||||||
|
Server zaehlt abgebrochene Versuche und sperrt die Adresse eine Weile aus
|
||||||
|
(``BlacklistTimeout``). Eine Ueberwachung, die ihr eigenes Ziel aussperrt,
|
||||||
|
waere ein schoener Eigentor -- also fragt sie nur, ob der Port annimmt.
|
||||||
|
|
||||||
|
Welche Anmeldeverfahren der Server kann, misst erst die Seite, die es
|
||||||
|
wirklich braucht -- und auch die nicht oefter als noetig.
|
||||||
|
"""
|
||||||
host = ziel(device)
|
host = ziel(device)
|
||||||
return Zugang(ziel=host, port=port, erreichbar=erreichbar(host, port) if pruefen else False)
|
return Zugang(ziel=host, port=port, erreichbar=erreichbar(host, port))
|
||||||
|
|
||||||
|
|
||||||
def verbindungsdatei(device: Mapping[str, Any], *, port: int = STANDARD_PORT) -> str:
|
def zugang(device: Mapping[str, Any], *, port: int = STANDARD_PORT, pruefen: bool = True) -> Zugang:
|
||||||
|
"""Ziel, Erreichbarkeit und Verfahren in einem Stueck -- so nimmt es die Vorlage."""
|
||||||
|
host = ziel(device)
|
||||||
|
if not pruefen:
|
||||||
|
return Zugang(ziel=host, port=port, erreichbar=False)
|
||||||
|
fassung, verfahren, absage = sicherheitsverfahren(host, port)
|
||||||
|
return Zugang(
|
||||||
|
ziel=host,
|
||||||
|
port=port,
|
||||||
|
erreichbar=bool(fassung),
|
||||||
|
verfahren=verfahren,
|
||||||
|
absage=absage,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def zugang_gemerkt(device: Mapping[str, Any], *, port: int = STANDARD_PORT) -> Zugang:
|
||||||
|
"""Der Zugang, wie er in der Datenbank steht -- **ohne** das Geraet zu fragen.
|
||||||
|
|
||||||
|
Frueher hat diese Funktion selbst gemessen, wenn die letzte Messung alt war.
|
||||||
|
Das ist gestrichen, und zwar aus Erfahrung: jede Messung ist eine
|
||||||
|
angefangene und abgebrochene Protokollverhandlung, und ein RealVNC Server
|
||||||
|
zaehlt so etwas. Wer eine Bildschirmseite oeffnet, klopfte damit zweimal an
|
||||||
|
-- einmal zum Messen, einmal fuer die Sitzung --, und das zweite Klopfen
|
||||||
|
fiel dem ersten zum Opfer.
|
||||||
|
|
||||||
|
Jetzt beruehrt das Oeffnen einer Seite das Geraet gar nicht mehr. Die
|
||||||
|
Erreichbarkeit kommt aus dem Ueberwachungsdurchlauf (ein blosser
|
||||||
|
Verbindungsaufbau), die Anmeldeverfahren aus der letzten **ausdruecklichen**
|
||||||
|
Pruefung -- der Knopf dafuer steht auf der Seite und wird gedrueckt, wenn
|
||||||
|
etwas nicht geht.
|
||||||
|
"""
|
||||||
|
gespeichert = str(device.get("vnc_types") or "")
|
||||||
|
return Zugang(
|
||||||
|
ziel=ziel(device),
|
||||||
|
port=port,
|
||||||
|
erreichbar=bool(device.get("vnc_open")),
|
||||||
|
verfahren=tuple(
|
||||||
|
int(teil) for teil in gespeichert.split(",") if teil.strip().isdigit()
|
||||||
|
),
|
||||||
|
absage=str(device.get("vnc_reason") or ""),
|
||||||
|
bewaehrt=int(device.get("vnc_method") or 0),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def verbindungsdatei(
|
||||||
|
device: Mapping[str, Any], *, port: int = STANDARD_PORT, warnung_aus: bool = False
|
||||||
|
) -> str:
|
||||||
"""Der Inhalt der ``.vnc``-Datei -- drei Zeilen, keine vierte.
|
"""Der Inhalt der ``.vnc``-Datei -- drei Zeilen, keine vierte.
|
||||||
|
|
||||||
``ConnMethod=tcp`` ist die direkte Verbindung (nicht ueber den Cloud-Dienst
|
``ConnMethod=tcp`` ist die direkte Verbindung (nicht ueber den Cloud-Dienst
|
||||||
von RealVNC), ``FriendlyName`` der Name, den der Viewer anzeigt, ``Host``
|
von RealVNC), ``FriendlyName`` der Name, den der Viewer anzeigt, ``Host``
|
||||||
das Ziel. Ein abweichender Port gehoert in dieser Datei an den Host, mit
|
das Ziel. Ein abweichender Port gehoert in dieser Datei an den Host, mit
|
||||||
Doppelpunkt -- einen eigenen Schluessel dafuer gibt es nicht.
|
Doppelpunkt -- einen eigenen Schluessel dafuer gibt es nicht.
|
||||||
|
|
||||||
|
``warnung_aus`` haengt eine vierte Zeile an: ``WarnUnencrypted=False``. Sie
|
||||||
|
betrifft **den Viewer**, nicht den Server -- er zeigt sonst bei jeder
|
||||||
|
Verbindung zu einem Geraet ohne Streckenverschluesselung einen roten
|
||||||
|
Hinweis, den man wegklicken muss. Wer ``Encryption=AlwaysOff`` auf dem
|
||||||
|
Geraet gesetzt hat (damit der Browser ueberhaupt verbinden kann), sieht
|
||||||
|
diesen Hinweis fortan bei jedem Start. Er bleibt in der Vorgabe stehen:
|
||||||
|
eine unverschluesselte Strecke soll man einmal bewusst hinnehmen, nicht
|
||||||
|
stillschweigend. Abschaltbar unter Verwaltung -> System.
|
||||||
"""
|
"""
|
||||||
host = ziel(device)
|
host = ziel(device)
|
||||||
if port != STANDARD_PORT:
|
if port != STANDARD_PORT:
|
||||||
@@ -144,6 +386,8 @@ def verbindungsdatei(device: Mapping[str, Any], *, port: int = STANDARD_PORT) ->
|
|||||||
f"FriendlyName={str(device.get('name') or host).strip()}",
|
f"FriendlyName={str(device.get('name') or host).strip()}",
|
||||||
f"Host={host}",
|
f"Host={host}",
|
||||||
]
|
]
|
||||||
|
if warnung_aus:
|
||||||
|
zeilen.append("WarnUnencrypted=False")
|
||||||
return ZEILE.join(zeilen) + ZEILE
|
return ZEILE.join(zeilen) + ZEILE
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,28 @@ TESM_SETTINGS: tuple[SettingSpec, ...] = (
|
|||||||
minimum=1,
|
minimum=1,
|
||||||
maximum=65535,
|
maximum=65535,
|
||||||
),
|
),
|
||||||
|
SettingSpec(
|
||||||
|
"vnc_idle_timeout",
|
||||||
|
300,
|
||||||
|
"int",
|
||||||
|
"Bildschirmsitzung bei Untätigkeit trennen nach (Sekunden)",
|
||||||
|
hint="Gezählt wird, wenn in **keine** Richtung mehr etwas fließt: der Bildschirm "
|
||||||
|
"ändert sich nicht und es kommt keine Eingabe. Eine Sitzung, die jemand offen "
|
||||||
|
"vergisst, hält sonst die Verbindung zum Gerät. 0 schaltet die Trennung ab.",
|
||||||
|
section="monitoring",
|
||||||
|
minimum=0,
|
||||||
|
maximum=86400,
|
||||||
|
),
|
||||||
|
SettingSpec(
|
||||||
|
"vnc_warn_unencrypted_off",
|
||||||
|
False,
|
||||||
|
"bool",
|
||||||
|
"Warnung des Viewers vor unverschlüsselter Verbindung unterdrücken",
|
||||||
|
hint="Setzt WarnUnencrypted=False in die Verbindungsdatei. Betrifft nur den "
|
||||||
|
"RealVNC Viewer auf dem Arbeitsplatz, nicht die Verbindung selbst: die bleibt "
|
||||||
|
"unverschlüsselt, wenn das Gerät es so anbietet.",
|
||||||
|
section="monitoring",
|
||||||
|
),
|
||||||
# -- DHCP --------------------------------------------------------------- #
|
# -- DHCP --------------------------------------------------------------- #
|
||||||
SettingSpec("dhcp_domain", "local", "str", "Domainname", section="dhcp"),
|
SettingSpec("dhcp_domain", "local", "str", "Domainname", section="dhcp"),
|
||||||
SettingSpec(
|
SettingSpec(
|
||||||
|
|||||||
@@ -101,14 +101,26 @@
|
|||||||
{% if tile.vnc or tile.restart_methods %}
|
{% if tile.vnc or tile.restart_methods %}
|
||||||
<div class="tile__actions">
|
<div class="tile__actions">
|
||||||
{% if tile.vnc %}
|
{% if tile.vnc %}
|
||||||
{# Unmittelbar die Verbindung, keine Datei: auf der
|
{# Die Sitzung laeuft in TESM selbst: ein Klick, kein
|
||||||
Uebersicht will man hin, nicht etwas herunterladen.
|
Viewer auf dem Arbeitsplatz, keine Datei.
|
||||||
Die Verbindungsdatei fuer Viewer ohne registriertes
|
|
||||||
Schema steht auf der Detailseite. #}
|
Mit frischer Passwortbestätigung öffnet sie sich hier
|
||||||
<a class="btn btn--sm" href="{{ tile.vnc }}"
|
im Fenster -- man bleibt bei der Übersicht und sieht
|
||||||
title="Bildschirm öffnen ({{ tile.vnc }})">
|
die anderen Geräte weiter. Ohne sie führt der Knopf
|
||||||
{{ icon('monitor', size=14) }} VNC
|
auf die Geräteseite, wo danach gefragt wird. #}
|
||||||
</a>
|
{% if vnc_fenster %}
|
||||||
|
<button class="btn btn--sm" type="button"
|
||||||
|
data-vnc-oeffnen="{{ tile.vnc_quelle }}"
|
||||||
|
data-vnc-name="{{ tile.name }}"
|
||||||
|
title="Bildschirm im Fenster öffnen">
|
||||||
|
{{ icon('monitor', size=14) }} VNC
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<a class="btn btn--sm" href="{{ url_for('screen.screen', device_id=tile.id) }}"
|
||||||
|
title="Bildschirm im Browser öffnen">
|
||||||
|
{{ icon('monitor', size=14) }} VNC
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="tile__spacer" aria-hidden="true"></span>
|
<span class="tile__spacer" aria-hidden="true"></span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{# Die Bildschirmfläche -- einmal geschrieben, an zwei Orten benutzt: auf der
|
||||||
|
Geräteseite und im Fenster der Statusübersicht.
|
||||||
|
|
||||||
|
Die Teile finden sich über `data-rolle`, nicht über Kennungen: im Fenster
|
||||||
|
gäbe es sie sonst zweimal auf derselben Seite, und die zweite wäre die, die
|
||||||
|
niemand findet. #}
|
||||||
|
{% macro vnc_flaeche(quelle='') %}
|
||||||
|
<div class="screen" {% if quelle %}data-sitzung="{{ quelle }}"{% endif %}>
|
||||||
|
<div data-rolle="leinwand" class="screen__canvas" hidden></div>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro vnc_status() %}
|
||||||
|
<div data-rolle="status" class="screen__status">Wird vorbereitet …</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro vnc_zugangsdaten() %}
|
||||||
|
<form data-rolle="zugangsdaten" class="row row--tight" hidden onsubmit="return false">
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">Benutzer</span>
|
||||||
|
<input class="input" type="text" data-rolle="benutzer" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">Passwort</span>
|
||||||
|
<input class="input" type="password" data-rolle="passwort" autocomplete="off">
|
||||||
|
</label>
|
||||||
|
<button class="btn btn--primary" type="button" data-rolle="anmelden">Anmelden</button>
|
||||||
|
</form>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
{# Tasten, die der Arbeitsplatz selbst abfängt und die deshalb einen Knopf
|
||||||
|
brauchen: Strg+Alt+Entf schaltet der Browser gar nicht erst durch, Alt+F4
|
||||||
|
schlösse das Fenster hier statt das Fenster dort. #}
|
||||||
|
{% macro vnc_tasten() %}
|
||||||
|
<button class="btn btn--sm" type="button" data-rolle="taste-cad" hidden
|
||||||
|
title="Strg+Alt+Entf an das Gerät senden">Strg+Alt+Entf</button>
|
||||||
|
<button class="btn btn--sm" type="button" data-rolle="taste-altf4" hidden
|
||||||
|
title="Alt+F4 an das Gerät senden – schließt dort das oberste Fenster">Alt+F4</button>
|
||||||
|
<button class="btn btn--sm" type="button" data-rolle="taste-f5" hidden
|
||||||
|
title="F5 an das Gerät senden – der Browser des Arbeitsplatzes würde sonst diese Seite neu laden">F5</button>
|
||||||
|
<button class="btn btn--sm" type="button" data-rolle="trennen" hidden>Trennen</button>
|
||||||
|
{% endmacro %}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
{% extends "tesm_core/base.html" %}
|
{% extends "tesm_core/base.html" %}
|
||||||
{% from "tesm_core/macros.html" import search_box %}
|
{% from "tesm_core/macros.html" import search_box, modal %}
|
||||||
|
{% from "tesm/_vnc_flaeche.html" import vnc_flaeche, vnc_status, vnc_zugangsdaten, vnc_tasten %}
|
||||||
|
|
||||||
{% block page_title %}Statusübersicht{% endblock %}
|
{% block page_title %}Statusübersicht{% endblock %}
|
||||||
{% block page_sub %}
|
{% block page_sub %}
|
||||||
@@ -19,4 +20,27 @@
|
|||||||
data-refresh-interval="20000">
|
data-refresh-interval="20000">
|
||||||
{% include "tesm/_dashboard_tiles.html" %}
|
{% include "tesm/_dashboard_tiles.html" %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% if vnc_fenster %}
|
||||||
|
{# Ein Fenster für alle Kacheln: welches Gerät gemeint ist, bringt der Knopf
|
||||||
|
beim Öffnen mit. Ein Dialog je Kachel wäre dieselbe Sache dutzendfach im
|
||||||
|
Quelltext -- und dutzendfach eine Sitzung, die niemand aufräumt. #}
|
||||||
|
{% call modal('vnc-fenster', 'Bildschirm', 'Tastatur und Maus gehen an das Gerät. Die Zwischenablage bleibt getrennt.', 'wide') %}
|
||||||
|
<div class="modal__body stack" data-rolle="behaelter">
|
||||||
|
<div class="row row--tight">
|
||||||
|
<strong data-rolle="titel" class="flex-1"></strong>
|
||||||
|
{{ vnc_tasten() }}
|
||||||
|
</div>
|
||||||
|
{{ vnc_status() }}
|
||||||
|
{{ vnc_zugangsdaten() }}
|
||||||
|
{{ vnc_flaeche() }}
|
||||||
|
</div>
|
||||||
|
{% endcall %}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
{% if vnc_fenster %}
|
||||||
|
<script type="module" src="{{ asset_url('js/screen.js') }}" nonce="{{ csp_nonce }}"></script>
|
||||||
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -17,11 +17,8 @@
|
|||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if vnc and vnc.erreichbar %}
|
{% if vnc and vnc.erreichbar %}
|
||||||
{# Wie auf der Statusübersicht: der Knopf öffnet die Verbindung. Die
|
<a class="btn btn--sm" href="{{ url_for('screen.screen', device_id=device.id) }}"
|
||||||
Verbindungsdatei steht eine Zeile tiefer in den Stammdaten -- für
|
title="Bildschirm im Browser öffnen">
|
||||||
Arbeitsplätze, an denen kein Viewer das Schema übernimmt. #}
|
|
||||||
<a class="btn btn--sm" href="{{ vnc.adresse }}"
|
|
||||||
title="Bildschirm öffnen ({{ vnc.adresse }})">
|
|
||||||
{{ icon('monitor', size=15) }} VNC
|
{{ icon('monitor', size=15) }} VNC
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -238,12 +235,16 @@
|
|||||||
<dt>Bildschirm (VNC)</dt>
|
<dt>Bildschirm (VNC)</dt>
|
||||||
<dd>
|
<dd>
|
||||||
{% if vnc.erreichbar %}
|
{% if vnc.erreichbar %}
|
||||||
<a class="mono" href="{{ vnc.adresse }}">{{ vnc.adresse }}</a>
|
<span class="mono">{{ vnc.ziel }}:{{ vnc.port }}</span>
|
||||||
<div class="fs-xs faint">
|
<div class="fs-xs faint">
|
||||||
Öffnet den Viewer unmittelbar, sofern er <code>vnc://</code> übernimmt.
|
<a href="{{ url_for('screen.screen', device_id=device.id) }}">
|
||||||
Sonst die
|
Im Browser öffnen</a> – oder die
|
||||||
<a href="{{ url_for('devices.vnc_connection', device_id=device.id) }}">
|
<a href="{{ url_for('devices.vnc_connection', device_id=device.id) }}">
|
||||||
Verbindungsdatei</a> – sie enthält nur Ziel und Namen.
|
Verbindungsdatei</a> für einen Viewer auf dem Arbeitsplatz; sie
|
||||||
|
enthält nur Ziel und Namen.
|
||||||
|
{% if vnc.hinderungsgrund %}
|
||||||
|
<div class="mt-1">{{ vnc.hinderungsgrund }}</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="faint">
|
<span class="faint">
|
||||||
|
|||||||
@@ -377,6 +377,10 @@
|
|||||||
<div class="card__title">{{ icon('clock', size=17) }} Leases</div>
|
<div class="card__title">{{ icon('clock', size=17) }} Leases</div>
|
||||||
<div class="card__sub">
|
<div class="card__sub">
|
||||||
Direkt aus {{ lease_file }} gelesen – je Adresse gilt der letzte Eintrag.
|
Direkt aus {{ lease_file }} gelesen – je Adresse gilt der letzte Eintrag.
|
||||||
|
{% if current_user.has('dhcp_lease_adopt.read') and current_user.has('devices.create') %}
|
||||||
|
Eine <strong>dynamische</strong> Lease lässt sich als Client übernehmen; sie bekommt
|
||||||
|
damit eine feste Adresse und wird überwacht.
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -394,7 +398,7 @@
|
|||||||
<div class="table-wrap" style="max-height:420px;overflow:auto">
|
<div class="table-wrap" style="max-height:420px;overflow:auto">
|
||||||
<table class="table table--compact">
|
<table class="table table--compact">
|
||||||
<thead><tr><th>IP</th><th>MAC</th><th>Hostname</th><th>Art</th><th>Zustand</th>
|
<thead><tr><th>IP</th><th>MAC</th><th>Hostname</th><th>Art</th><th>Zustand</th>
|
||||||
<th>Läuft ab</th></tr></thead>
|
<th>Läuft ab</th><th></th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for lease in leases %}
|
{% for lease in leases %}
|
||||||
<tr>
|
<tr>
|
||||||
@@ -413,6 +417,31 @@
|
|||||||
<td class="fs-sm">{{ lease.expires_text }}
|
<td class="fs-sm">{{ lease.expires_text }}
|
||||||
<div class="fs-xs faint">{{ lease.remaining_text }}</div>
|
<div class="fs-xs faint">{{ lease.remaining_text }}</div>
|
||||||
</td>
|
</td>
|
||||||
|
<td class="nowrap">
|
||||||
|
{# Übernehmen gibt es nur für dynamische Leases: was reserviert
|
||||||
|
ist, gehört schon zu einem gepflegten Datensatz. Und nur,
|
||||||
|
wenn beide Rechte stehen -- hier entsteht ein Client. #}
|
||||||
|
{% if lease.address not in reserved_ips
|
||||||
|
and current_user.has('dhcp_lease_adopt.read')
|
||||||
|
and current_user.has('devices.create') %}
|
||||||
|
{% if lease.mac in bekannte_macs %}
|
||||||
|
<span class="faint fs-xs" title="Zu dieser MAC gibt es bereits einen Client">
|
||||||
|
bereits Client
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<form method="post" action="{{ url_for('dhcp.adopt_lease') }}">
|
||||||
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="mac" value="{{ lease.mac }}">
|
||||||
|
<button class="btn btn--sm btn--ghost" type="submit"
|
||||||
|
data-confirm="{{ lease.hostname or lease.address }} als überwachten Client anlegen? Switch, Port und Zugangsdaten tragen Sie danach nach."
|
||||||
|
data-confirm-title="Als Client übernehmen"
|
||||||
|
data-confirm-ok="Übernehmen">
|
||||||
|
{{ icon('plus', size=14) }} Als Client
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -242,8 +242,7 @@
|
|||||||
data-viewer-pdflib="{{ asset_url('vendor/pdflib/pdf-lib.min.js') }}"
|
data-viewer-pdflib="{{ asset_url('vendor/pdflib/pdf-lib.min.js') }}"
|
||||||
data-viewer-konto="{{ current_user.username }}"
|
data-viewer-konto="{{ current_user.username }}"
|
||||||
data-viewer-zeuge="{{ '1' if signature_witness else '0' }}"
|
data-viewer-zeuge="{{ '1' if signature_witness else '0' }}"
|
||||||
data-viewer-zeit="{{ serverzeit }}"
|
data-viewer-zeit="{{ serverzeit }}">
|
||||||
data-viewer-ordnerliste="{{ verzeichnisse | tojson }}">
|
|
||||||
<div class="viewer">
|
<div class="viewer">
|
||||||
<div class="viewer__stage" data-viewer-bildbuehne hidden>
|
<div class="viewer__stage" data-viewer-bildbuehne hidden>
|
||||||
<img class="viewer__img" alt="" data-viewer-bild>
|
<img class="viewer__img" alt="" data-viewer-bild>
|
||||||
|
|||||||
@@ -6,7 +6,8 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
{{ alert('Jeder Neustart wird mit Methode, Ergebnis, Dauer und Auslöser festgehalten -- automatisch
|
{{ alert('Jeder Neustart wird mit Methode, Ergebnis, Dauer und Auslöser festgehalten -- automatisch
|
||||||
wie manuell, über den PoE-Port des Switches ebenso wie per Neustart-Befehl aus der Wartung.
|
wie manuell, über den PoE-Port des Switches ebenso wie per Neustart-Befehl aus der Wartung.
|
||||||
Der Vorgänger protokollierte Neustarts ausdrücklich nicht.', 'info') }}
|
Auch ein Neustart, den die Überwachung auslösen wollte und nicht durfte, steht hier:
|
||||||
|
als "übersprungen" mit dem Grund. Der Vorgänger protokollierte Neustarts ausdrücklich nicht.', 'info') }}
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
@@ -32,7 +33,7 @@ Der Vorgänger protokollierte Neustarts ausdrücklich nicht.', 'info') }}
|
|||||||
</tr></thead>
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for row in rows %}
|
{% for row in rows %}
|
||||||
<tr data-filter-text="{{ row.device_name }} {{ row.result }} {{ row.actor }} {{ method_labels.get(row.method, row.method) }} {{ row.detail }}">
|
<tr data-filter-text="{{ row.device_name }} {{ row.result }} {{ row.actor }} {{ method_labels.get(row.method, row.method) }} {{ trigger_labels.get(row.trigger, row.trigger) }} {{ row.detail }}">
|
||||||
<td class="fs-sm nowrap" data-sort-value="{{ row.ts }}">{{ row.ts | dt }}</td>
|
<td class="fs-sm nowrap" data-sort-value="{{ row.ts }}">{{ row.ts | dt }}</td>
|
||||||
<td class="fw-600 fs-sm">{{ row.device_name or '–' | safe }}</td>
|
<td class="fw-600 fs-sm">{{ row.device_name or '–' | safe }}</td>
|
||||||
<td class="fs-sm">
|
<td class="fs-sm">
|
||||||
@@ -41,10 +42,15 @@ Der Vorgänger protokollierte Neustarts ausdrücklich nicht.', 'info') }}
|
|||||||
jahrelang als PoE ausgegeben. #}
|
jahrelang als PoE ausgegeben. #}
|
||||||
{% if row.method == 'ssh' %}{{ pill('SSH', 'info') }}
|
{% if row.method == 'ssh' %}{{ pill('SSH', 'info') }}
|
||||||
{% elif row.method == 'rpc' %}{{ pill('RPC', 'accent') }}
|
{% elif row.method == 'rpc' %}{{ pill('RPC', 'accent') }}
|
||||||
{% elif row.method == 'pö' %}{{ pill('PoE-Port', '') }}
|
{# 'poe', nicht 'pö'. Der Umlaut stand hier durch eine
|
||||||
|
Ersetzung, die "oe" fuer eine Umschrift hielt -- und traf den
|
||||||
|
haeufigsten Fall von allen: jeder automatische Neustart laeuft
|
||||||
|
ueber den PoE-Port. Sie standen damit als orangefarbenes
|
||||||
|
"poe" da, wo "PoE-Port" stehen sollte. #}
|
||||||
|
{% elif row.method == 'poe' %}{{ pill('PoE-Port', '') }}
|
||||||
{% else %}{{ pill(row.method or 'unbekannt', 'warning') }}{% endif %}
|
{% else %}{{ pill(row.method or 'unbekannt', 'warning') }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="fs-sm">{{ row.trigger }}</td>
|
<td class="fs-sm">{{ trigger_labels.get(row.trigger, row.trigger) }}</td>
|
||||||
<td class="fs-sm">{{ row.actor }}</td>
|
<td class="fs-sm">{{ row.actor }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if row.result == 'ok' %}{{ pill('erfolgreich', 'online') }}
|
{% if row.result == 'ok' %}{{ pill('erfolgreich', 'online') }}
|
||||||
|
|||||||
@@ -45,15 +45,78 @@ brauchen dafür weder SSH noch einen Host-Schlüssel, sondern die offene Datei-
|
|||||||
Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Port.',
|
Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Port.',
|
||||||
'info') }}
|
'info') }}
|
||||||
|
|
||||||
<form method="post" id="maintenance-form" action="{{ url_for('maintenance.start_update') }}">
|
{# ``stack``: Massenbefehl und Geräteauswahl sind zwei Bereiche und sollen auch
|
||||||
|
so aussehen. Ohne das stossen die beiden Karten aneinander. #}
|
||||||
|
<form method="post" id="maintenance-form" class="stack"
|
||||||
|
action="{{ url_for('maintenance.start_update') }}">
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
|
||||||
|
{% if current_user.has('servicing_command.read') %}
|
||||||
|
{# Zugeklappt beim Laden: der Massenbefehl ist das seltenere Werkzeug, die
|
||||||
|
Geräteliste darunter das tägliche. Aufgeklappt schob er sie aus dem
|
||||||
|
Bild. #}
|
||||||
|
<section class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<button class="btn btn--ghost btn--sm" type="button" data-befehl-schalter
|
||||||
|
aria-expanded="false" title="Massenbefehl ein- und ausklappen">
|
||||||
|
{{ icon('chevron-right', size=14) }}
|
||||||
|
</button>
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="card__title">{{ icon('terminal', size=17) }} Massenbefehl</div>
|
||||||
|
<div class="card__sub">
|
||||||
|
Eine Befehlszeile über SSH, auf allen unten ausgewählten Clients – jeder als
|
||||||
|
eigener Auftrag mit eigener Ausgabe. <strong>{{ '{sudo}' }}</strong> im Befehl
|
||||||
|
wird ersetzt: mit <code>sudo -n</code>, wo es ohne Passwort geht, sonst mit dem
|
||||||
|
hinterlegten Passwort über stdin. Ohne diese Marke läuft der Befehl als das
|
||||||
|
angemeldete Konto.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card__body stack" data-befehl-feld hidden>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">Vorlage</span>
|
||||||
|
<select class="select" id="befehlsvorlage">
|
||||||
|
<option value="">– eigene Eingabe –</option>
|
||||||
|
{% for name, befehl, erklaerung in befehlsvorlagen %}
|
||||||
|
<option value="{{ befehl }}" data-erklaerung="{{ erklaerung }}">{{ name }}</option>
|
||||||
|
{% endfor %}
|
||||||
|
</select>
|
||||||
|
<span class="field__hint" id="befehlserklaerung">
|
||||||
|
Eine Vorlage füllt nur das Feld darunter – abgeschickt wird von Hand.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span class="field__label">Befehl</span>
|
||||||
|
<input class="input mono" type="text" name="befehl" id="befehl"
|
||||||
|
maxlength="500" autocomplete="off"
|
||||||
|
placeholder='echo "Encryption=AlwaysOff" | {sudo} tee -a /etc/vnc/config.d/common.custom'
|
||||||
|
{% if not current_user.has('servicing_command.execute') %}disabled{% endif %}>
|
||||||
|
<span class="field__hint">
|
||||||
|
Genau eine Zeile. Was der Befehl bewirkt, weiß der, der ihn schreibt – TESM
|
||||||
|
führt ihn aus und hält fest, was zurückkam.
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="table-toolbar">
|
<div class="table-toolbar">
|
||||||
<label class="check">
|
<label class="check">
|
||||||
<input type="checkbox" id="select-all">
|
<input type="checkbox" id="select-all">
|
||||||
<span>Alle auswählen</span>
|
<span>Alle auswählen</span>
|
||||||
</label>
|
</label>
|
||||||
|
<button class="btn btn--sm btn--ghost" type="button" data-alle-einklappen>
|
||||||
|
{{ icon('chevron-up', size=14) }} Alle einklappen
|
||||||
|
</button>
|
||||||
<span class="table-toolbar__spacer"></span>
|
<span class="table-toolbar__spacer"></span>
|
||||||
|
{% if current_user.has('servicing_command.execute') %}
|
||||||
|
<button class="btn btn--sm" type="submit"
|
||||||
|
formaction="{{ url_for('maintenance.start_command') }}"
|
||||||
|
data-confirm="Den eingetragenen Befehl auf allen ausgewählten Geräten ausführen? Was er bewirkt, entscheidet er selbst – TESM führt ihn nur aus."
|
||||||
|
data-confirm-title="Befehl senden" data-confirm-ok="Senden" data-confirm-style="danger">
|
||||||
|
{{ icon('terminal', size=15) }} Befehl senden
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
{% if current_user.has('servicing_update.execute') %}
|
{% if current_user.has('servicing_update.execute') %}
|
||||||
<button class="btn btn--sm btn--primary" type="submit"
|
<button class="btn btn--sm btn--primary" type="submit"
|
||||||
formaction="{{ url_for('maintenance.start_update') }}"
|
formaction="{{ url_for('maintenance.start_update') }}"
|
||||||
@@ -79,6 +142,7 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
<th class="sortable">Gerät</th>
|
<th class="sortable">Gerät</th>
|
||||||
<th class="sortable">IP</th>
|
<th class="sortable">IP</th>
|
||||||
<th class="sortable">Zustand</th>
|
<th class="sortable">Zustand</th>
|
||||||
|
<th class="sortable">Auftrag</th>
|
||||||
<th class="sortable">Zugangsdaten</th>
|
<th class="sortable">Zugangsdaten</th>
|
||||||
<th class="sortable">Host-Schlüssel</th>
|
<th class="sortable">Host-Schlüssel</th>
|
||||||
<th></th>
|
<th></th>
|
||||||
@@ -95,13 +159,36 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
<span class="sr-only">{{ device.name }} auswählen</span>
|
<span class="sr-only">{{ device.name }} auswählen</span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="fw-600">{{ device.name }}</td>
|
<td class="fw-600">
|
||||||
|
{# Mitlesen, ohne die Seite zu verlassen -- so war es in der
|
||||||
|
Fassung 1.2.8, und es fehlte seither. Die Zeile darunter
|
||||||
|
klappt auf, sobald für dieses Gerät etwas läuft. #}
|
||||||
|
<button class="btn btn--ghost btn--sm" type="button"
|
||||||
|
data-ausgabe-schalter="{{ device.name }}"
|
||||||
|
title="Ausgabe dieses Geräts mitlesen">
|
||||||
|
{{ icon('chevron-right', size=13) }}
|
||||||
|
</button>
|
||||||
|
{{ device.name }}
|
||||||
|
</td>
|
||||||
<td class="mono fs-sm">{{ device.ip }}</td>
|
<td class="mono fs-sm">{{ device.ip }}</td>
|
||||||
<td>
|
<td data-zustand="{{ device.id }}">
|
||||||
{% if device.state == 'online' %}{{ pill('online', 'online') }}
|
{% if device.state == 'online' %}{{ pill('online', 'online') }}
|
||||||
{% elif device.state == 'offline' %}{{ pill('offline', 'offline') }}
|
{% elif device.state == 'offline' %}{{ pill('offline', 'offline') }}
|
||||||
{% else %}{{ pill('unbekannt', 'warning') }}{% endif %}
|
{% else %}{{ pill('unbekannt', 'warning') }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
{# Wie weit ist der Auftrag dieses Geräts? Ohne diese Spalte
|
||||||
|
blieb nur die Auftragsliste weiter unten -- und dort steht
|
||||||
|
nicht, welche Zeile zu welchem Gerät gehört, wenn vierzig
|
||||||
|
Aufträge in derselben Sekunde starten. #}
|
||||||
|
<td data-auftragslage="{{ device.name }}">
|
||||||
|
{% set lage = auftragslage.get(device.name) %}
|
||||||
|
{% if not lage %}<span class="faint fs-xs">–</span>
|
||||||
|
{% elif lage.state == 'running' %}{{ pill('läuft', 'info') }}
|
||||||
|
{% elif lage.state == 'pending' %}{{ pill('wartet', '') }}
|
||||||
|
{% elif lage.state == 'succeeded' %}{{ pill('fertig', 'online') }}
|
||||||
|
{% elif lage.state == 'failed' %}{{ pill('Fehler', 'danger') }}
|
||||||
|
{% else %}{{ pill('unklar', 'warning') }}{% endif %}
|
||||||
|
</td>
|
||||||
<td class="fs-sm">
|
<td class="fs-sm">
|
||||||
{{ device.credential_name }}
|
{{ device.credential_name }}
|
||||||
<div class="fs-xs faint">{{ category_labels.get(device.category, device.category) }}</div>
|
<div class="fs-xs faint">{{ category_labels.get(device.category, device.category) }}</div>
|
||||||
@@ -129,6 +216,11 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr data-ausgabe-zeile="{{ device.name }}" hidden>
|
||||||
|
<td colspan="8" class="p-0">
|
||||||
|
<pre class="job-output" data-ausgabe="{{ device.name }}">Noch keine Ausgabe.</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -161,13 +253,13 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
<thead><tr><th>Gestartet</th><th>Gerät</th><th>Art</th><th>Zustand</th><th>Fortschritt</th><th>Meldung</th></tr></thead>
|
<thead><tr><th>Gestartet</th><th>Gerät</th><th>Art</th><th>Zustand</th><th>Fortschritt</th><th>Meldung</th></tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for job in jobs %}
|
{% for job in jobs %}
|
||||||
<tr>
|
<tr data-auftrag="{{ job.id }}">
|
||||||
<td class="fs-sm nowrap">{{ job.created_at | dt }}</td>
|
<td class="fs-sm nowrap">{{ job.created_at | dt }}</td>
|
||||||
<td class="fs-sm fw-600">
|
<td class="fs-sm fw-600">
|
||||||
<a href="{{ url_for('maintenance.job_detail', job_id=job.id) }}">{{ job.subject }}</a>
|
<a href="{{ url_for('maintenance.job_detail', job_id=job.id) }}">{{ job.subject }}</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="fs-sm">{{ 'Aktualisierung' if job.kind.endswith('update') else 'Neustart' }}</td>
|
<td class="fs-sm">{{ 'Aktualisierung' if job.kind.endswith('update') else 'Neustart' }}</td>
|
||||||
<td>
|
<td data-auftrag-zustand>
|
||||||
{% if job.state == 'succeeded' %}{{ pill('erfolgreich', 'online') }}
|
{% if job.state == 'succeeded' %}{{ pill('erfolgreich', 'online') }}
|
||||||
{% elif job.state == 'failed' %}{{ pill('fehlgeschlagen', 'danger') }}
|
{% elif job.state == 'failed' %}{{ pill('fehlgeschlagen', 'danger') }}
|
||||||
{% elif job.state == 'running' %}{{ pill('läuft', 'info') }}
|
{% elif job.state == 'running' %}{{ pill('läuft', 'info') }}
|
||||||
@@ -175,11 +267,12 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
{% else %}{{ pill('wartet', '') }}{% endif %}
|
{% else %}{{ pill('wartet', '') }}{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td style="min-width:120px">
|
<td style="min-width:120px">
|
||||||
<div class="progress {{ 'progress--success' if job.state == 'succeeded' else ('progress--danger' if job.state == 'failed' else '') }}">
|
<div class="progress {{ 'progress--success' if job.state == 'succeeded' else ('progress--danger' if job.state == 'failed' else '') }}"
|
||||||
|
data-auftrag-balken>
|
||||||
<div class="progress__bar" style="width: {{ job.progress }}%"></div>
|
<div class="progress__bar" style="width: {{ job.progress }}%"></div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="fs-xs muted">{{ job.message }}</td>
|
<td class="fs-xs muted" data-auftrag-meldung>{{ job.message }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -193,6 +286,18 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
|
|
||||||
{% block scripts %}
|
{% block scripts %}
|
||||||
<script type="module" nonce="{{ csp_nonce }}">
|
<script type="module" nonce="{{ csp_nonce }}">
|
||||||
|
// Eine Vorlage füllt das Feld und erklärt sich -- abgeschickt wird von Hand.
|
||||||
|
const vorlage = document.getElementById("befehlsvorlage");
|
||||||
|
const befehl = document.getElementById("befehl");
|
||||||
|
const erklaerung = document.getElementById("befehlserklaerung");
|
||||||
|
vorlage?.addEventListener("change", () => {
|
||||||
|
const gewaehlt = vorlage.selectedOptions[0];
|
||||||
|
if (vorlage.value) befehl.value = vorlage.value;
|
||||||
|
erklaerung.textContent =
|
||||||
|
gewaehlt?.dataset.erklaerung ||
|
||||||
|
"Eine Vorlage füllt nur das Feld darunter – abgeschickt wird von Hand.";
|
||||||
|
});
|
||||||
|
|
||||||
// Auswahl beim Laden bewusst zurücksetzen: Browser stellen den Checked-Zustand
|
// Auswahl beim Laden bewusst zurücksetzen: Browser stellen den Checked-Zustand
|
||||||
// sonst aus dem Verlauf wieder her – und dann wird bei "Neustarten"
|
// sonst aus dem Verlauf wieder her – und dann wird bei "Neustarten"
|
||||||
// versehentlich das falsche Gerät erwischt.
|
// versehentlich das falsche Gerät erwischt.
|
||||||
@@ -205,14 +310,172 @@ Druckerfreigabe. Der PoE-Neustart ist davon getrennt: er schaltet den Switch-Por
|
|||||||
.forEach((box) => (box.checked = all.checked));
|
.forEach((box) => (box.checked = all.checked));
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auftragsliste nachladen, solange etwas läuft.
|
// Mitlesen: die Zeile unter dem Gerät zeigt, was dort gerade passiert.
|
||||||
|
//
|
||||||
|
// Vorher lud diese Seite sich alle zwei Sekunden komplett neu, solange etwas
|
||||||
|
// lief -- das verwarf jede Auswahl, jeden Tastendruck im Befehlsfeld und
|
||||||
|
// sprang nach oben. Jetzt kommen nur die letzten Zeilen, und die Seite
|
||||||
|
// bleibt stehen.
|
||||||
|
// Der Massenbefehl: zugeklappt geladen, per Kopfzeile aufgeklappt.
|
||||||
|
const befehlSchalter = document.querySelector("[data-befehl-schalter]");
|
||||||
|
const befehlFeld = document.querySelector("[data-befehl-feld]");
|
||||||
|
befehlSchalter?.addEventListener("click", () => {
|
||||||
|
const zu = befehlFeld.hidden;
|
||||||
|
befehlFeld.hidden = !zu;
|
||||||
|
befehlSchalter.setAttribute("aria-expanded", zu ? "true" : "false");
|
||||||
|
befehlSchalter.style.transform = zu ? "rotate(90deg)" : "";
|
||||||
|
if (zu) document.getElementById("befehl")?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
const schalter = Array.from(document.querySelectorAll("[data-ausgabe-schalter]"));
|
||||||
|
// Wer von Hand zuklappt, will es zu haben. Ohne dieses Gedaechtnis riss die
|
||||||
|
// naechste Statusabfrage die Zeile wieder auf, weil dort noch Ausgabe stand
|
||||||
|
// -- zwei Sekunden spaeter, jedes Mal.
|
||||||
|
const zugeklappt = new Set();
|
||||||
|
// Beim ersten Fuellen ans Ende springen, danach nur, wenn man ohnehin unten
|
||||||
|
// steht. So steht nach dem Neuladen der Seite das Aktuelle im Bild und nicht
|
||||||
|
// der Anfang von vorgestern -- und wer hochscrollt, um etwas zu lesen, wird
|
||||||
|
// nicht wieder heruntergezogen.
|
||||||
|
const gefuellt = new Set();
|
||||||
|
|
||||||
|
function klappen(name, zu) {
|
||||||
|
const zeile = document.querySelector(`[data-ausgabe-zeile="${CSS.escape(name)}"]`);
|
||||||
|
if (!zeile) return;
|
||||||
|
zeile.hidden = zu;
|
||||||
|
if (zu) zugeklappt.add(name);
|
||||||
|
else zugeklappt.delete(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
schalter.forEach((knopf) => {
|
||||||
|
knopf.addEventListener("click", () => {
|
||||||
|
const name = knopf.dataset.ausgabeSchalter;
|
||||||
|
const zeile = document.querySelector(`[data-ausgabe-zeile="${CSS.escape(name)}"]`);
|
||||||
|
if (zeile) klappen(name, !zeile.hidden);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
document.querySelector("[data-alle-einklappen]")?.addEventListener("click", () => {
|
||||||
|
schalter.forEach((knopf) => klappen(knopf.dataset.ausgabeSchalter, true));
|
||||||
|
});
|
||||||
|
|
||||||
|
function zeigen(name, text) {
|
||||||
|
const feld = document.querySelector(`[data-ausgabe="${CSS.escape(name)}"]`);
|
||||||
|
const zeile = document.querySelector(`[data-ausgabe-zeile="${CSS.escape(name)}"]`);
|
||||||
|
if (!feld || !zeile) return;
|
||||||
|
const erstmals = !gefuellt.has(name);
|
||||||
|
const unten = feld.scrollTop + feld.clientHeight >= feld.scrollHeight - 24;
|
||||||
|
feld.textContent = text || "Noch keine Ausgabe.";
|
||||||
|
if (text) gefuellt.add(name);
|
||||||
|
if (erstmals || unten) feld.scrollTop = feld.scrollHeight;
|
||||||
|
// Was läuft, klappt von selbst auf -- wer zusieht, will nicht erst suchen.
|
||||||
|
if (text && zeile.hidden && !zugeklappt.has(name)) zeile.hidden = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zustand, Fortschritt und Meldung eines Auftrags nachziehen.
|
||||||
|
//
|
||||||
|
// Vorher lud die Seite sich dafür komplett neu, sobald der letzte Auftrag
|
||||||
|
// durch war -- und riss damit genau das weg, was man lesen wollte: die
|
||||||
|
// Ausgabe stand da, dann sprang die Seite, und danach war sie leer.
|
||||||
|
const ZUSTAENDE = {
|
||||||
|
succeeded: ["erfolgreich", "pill--online", "progress--success"],
|
||||||
|
failed: ["fehlgeschlagen", "pill--danger", "progress--danger"],
|
||||||
|
running: ["läuft", "pill--info", ""],
|
||||||
|
unknown: ["unklar", "pill--warning", ""],
|
||||||
|
pending: ["wartet", "", ""],
|
||||||
|
};
|
||||||
|
|
||||||
|
function auftragZeigen(job) {
|
||||||
|
const zeile = document.querySelector(`[data-auftrag="${CSS.escape(job.id)}"]`);
|
||||||
|
if (!zeile) return false;
|
||||||
|
const [text, pillenKlasse, balkenKlasse] = ZUSTAENDE[job.state] || ZUSTAENDE.pending;
|
||||||
|
const pille = zeile.querySelector("[data-auftrag-zustand] .pill");
|
||||||
|
if (pille && pille.textContent.trim() !== text) {
|
||||||
|
pille.textContent = text;
|
||||||
|
pille.className = `pill ${pillenKlasse}`.trim();
|
||||||
|
}
|
||||||
|
const balken = zeile.querySelector("[data-auftrag-balken]");
|
||||||
|
if (balken) {
|
||||||
|
balken.className = `progress ${balkenKlasse}`.trim();
|
||||||
|
const inner = balken.querySelector(".progress__bar");
|
||||||
|
if (inner) inner.style.width = `${job.progress || 0}%`;
|
||||||
|
}
|
||||||
|
const meldung = zeile.querySelector("[data-auftrag-meldung]");
|
||||||
|
if (meldung && meldung.textContent.trim() !== (job.message || "")) {
|
||||||
|
meldung.textContent = job.message || "";
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dieselben fünf Zustände wie in der Auftragsliste -- hier aber neben dem
|
||||||
|
// Gerät, wo man sie sucht.
|
||||||
|
const LAGE = {
|
||||||
|
pending: ["wartet", ""],
|
||||||
|
running: ["läuft", "pill--info"],
|
||||||
|
succeeded: ["fertig", "pill--online"],
|
||||||
|
failed: ["Fehler", "pill--danger"],
|
||||||
|
unknown: ["unklar", "pill--warning"],
|
||||||
|
};
|
||||||
|
|
||||||
|
function lageZeigen(name, zustand) {
|
||||||
|
const zelle = document.querySelector(`[data-auftragslage="${CSS.escape(name)}"]`);
|
||||||
|
if (!zelle) return;
|
||||||
|
const [text, klasse] = LAGE[zustand] || LAGE.pending;
|
||||||
|
const vorhanden = zelle.querySelector(".pill");
|
||||||
|
if (vorhanden && vorhanden.textContent.trim() === text) return;
|
||||||
|
const pille = document.createElement("span");
|
||||||
|
pille.className = `pill ${klasse}`.trim();
|
||||||
|
pille.textContent = text;
|
||||||
|
zelle.replaceChildren(pille);
|
||||||
|
}
|
||||||
|
|
||||||
|
let neueAuftraege = false;
|
||||||
async function poll() {
|
async function poll() {
|
||||||
|
let weiter = 10000;
|
||||||
try {
|
try {
|
||||||
const state = await window.tesm.api("{{ url_for('maintenance.status') }}");
|
const state = await window.tesm.api("{{ url_for('maintenance.status') }}", { auto: true });
|
||||||
if (state.running) window.setTimeout(() => location.reload(), state.poll_ms);
|
weiter = state.poll_ms || 10000;
|
||||||
|
// Der neueste Auftrag je Gerät gewinnt; die Liste kommt neueste zuerst.
|
||||||
|
const gesehen = new Set();
|
||||||
|
state.jobs.forEach((job) => {
|
||||||
|
if (!gesehen.has(job.subject)) {
|
||||||
|
gesehen.add(job.subject);
|
||||||
|
lageZeigen(job.subject, job.state);
|
||||||
|
}
|
||||||
|
if (job.output) zeigen(job.subject, job.output);
|
||||||
|
// Ein Auftrag, der in der Tabelle noch gar nicht steht, ist neu --
|
||||||
|
// dafür braucht es die Seite einmal frisch, aber erst, wenn nichts
|
||||||
|
// mehr läuft.
|
||||||
|
if (!auftragZeigen(job)) neueAuftraege = true;
|
||||||
|
});
|
||||||
|
// Erreichbarkeit mitziehen -- sonst steht die Seite mit dem Stand ihres
|
||||||
|
// Aufbaus da, und wer nach einem Neustart auf die Rückkehr wartet, sieht
|
||||||
|
// "unbekannt", bis er von Hand neu lädt.
|
||||||
|
(state.devices || []).forEach((geraet) => {
|
||||||
|
const zelle = document.querySelector(`[data-zustand="${geraet.id}"]`);
|
||||||
|
if (!zelle) return;
|
||||||
|
const [text, art] =
|
||||||
|
geraet.state === "online"
|
||||||
|
? ["online", "pill--online"]
|
||||||
|
: geraet.state === "offline"
|
||||||
|
? ["offline", "pill--offline"]
|
||||||
|
: ["unbekannt", "pill--warning"];
|
||||||
|
const pille = zelle.querySelector(".pill");
|
||||||
|
if (pille && pille.textContent.trim() !== text) {
|
||||||
|
pille.textContent = text;
|
||||||
|
pille.className = `pill ${art}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Neu laden nur, wenn Aufträge dazugekommen sind, die in der Tabelle
|
||||||
|
// fehlen -- und auch dann erst, wenn nichts mehr läuft. Wer zusieht,
|
||||||
|
// soll nicht mitten im Lesen weggerissen werden.
|
||||||
|
if (neueAuftraege && !state.running) {
|
||||||
|
window.setTimeout(() => location.reload(), 8000);
|
||||||
|
neueAuftraege = false;
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
/* stumm bleiben – die Seite ist auch ohne Aktualisierung nutzbar */
|
/* stumm bleiben – die Seite ist auch ohne Aktualisierung nutzbar */
|
||||||
}
|
}
|
||||||
|
window.setTimeout(poll, weiter);
|
||||||
}
|
}
|
||||||
poll();
|
poll();
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
{% extends "tesm_core/base.html" %}
|
||||||
|
{% from "tesm_core/macros.html" import alert, pill %}
|
||||||
|
{% from "tesm/_vnc_flaeche.html" import vnc_flaeche, vnc_status, vnc_zugangsdaten, vnc_tasten %}
|
||||||
|
|
||||||
|
{% block page_title %}Bildschirm: {{ device.name }}{% endblock %}
|
||||||
|
{% block page_sub %}{{ zugang.ziel }}:{{ zugang.port }}{% endblock %}
|
||||||
|
|
||||||
|
{% block breadcrumb %}
|
||||||
|
<nav class="breadcrumb">
|
||||||
|
<span><a href="{{ url_for('devices.overview') }}">Clients</a></span>
|
||||||
|
<span><a href="{{ url_for('devices.detail', device_id=device.id) }}">{{ device.name }}</a></span>
|
||||||
|
<span>Bildschirm</span>
|
||||||
|
</nav>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="stack">
|
||||||
|
{% if zugang.hinderungsgrund %}
|
||||||
|
{{ alert(zugang.hinderungsgrund, 'warning') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="card__title">
|
||||||
|
{{ icon('monitor', size=17) }} {{ device.name }}
|
||||||
|
{% if zugang.absage %}{{ pill('Verbindung abgelehnt', 'warning') }}
|
||||||
|
{% elif zugang.verfahren %}
|
||||||
|
{% if zugang.browsertauglich %}{{ pill('Browser-Sitzung möglich', 'online') }}
|
||||||
|
{% else %}{{ pill('Verfahren nicht unterstützt', 'warning') }}{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="card__sub">
|
||||||
|
Tastatur und Maus gehen an das Gerät, sobald das Bild steht. Die Zwischenablage
|
||||||
|
bleibt getrennt.
|
||||||
|
{% if zugang.verfahren %}
|
||||||
|
Zuletzt angebotene Anmeldeverfahren:
|
||||||
|
{% for nummer in zugang.verfahren %}{{ verfahren_name(nummer) }}{% if not loop.last %}, {% endif %}{% endfor %}.
|
||||||
|
{% else %}
|
||||||
|
Welche Anmeldeverfahren das Gerät anbietet, ist nicht bekannt – die Seite
|
||||||
|
fragt es nicht von sich aus.
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row row--tight">
|
||||||
|
{{ vnc_tasten() }}
|
||||||
|
{# Ausdrücklich ein Knopf und keine Nebenwirkung des Seitenaufbaus: die
|
||||||
|
Prüfung bricht eine Protokollverhandlung ab, und RealVNC zählt das.
|
||||||
|
Wer sucht, drückt ihn; wer nur arbeiten will, merkt nichts davon. #}
|
||||||
|
<form method="post" data-no-guard
|
||||||
|
action="{{ url_for('screen.probe', device_id=device.id) }}">
|
||||||
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
<button class="btn btn--sm btn--ghost" type="submit"
|
||||||
|
title="Fragt das Gerät, welche Anmeldeverfahren es anbietet. Das ist
|
||||||
|
eine eigene Verbindung, die abgebrochen wird – ein RealVNC Server zählt so etwas mit.
|
||||||
|
Nicht unmittelbar vor dem Verbinden drücken.">
|
||||||
|
Verfahren prüfen
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<a class="btn btn--sm btn--ghost"
|
||||||
|
href="{{ url_for('devices.vnc_connection', device_id=device.id) }}">
|
||||||
|
{{ icon('download', size=14) }} Verbindungsdatei
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card__body stack" id="screen" data-sitzung="{{ quelle }}">
|
||||||
|
{{ vnc_status() }}
|
||||||
|
{{ vnc_zugangsdaten() }}
|
||||||
|
{{ vnc_flaeche() }}
|
||||||
|
|
||||||
|
{% if not darf_geheimnis and device.credential_id %}
|
||||||
|
<p class="fs-xs faint">
|
||||||
|
Für diesen Client sind Zugangsdaten hinterlegt. TESM gibt sie nur an Konten
|
||||||
|
heraus, die Geheimnisse ohnehin einsehen dürfen – sonst wären sie über den
|
||||||
|
Umweg einer Bildschirmsitzung für jeden lesbar, der sie öffnen darf. Tragen
|
||||||
|
Sie sie oben ein; sie werden nicht gespeichert.
|
||||||
|
</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block scripts %}
|
||||||
|
<script type="module" src="{{ asset_url('js/screen.js') }}" nonce="{{ csp_nonce }}"></script>
|
||||||
|
{% endblock %}
|
||||||
@@ -46,13 +46,6 @@
|
|||||||
{{ checkbox('dashboard_public', 'Statusübersicht ohne Anmeldung erreichbar',
|
{{ checkbox('dashboard_public', 'Statusübersicht ohne Anmeldung erreichbar',
|
||||||
checked=values.dashboard_public,
|
checked=values.dashboard_public,
|
||||||
hint_text=specs['dashboard_public'].hint) }}
|
hint_text=specs['dashboard_public'].hint) }}
|
||||||
{{ checkbox('vnc_enabled', 'Bildschirmzugriff (VNC) anbieten',
|
|
||||||
checked=values.vnc_enabled,
|
|
||||||
hint_text=specs['vnc_enabled'].hint) }}
|
|
||||||
<div class="form-grid">
|
|
||||||
{{ field('vnc_port', 'VNC-Port', value=values.vnc_port, type='number',
|
|
||||||
hint_text=specs['vnc_port'].hint) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="card__footer"><button class="btn btn--primary" type="submit">Speichern</button></div>
|
<div class="card__footer"><button class="btn btn--primary" type="submit">Speichern</button></div>
|
||||||
</form>
|
</form>
|
||||||
@@ -113,6 +106,39 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="card__title">{{ icon('monitor', size=17) }} Bildschirm (VNC)</div>
|
||||||
|
<div class="card__sub">
|
||||||
|
Dieselben Regeln wie bei der Konsole: eigenes Recht, frische
|
||||||
|
Passwortbestätigung, getrennte Zwischenablage. Der Knopf erscheint nur, wo
|
||||||
|
ein VNC-Server antwortet – das prüft der Überwachungsdurchlauf mit.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ url_for('settings.system_save') }}">
|
||||||
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
<div class="card__body stack">
|
||||||
|
{{ checkbox('vnc_enabled', 'Bildschirmzugriff (VNC) anbieten',
|
||||||
|
checked=values.vnc_enabled,
|
||||||
|
hint_text=specs['vnc_enabled'].hint) }}
|
||||||
|
<div class="form-grid">
|
||||||
|
{{ field('vnc_port', 'VNC-Port', value=values.vnc_port, type='number',
|
||||||
|
hint_text=specs['vnc_port'].hint) }}
|
||||||
|
{{ field('vnc_idle_timeout', 'Trennen bei Untätigkeit (Sekunden)',
|
||||||
|
value=values.vnc_idle_timeout, type='number',
|
||||||
|
hint_text=specs['vnc_idle_timeout'].hint) }}
|
||||||
|
</div>
|
||||||
|
{{ checkbox('vnc_warn_unencrypted_off',
|
||||||
|
'Warnung des Viewers vor unverschlüsselter Verbindung unterdrücken',
|
||||||
|
checked=values.vnc_warn_unencrypted_off,
|
||||||
|
hint_text=specs['vnc_warn_unencrypted_off'].hint) }}
|
||||||
|
</div>
|
||||||
|
<div class="card__footer"><button class="btn btn--primary" type="submit">Speichern</button></div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
|
|||||||
@@ -113,7 +113,11 @@ class MonitorDaemon:
|
|||||||
time.sleep(min(1.0, remaining))
|
time.sleep(min(1.0, remaining))
|
||||||
remaining -= 1.0
|
remaining -= 1.0
|
||||||
|
|
||||||
live.info("monitor", "Überwachungsprozess beendet")
|
# Warnstufe, nicht INFO: dass die Ueberwachung *laeuft*, ist der
|
||||||
|
# Normalfall und darf gruen sein. Dass sie **steht**, ist der Zustand,
|
||||||
|
# in dem niemand mehr erfaehrt, dass ein Geraet weg ist -- und genau
|
||||||
|
# diese Zeile sucht man hinterher im Protokoll.
|
||||||
|
live.warning("monitor", "Überwachungsprozess beendet")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+70
-20
@@ -39,6 +39,7 @@ HTTPS=0
|
|||||||
HTTPS_PORT=""
|
HTTPS_PORT=""
|
||||||
DOMAIN=""
|
DOMAIN=""
|
||||||
ACME_EMAIL=""
|
ACME_EMAIL=""
|
||||||
|
ERZWINGEN=0
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||||
@@ -49,6 +50,39 @@ warn() { printf '%s !!%s %s\n' "$COLOR_WARN" "$COLOR_OFF" "$*"; }
|
|||||||
fail() { printf '%s !!%s %s\n' "$COLOR_ERR" "$COLOR_OFF" "$*" >&2; exit 1; }
|
fail() { printf '%s !!%s %s\n' "$COLOR_ERR" "$COLOR_OFF" "$*" >&2; exit 1; }
|
||||||
info() { printf ' %s\n' "$*"; }
|
info() { printf ' %s\n' "$*"; }
|
||||||
|
|
||||||
|
# -- Welche Fassung liegt hier, und welche laeuft schon? ---------------------
|
||||||
|
#
|
||||||
|
# Ein Installationslauf auf dieselbe Fassung macht nichts besser: er baut die
|
||||||
|
# Python-Umgebung neu, kopiert dieselben Dateien, laedt nginx neu und startet
|
||||||
|
# den Dienst -- mit der einzigen Wirkung, dass die Anwendung ein paar Sekunden
|
||||||
|
# weg ist. Wer "update.sh" in einen Zeitplan haengt, bekaeme das taeglich.
|
||||||
|
#
|
||||||
|
# Verglichen wird die Anwendung **und** der Kern: die beiden Nummern laufen
|
||||||
|
# absichtlich auseinander (siehe pyproject.toml), und eine Auslieferung kann
|
||||||
|
# denselben Anwendungsstand mit neuem Kern enthalten.
|
||||||
|
|
||||||
|
#: Die Nummer aus einer pyproject.toml. Die erste Zeile "version = ..." gilt --
|
||||||
|
#: weitere Vorkommen stehen in Abhaengigkeiten und gehen uns nichts an.
|
||||||
|
paket_version() {
|
||||||
|
sed -n 's/^version = "\([^"]*\)"/\1/p' "$1" 2>/dev/null | head -1
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Was in dieser Installation laeuft: "<anwendung> <kern>", sonst leer.
|
||||||
|
laufende_version() {
|
||||||
|
local wurzel="$1" modul="$2"
|
||||||
|
[[ -x "$wurzel/venv/bin/python" ]] || return 0
|
||||||
|
"$wurzel/venv/bin/python" -c \
|
||||||
|
"import $modul, tesm_core; print($modul.__version__, tesm_core.__version__)" \
|
||||||
|
2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Ist das, was hier liegt, schon installiert? Eine eigene Funktion, weil genau
|
||||||
|
#: diese Frage sich pruefen lassen muss -- ohne einen Server aufzusetzen.
|
||||||
|
gleiche_version() {
|
||||||
|
local laeuft="$1" liegt="$2"
|
||||||
|
[[ -n "$laeuft" && "$laeuft" == "$liegt" ]]
|
||||||
|
}
|
||||||
|
|
||||||
#: Merker fuer die EXIT-Falle. Zurueckgerollt wird nur, was auch gesichert ist.
|
#: Merker fuer die EXIT-Falle. Zurueckgerollt wird nur, was auch gesichert ist.
|
||||||
SICHERUNG_STEHT=0
|
SICHERUNG_STEHT=0
|
||||||
INSTALL_FERTIG=0
|
INSTALL_FERTIG=0
|
||||||
@@ -129,6 +163,7 @@ Verwendung: install.sh --app <tesm|tesm-license> [Optionen]
|
|||||||
--acme-email <a> Kontaktadresse fuer Let's Encrypt.
|
--acme-email <a> Kontaktadresse fuer Let's Encrypt.
|
||||||
--skip-packages Keine Systempakete installieren
|
--skip-packages Keine Systempakete installieren
|
||||||
--yes Rueckfragen ueberspringen
|
--yes Rueckfragen ueberspringen
|
||||||
|
--force Auch dann einrichten, wenn dieselbe Fassung schon laeuft
|
||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,6 +179,7 @@ while [[ $# -gt 0 ]]; do
|
|||||||
--acme-email) ACME_EMAIL="${2:?}"; shift 2 ;;
|
--acme-email) ACME_EMAIL="${2:?}"; shift 2 ;;
|
||||||
--skip-packages) SKIP_PACKAGES=1; shift ;;
|
--skip-packages) SKIP_PACKAGES=1; shift ;;
|
||||||
--yes|-y) ASSUME_YES=1; shift ;;
|
--yes|-y) ASSUME_YES=1; shift ;;
|
||||||
|
--force) ERZWINGEN=1; shift ;;
|
||||||
-h|--help) usage; exit 0 ;;
|
-h|--help) usage; exit 0 ;;
|
||||||
*) fail "unbekannte Option: $1" ;;
|
*) fail "unbekannte Option: $1" ;;
|
||||||
esac
|
esac
|
||||||
@@ -261,6 +297,23 @@ FAILED_DIR="/srv/$NAME-failed-$TIMESTAMP"
|
|||||||
IS_UPDATE=0
|
IS_UPDATE=0
|
||||||
[[ -f "$ROOT/data/app.db" ]] && IS_UPDATE=1
|
[[ -f "$ROOT/data/app.db" ]] && IS_UPDATE=1
|
||||||
|
|
||||||
|
# Laeuft hier schon genau das, was hier liegt? Dann ist Schluss, bevor
|
||||||
|
# irgendetwas angefasst wird -- und zwar mit Ausstiegscode 0: es ist nichts
|
||||||
|
# schiefgegangen, es war nur nichts zu tun.
|
||||||
|
MODUL="tesm"
|
||||||
|
[[ "$APP" == "tesm-license" ]] && MODUL="tesm_license"
|
||||||
|
LIEGT="$(paket_version "$REPO_DIR/apps/$APP/pyproject.toml") $(
|
||||||
|
paket_version "$REPO_DIR/packages/tesm-core/pyproject.toml")"
|
||||||
|
LAEUFT="$(laufende_version "$ROOT" "$MODUL")"
|
||||||
|
if [[ $IS_UPDATE -eq 1 && $ERZWINGEN -eq 0 ]] && gleiche_version "$LAEUFT" "$LIEGT"; then
|
||||||
|
step "Nichts zu tun"
|
||||||
|
info "$APP ${LIEGT%% *} (tesm-core ${LIEGT##* }) laeuft hier bereits."
|
||||||
|
info "Ein zweiter Durchlauf wuerde nur die Umgebung neu bauen und den"
|
||||||
|
info "Dienst neu starten -- ohne dass sich etwas aendert."
|
||||||
|
info "Trotzdem neu einrichten: --force"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
step "TESM-Installation"
|
step "TESM-Installation"
|
||||||
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
|
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
|
||||||
info "Ziel : $ROOT"
|
info "Ziel : $ROOT"
|
||||||
@@ -278,6 +331,7 @@ else
|
|||||||
info "nginx : Port $HTTP_PORT (nur HTTP)"
|
info "nginx : Port $HTTP_PORT (nur HTTP)"
|
||||||
fi
|
fi
|
||||||
info "Modus : $([[ $IS_UPDATE -eq 1 ]] && echo Aktualisierung || echo Neuinstallation)"
|
info "Modus : $([[ $IS_UPDATE -eq 1 ]] && echo Aktualisierung || echo Neuinstallation)"
|
||||||
|
info "Fassung : ${LIEGT%% *} (tesm-core ${LIEGT##* })${LAEUFT:+, bisher ${LAEUFT%% *}}"
|
||||||
|
|
||||||
if [[ $ASSUME_YES -eq 0 ]]; then
|
if [[ $ASSUME_YES -eq 0 ]]; then
|
||||||
read -r -p "Fortfahren? [j/N] " answer
|
read -r -p "Fortfahren? [j/N] " answer
|
||||||
@@ -364,26 +418,16 @@ info "Dateien abgeglichen (Instanzdaten unter data/ bleiben unangetastet)"
|
|||||||
|
|
||||||
# -- venv --------------------------------------------------------------------
|
# -- venv --------------------------------------------------------------------
|
||||||
step "Python-Umgebung"
|
step "Python-Umgebung"
|
||||||
if [[ ! -x "$ROOT/venv/bin/python3" ]]; then
|
# Aufbauen, pruefen und reparieren steht in genau einem Programm -- und
|
||||||
python3 -m venv "$ROOT/venv"
|
# dasselbe laeuft als ExecStartPre vor jedem Dienststart. Zwei getrennte
|
||||||
info "venv erzeugt"
|
# Paketlisten waeren zwei Listen, die auseinanderlaufen: die Reparatur baute
|
||||||
fi
|
# dann eine Umgebung, die nicht mehr der Installation entspricht.
|
||||||
# Immer ueber "python3 -m pip" -- der pip-Shim im venv baeckt einen absoluten
|
# Ueber "bash" und nicht als Programm: Dateien im Baum tragen kein
|
||||||
# Pfad in seinen Shebang, der nach einer Umbenennung des Verzeichnisses bricht.
|
# Ausfuehrungsrecht -- git kennt auf einer Windows-Buildmaschine keines, und
|
||||||
"$ROOT/venv/bin/python3" -m pip install --quiet --upgrade pip setuptools wheel
|
# install.sh, tesm-helper und tesm-motd werden aus demselben Grund ebenso
|
||||||
PAKETE=("$ROOT/packages/tesm-licensing" "$ROOT/packages/tesm-core")
|
# aufgerufen oder mit ausdruecklichem Modus installiert. Ohne das brach die
|
||||||
# Die Serverhaelfte des Lizenzprotokolls -- ausstellen, erneuern, antworten --
|
# Aktualisierung mit "Permission denied" ab (und rollte zurueck).
|
||||||
# gehoert ausschliesslich zum Lizenzserver. Ein Client verifiziert Lizenzen und
|
bash "$ROOT/deploy/tesm-venv" "$ROOT" "$APP" --immer
|
||||||
# stellt Anfragen; den Code zum Ausstellen soll er nicht einmal mitbringen.
|
|
||||||
if [[ "$APP" == "tesm-license" ]]; then
|
|
||||||
[[ -d "$ROOT/packages/tesm-licensing-server" ]] ||
|
|
||||||
fail "packages/tesm-licensing-server fehlt -- ohne sie kann der Lizenzserver nichts ausstellen"
|
|
||||||
PAKETE+=("$ROOT/packages/tesm-licensing-server")
|
|
||||||
fi
|
|
||||||
"$ROOT/venv/bin/python3" -m pip install --quiet "${PAKETE[@]}" "$ROOT/apps/$APP" gunicorn
|
|
||||||
if [[ "$APP" == "tesm" ]]; then
|
|
||||||
"$ROOT/venv/bin/python3" -m pip install --quiet flask-sock
|
|
||||||
fi
|
|
||||||
info "Abhaengigkeiten installiert"
|
info "Abhaengigkeiten installiert"
|
||||||
|
|
||||||
# -- Statische Dateien fuer nginx --------------------------------------------
|
# -- Statische Dateien fuer nginx --------------------------------------------
|
||||||
@@ -413,6 +457,12 @@ install_if_changed() {
|
|||||||
install_if_changed "$ROOT/deploy/tesm-helper" /usr/local/lib/tesm/tesm-helper 0755 && \
|
install_if_changed "$ROOT/deploy/tesm-helper" /usr/local/lib/tesm/tesm-helper 0755 && \
|
||||||
info "Helfer aktualisiert" || info "Helfer unveraendert"
|
info "Helfer aktualisiert" || info "Helfer unveraendert"
|
||||||
|
|
||||||
|
# Derselbe Baustein, der oben die Umgebung gebaut hat: die Dienste rufen ihn
|
||||||
|
# vor jedem Start auf und reparieren damit, was ein Release-Upgrade des
|
||||||
|
# Betriebssystems ihnen unter den Fuessen weggezogen hat.
|
||||||
|
install_if_changed "$ROOT/deploy/tesm-venv" /usr/local/lib/tesm/tesm-venv 0755 >/dev/null \
|
||||||
|
&& info "Umgebungspfleger aktualisiert" || true
|
||||||
|
|
||||||
# visudo -c prueft die Datei, bevor sie aktiv wird -- eine kaputte sudoers-Datei
|
# visudo -c prueft die Datei, bevor sie aktiv wird -- eine kaputte sudoers-Datei
|
||||||
# waere sonst ein Totalausfall der Administration.
|
# waere sonst ein Totalausfall der Administration.
|
||||||
TMP_SUDO="$(mktemp)"
|
TMP_SUDO="$(mktemp)"
|
||||||
|
|||||||
@@ -15,11 +15,23 @@ Environment=TESM_LICENSE_LOG_DIR=/var/log/tesm-license
|
|||||||
Environment=TESM_LICENSE_WEB_PROCESS=1
|
Environment=TESM_LICENSE_WEB_PROCESS=1
|
||||||
Environment=TESM_LICENSE_COOKIE_SECURE=1
|
Environment=TESM_LICENSE_COOKIE_SECURE=1
|
||||||
Environment=PYTHONUNBUFFERED=1
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
# Vor jedem Start: traegt die Python-Umgebung noch? Ein Release-Upgrade des
|
||||||
|
# Betriebssystems tauscht die Minor-Fassung von Python aus, und der venv haengt
|
||||||
|
# daran -- danach findet der Dienst kein einziges Paket mehr und scheitert mit
|
||||||
|
# "No module named gunicorn". Ist alles heil, kostet diese Zeile Millisekunden;
|
||||||
|
# sonst baut sie die Umgebung neu, einmal je Systemstart.
|
||||||
|
#
|
||||||
|
# Das "+" laesst den Schritt als root laufen. Noetig, weil der venv root
|
||||||
|
# gehoert, der Dienst aber als unprivilegierter Benutzer laeuft.
|
||||||
|
ExecStartPre=+/usr/local/lib/tesm/tesm-venv /srv/tesm-license tesm-license
|
||||||
ExecStart=/srv/tesm-license/venv/bin/gunicorn \
|
ExecStart=/srv/tesm-license/venv/bin/gunicorn \
|
||||||
--workers 1 --worker-class gthread --threads 8 \
|
--workers 1 --worker-class gthread --threads 8 \
|
||||||
--bind 127.0.0.1:5001 \
|
--bind 127.0.0.1:5001 \
|
||||||
--access-logfile - --error-logfile - \
|
--access-logfile - --error-logfile - \
|
||||||
tesm_license.wsgi:app
|
tesm_license.wsgi:app
|
||||||
|
# Ein Neuaufbau der Python-Umgebung laedt Pakete und dauert Minuten --
|
||||||
|
# die Vorgabe von 90 Sekunden wuerde ihn mittendrin abschneiden.
|
||||||
|
TimeoutStartSec=900
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,19 @@ Environment=TESM_BASE_DIR=/srv/tesm
|
|||||||
Environment=TESM_DATA_DIR=/srv/tesm/data
|
Environment=TESM_DATA_DIR=/srv/tesm/data
|
||||||
Environment=TESM_LOG_DIR=/var/log/tesm
|
Environment=TESM_LOG_DIR=/var/log/tesm
|
||||||
Environment=PYTHONUNBUFFERED=1
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
# Vor jedem Start: traegt die Python-Umgebung noch? Ein Release-Upgrade des
|
||||||
|
# Betriebssystems tauscht die Minor-Fassung von Python aus, und der venv haengt
|
||||||
|
# daran -- danach findet der Dienst kein einziges Paket mehr und scheitert mit
|
||||||
|
# "No module named gunicorn". Ist alles heil, kostet diese Zeile Millisekunden;
|
||||||
|
# sonst baut sie die Umgebung neu, einmal je Systemstart.
|
||||||
|
#
|
||||||
|
# Das "+" laesst den Schritt als root laufen. Noetig, weil der venv root
|
||||||
|
# gehoert, der Dienst aber als unprivilegierter Benutzer laeuft.
|
||||||
|
ExecStartPre=+/usr/local/lib/tesm/tesm-venv /srv/tesm tesm
|
||||||
ExecStart=/srv/tesm/venv/bin/tesm-monitor
|
ExecStart=/srv/tesm/venv/bin/tesm-monitor
|
||||||
|
# Ein Neuaufbau der Python-Umgebung laedt Pakete und dauert Minuten --
|
||||||
|
# die Vorgabe von 90 Sekunden wuerde ihn mittendrin abschneiden.
|
||||||
|
TimeoutStartSec=900
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
|
|
||||||
|
|||||||
@@ -15,11 +15,23 @@ Environment=TESM_LOG_DIR=/var/log/tesm
|
|||||||
Environment=TESM_WEB_PROCESS=1
|
Environment=TESM_WEB_PROCESS=1
|
||||||
Environment=TESM_COOKIE_SECURE=1
|
Environment=TESM_COOKIE_SECURE=1
|
||||||
Environment=PYTHONUNBUFFERED=1
|
Environment=PYTHONUNBUFFERED=1
|
||||||
|
# Vor jedem Start: traegt die Python-Umgebung noch? Ein Release-Upgrade des
|
||||||
|
# Betriebssystems tauscht die Minor-Fassung von Python aus, und der venv haengt
|
||||||
|
# daran -- danach findet der Dienst kein einziges Paket mehr und scheitert mit
|
||||||
|
# "No module named gunicorn". Ist alles heil, kostet diese Zeile Millisekunden;
|
||||||
|
# sonst baut sie die Umgebung neu, einmal je Systemstart.
|
||||||
|
#
|
||||||
|
# Das "+" laesst den Schritt als root laufen. Noetig, weil der venv root
|
||||||
|
# gehoert, der Dienst aber als unprivilegierter Benutzer laeuft.
|
||||||
|
ExecStartPre=+/usr/local/lib/tesm/tesm-venv /srv/tesm tesm
|
||||||
ExecStart=/srv/tesm/venv/bin/gunicorn \
|
ExecStart=/srv/tesm/venv/bin/gunicorn \
|
||||||
--workers 1 --worker-class gthread --threads 16 --timeout 0 \
|
--workers 1 --worker-class gthread --threads 16 --timeout 0 \
|
||||||
--bind 127.0.0.1:5000 \
|
--bind 127.0.0.1:5000 \
|
||||||
--access-logfile - --error-logfile - \
|
--access-logfile - --error-logfile - \
|
||||||
tesm.wsgi:app
|
tesm.wsgi:app
|
||||||
|
# Ein Neuaufbau der Python-Umgebung laedt Pakete und dauert Minuten --
|
||||||
|
# die Vorgabe von 90 Sekunden wuerde ihn mittendrin abschneiden.
|
||||||
|
TimeoutStartSec=900
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
|
|
||||||
|
|||||||
@@ -150,6 +150,22 @@ def main(argv: list[str] | None = None) -> int:
|
|||||||
globals().update(AMBER="", DIM="", BOLD="", RESET="")
|
globals().update(AMBER="", DIM="", BOLD="", RESET="")
|
||||||
|
|
||||||
root = Path(args.root)
|
root = Path(args.root)
|
||||||
|
|
||||||
|
# Gibt es diese Installation ueberhaupt noch? Wenn nicht, ist hier nichts
|
||||||
|
# zu melden -- auch kein Fehler.
|
||||||
|
#
|
||||||
|
# Der Hook unter /etc/update-motd.d bleibt liegen, wenn jemand eine
|
||||||
|
# Installation von Hand entfernt; einen Rueckbau bringt TESM nicht mit.
|
||||||
|
# Vorher druckte jeder dieser Hooks trotzdem seinen vollen Banner und
|
||||||
|
# darunter "Datenbank nicht lesbar". Auf dem Testserver standen so
|
||||||
|
# vierzehn Bloecke fuer vierzehn laengst geloeschte Probeinstallationen
|
||||||
|
# vor jeder Anmeldung -- und der echte Banner ging darin unter.
|
||||||
|
#
|
||||||
|
# Eine entfernte Installation ist keine kaputte. Eine Datenbank, die da
|
||||||
|
# ist und sich nicht lesen laesst, dagegen schon: die meldet sich weiter.
|
||||||
|
if not (root / "data" / "app.db").is_file():
|
||||||
|
return 0
|
||||||
|
|
||||||
# Kein isatty()-Test: dieses Skript schreibt beim Login nicht auf ein
|
# Kein isatty()-Test: dieses Skript schreibt beim Login nicht auf ein
|
||||||
# Terminal, sondern ueber run-parts in eine Datei. Der Betrachter ist
|
# Terminal, sondern ueber run-parts in eine Datei. Der Betrachter ist
|
||||||
# trotzdem ein echtes Terminal -- deshalb immer Farbe.
|
# trotzdem ein echtes Terminal -- deshalb immer Farbe.
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Die Python-Umgebung einer Installation -- aufbauen, pruefen, reparieren.
|
||||||
|
#
|
||||||
|
# tesm-venv /srv/tesm tesm # reparieren, falls noetig (Dienststart)
|
||||||
|
# tesm-venv /srv/tesm tesm --immer # neu bestuecken (Installation)
|
||||||
|
# tesm-venv /srv/tesm tesm --pruefen # nur melden: 0 heil, 1 kaputt
|
||||||
|
#
|
||||||
|
# **Warum es das gibt.** Ein venv haengt an der Minor-Fassung von Python: die
|
||||||
|
# Pakete liegen unter lib/python3.X. Ein Release-Upgrade des Betriebssystems
|
||||||
|
# tauscht diese Fassung aus (Ubuntu 24.04 bringt 3.12, 26.04 bringt 3.14) --
|
||||||
|
# und danach zeigt venv/bin/python3 weiter auf /usr/bin/python3, also auf den
|
||||||
|
# **neuen** Interpreter. Der venv ist ausfuehrbar und trotzdem tot: er findet
|
||||||
|
# kein einziges Paket mehr, nicht einmal pip. Jeder Dienst scheitert mit
|
||||||
|
# "ModuleNotFoundError: No module named 'gunicorn'", nginx meldet 502.
|
||||||
|
#
|
||||||
|
# Real passiert, auf beiden Testservern, am 16.09.2026.
|
||||||
|
#
|
||||||
|
# Dieses Programm laeuft deshalb als ExecStartPre vor jedem Dienststart. Ist
|
||||||
|
# alles heil, endet es nach Millisekunden. Sonst baut es die Umgebung neu --
|
||||||
|
# aus denselben Paketen, die auch die Installation einspielt, denn es ist
|
||||||
|
# dasselbe Programm: install.sh ruft es mit --immer auf. Zwei getrennte Listen
|
||||||
|
# waeren zwei Listen, die auseinanderlaufen.
|
||||||
|
#
|
||||||
|
# **Was es nicht tut:** die alten Pakete unter lib/python3.X uebernehmen.
|
||||||
|
# cryptography, argon2-cffi und bcrypt sind kompiliert und fuer die alte
|
||||||
|
# Fassung gebaut; ein halb uebernommener venv waere schlimmer als ein neuer.
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# **Der venv muss fuer den Dienstbenutzer lesbar sein.**
|
||||||
|
#
|
||||||
|
# Dieses Programm laeuft als ExecStartPre der Dienst-Unit -- und erbt damit
|
||||||
|
# deren UMask=0027. Alles, was es anlegt, gehoerte root und waere 0750: der
|
||||||
|
# Dienst selbst laeuft unprivilegiert und koennte sein eigenes site-packages
|
||||||
|
# nicht einmal betreten. Gemessen: nach der Reparatur ging "import gunicorn"
|
||||||
|
# als root, als Benutzer tesm nicht -- die Reparatur baute etwas, das genau
|
||||||
|
# der Prozess nicht benutzen konnte, fuer den sie gedacht war.
|
||||||
|
#
|
||||||
|
# Im venv liegt nichts Geheimes; Schluessel und Datenbank stehen woanders.
|
||||||
|
umask 022
|
||||||
|
|
||||||
|
ROOT="${1:-}"
|
||||||
|
APP="${2:-}"
|
||||||
|
MODUS="reparieren"
|
||||||
|
[[ "${3:-}" == "--immer" ]] && MODUS="immer"
|
||||||
|
[[ "${3:-}" == "--pruefen" ]] && MODUS="pruefen"
|
||||||
|
|
||||||
|
[[ -n "$ROOT" && -n "$APP" ]] || {
|
||||||
|
echo "Aufruf: tesm-venv <verzeichnis> <tesm|tesm-license> [--immer|--pruefen]" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || {
|
||||||
|
echo "tesm-venv: unbekannte Anwendung '$APP'" >&2
|
||||||
|
exit 2
|
||||||
|
}
|
||||||
|
[[ -d "$ROOT" ]] || { echo "tesm-venv: $ROOT gibt es nicht" >&2; exit 2; }
|
||||||
|
|
||||||
|
NAME="$(basename "$ROOT")"
|
||||||
|
VENV="$ROOT/venv"
|
||||||
|
PY="$VENV/bin/python3"
|
||||||
|
MODUL="tesm"; [[ "$APP" == "tesm-license" ]] && MODUL="tesm_license"
|
||||||
|
LOG_DIR="${TESM_LOG_DIR:-/var/log/$NAME}"
|
||||||
|
#: Ein Versuch je Systemstart. /run ist ein tmpfs, der Merker verschwindet
|
||||||
|
#: also beim naechsten Start von selbst. Ohne ihn wuerde "Restart=always" nach
|
||||||
|
#: einem misslungenen Neuaufbau (kein Netz, kein PyPI) alle fuenf Sekunden
|
||||||
|
#: einen neuen pip-Lauf starten -- gegen einen Server, der ohnehin gerade
|
||||||
|
#: nicht erreichbar ist.
|
||||||
|
MERKER="/run/tesm-venv-$NAME.versucht"
|
||||||
|
|
||||||
|
melden() {
|
||||||
|
local stufe="$1" text="$2"
|
||||||
|
# Ins Journal: ExecStartPre-Ausgabe landet dort ohnehin.
|
||||||
|
printf 'tesm-venv: %s\n' "$text" >&2
|
||||||
|
# Und ins laufende Protokoll, damit es in der Oberflaeche sichtbar ist.
|
||||||
|
# Fehlschlaege hier duerfen den Start nicht kosten -- ein Protokoll ist
|
||||||
|
# kein Grund, einen Dienst nicht zu starten.
|
||||||
|
if [[ -d "$LOG_DIR" ]]; then
|
||||||
|
printf '%s %-7s %-10s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$stufe" "venv" "$text" \
|
||||||
|
>> "$LOG_DIR/live.log" 2>/dev/null || true
|
||||||
|
chown "$APP:$APP" "$LOG_DIR/live.log" 2>/dev/null || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Traegt diese Umgebung? Gefragt wird nicht nach der Versionsnummer, sondern
|
||||||
|
#: nach dem, was gebraucht wird -- so faellt auch ein venv auf, dem jemand ein
|
||||||
|
#: Paket herausgeloescht hat.
|
||||||
|
traegt() {
|
||||||
|
[[ -x "$PY" ]] || return 1
|
||||||
|
"$PY" -c "import pip" >/dev/null 2>&1 || return 1
|
||||||
|
# Beim allerersten Lauf ist die Anwendung noch nicht eingespielt -- dann
|
||||||
|
# zaehlt allein, dass pip benutzbar ist.
|
||||||
|
[[ "$MODUS" == "immer" ]] && return 0
|
||||||
|
"$PY" -c "import $MODUL" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
bestuecken() {
|
||||||
|
"$PY" -m pip install --quiet --upgrade pip setuptools wheel
|
||||||
|
local pakete=("$ROOT/packages/tesm-licensing" "$ROOT/packages/tesm-core")
|
||||||
|
if [[ "$APP" == "tesm-license" ]]; then
|
||||||
|
[[ -d "$ROOT/packages/tesm-licensing-server" ]] || {
|
||||||
|
echo "tesm-venv: packages/tesm-licensing-server fehlt" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
pakete+=("$ROOT/packages/tesm-licensing-server")
|
||||||
|
fi
|
||||||
|
"$PY" -m pip install --quiet "${pakete[@]}" "$ROOT/apps/$APP" gunicorn
|
||||||
|
# Die WebSocket-Bruecke braucht nur TESM (Terminal und Bildschirm).
|
||||||
|
[[ "$APP" == "tesm" ]] && "$PY" -m pip install --quiet flask-sock
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
if [[ "$MODUS" == "pruefen" ]]; then
|
||||||
|
traegt && exit 0 || exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if traegt && [[ "$MODUS" != "immer" ]]; then
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$MODUS" != "immer" && -e "$MERKER" ]]; then
|
||||||
|
melden "WARNUNG" \
|
||||||
|
"Die Python-Umgebung von $NAME ist unbrauchbar, und der Neuaufbau ist seit dem letzten Systemstart schon einmal gescheitert. Erneuter Versuch: 'bash $ROOT/deploy/install.sh --app $APP --yes'."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
touch "$MERKER" 2>/dev/null || true
|
||||||
|
|
||||||
|
if ! [[ -x "$PY" ]] || ! "$PY" -c "import pip" >/dev/null 2>&1; then
|
||||||
|
# Ohne --clear: ein erneutes "venv" ergaenzt das fehlende lib-Verzeichnis
|
||||||
|
# und holt pip zurueck. Gemessen, nicht angenommen.
|
||||||
|
python3 -m venv "$VENV"
|
||||||
|
[[ "$MODUS" == "immer" ]] ||
|
||||||
|
melden "HINWEIS" \
|
||||||
|
"Python-Umgebung von $NAME neu aufgebaut ($(python3 -V 2>&1)) -- das System hat seine Python-Fassung gewechselt."
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! bestuecken; then
|
||||||
|
melden "FEHLER" \
|
||||||
|
"Die Pakete fuer $NAME liessen sich nicht einspielen. Hat dieser Server Zugriff auf das Paketverzeichnis?"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Und noch einmal ausdruecklich, fuer den Fall, dass ein frueherer Lauf mit
|
||||||
|
# strengerer Maske gebaut hat: was hier liegt, muss lesbar sein. Idempotent
|
||||||
|
# und in einer Sekunde durch.
|
||||||
|
chmod -R a+rX "$VENV" 2>/dev/null || true
|
||||||
|
|
||||||
|
[[ "$MODUS" == "immer" ]] || melden "INFO" "Python-Umgebung von $NAME ist wieder vollstaendig."
|
||||||
|
rm -f "$MERKER" 2>/dev/null || true
|
||||||
|
exit 0
|
||||||
@@ -0,0 +1,303 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# =============================================================================
|
||||||
|
# Rueckbau einer Installation von TESM oder TESM-Lizenzserver.
|
||||||
|
#
|
||||||
|
# Aufruf:
|
||||||
|
# sudo bash deploy/uninstall.sh --app tesm
|
||||||
|
# sudo bash deploy/uninstall.sh --app tesm-license --instance test
|
||||||
|
# sudo bash deploy/uninstall.sh --app tesm --dry-run
|
||||||
|
#
|
||||||
|
# Das genaue Gegenstueck zu install.sh -- und es fehlte. Wer eine Installation
|
||||||
|
# von Hand loeschte ("rm -rf /srv/tesm-probe"), liess zurueck: die systemd-Unit
|
||||||
|
# (die systemd danach in einer Dauerschleife neu zu starten versuchte), den
|
||||||
|
# Begruessungshaken (der bei jeder Anmeldung seinen Banner druckte), den
|
||||||
|
# Shim unter /usr/local/bin, die Umgebungsdatei, die nginx-Site und den
|
||||||
|
# Dienstbenutzer. Auf dem Testserver standen so neun tote Dienste und vierzehn
|
||||||
|
# Banner. Genau das raeumt dieses Skript ab.
|
||||||
|
#
|
||||||
|
# Zwei Regeln, die den Unterschied machen:
|
||||||
|
#
|
||||||
|
# * **Daten bleiben.** Datenbank, Schluessel und Lizenz werden vorher in ein
|
||||||
|
# Archiv gelegt, nicht geloescht. Wer sie wirklich los sein will, sagt es
|
||||||
|
# mit --purge. Ein Rueckbau, der beilaeufig die Datenbank mitnimmt, ist
|
||||||
|
# keiner, sondern ein Unfall.
|
||||||
|
# * **Geteiltes bleibt, solange es jemand braucht.** sudoers-Datei und
|
||||||
|
# Dienstbenutzer gehoeren der *Anwendung* (nicht der Instanz), der Helfer
|
||||||
|
# unter /usr/local/lib/tesm allen Installationen beider Anwendungen. Sie
|
||||||
|
# verschwinden erst, wenn die letzte geht -- sonst legt der Rueckbau einer
|
||||||
|
# Testinstanz die Produktion daneben lahm.
|
||||||
|
# =============================================================================
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
APP=""
|
||||||
|
INSTANCE=""
|
||||||
|
PURGE=0
|
||||||
|
ASSUME_YES=0
|
||||||
|
DRY_RUN=0
|
||||||
|
|
||||||
|
#: Ueberschreibbar, damit sich die Entscheidungen dieses Skripts in einem Test
|
||||||
|
#: wirklich ausfuehren lassen -- und nicht nur als Textmuster geprueft werden.
|
||||||
|
ETC_TESM="${TESM_ETC_DIR:-/etc/tesm}"
|
||||||
|
SRV_DIR="${TESM_SRV_DIR:-/srv}"
|
||||||
|
UNIT_DIR="${TESM_SYSTEMD_DIR:-/etc/systemd/system}"
|
||||||
|
MOTD_DIR="${TESM_MOTD_DIR:-/etc/update-motd.d}"
|
||||||
|
BIN_DIR="${TESM_BIN_DIR:-/usr/local/bin}"
|
||||||
|
LIB_DIR="${TESM_LIB_DIR:-/usr/local/lib/tesm}"
|
||||||
|
SUDOERS_DIR="${TESM_SUDOERS_DIR:-/etc/sudoers.d}"
|
||||||
|
LOGROTATE_DIR="${TESM_LOGROTATE_DIR:-/etc/logrotate.d}"
|
||||||
|
NGINX_DIR="${TESM_NGINX_DIR:-/etc/nginx}"
|
||||||
|
LOG_BASE="${TESM_LOG_BASE:-/var/log}"
|
||||||
|
|
||||||
|
COLOR_OK=$'\033[32m'; COLOR_WARN=$'\033[33m'; COLOR_ERR=$'\033[31m'; COLOR_OFF=$'\033[0m'
|
||||||
|
step() { printf '\n%s==>%s %s\n' "$COLOR_OK" "$COLOR_OFF" "$*"; }
|
||||||
|
warn() { printf '%s !!%s %s\n' "$COLOR_WARN" "$COLOR_OFF" "$*" ; }
|
||||||
|
fail() { printf '%s !!%s %s\n' "$COLOR_ERR" "$COLOR_OFF" "$*" >&2; exit 1; }
|
||||||
|
info() { printf ' %s\n' "$*"; }
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'TEXT'
|
||||||
|
Rueckbau einer Installation.
|
||||||
|
|
||||||
|
--app <tesm|tesm-license> welche Anwendung (Pflicht)
|
||||||
|
--instance <name> benannte Instanz (sonst die Hauptinstallation)
|
||||||
|
--purge Daten nicht sichern, sondern loeschen
|
||||||
|
--dry-run nur zeigen, was geschehen wuerde
|
||||||
|
--yes, -y nicht nachfragen
|
||||||
|
TEXT
|
||||||
|
}
|
||||||
|
|
||||||
|
while [[ $# -gt 0 ]]; do
|
||||||
|
case "$1" in
|
||||||
|
--app) APP="${2:?}"; shift 2 ;;
|
||||||
|
--instance) INSTANCE="${2:?}"; shift 2 ;;
|
||||||
|
--purge) PURGE=1; shift ;;
|
||||||
|
--dry-run) DRY_RUN=1; shift ;;
|
||||||
|
--yes|-y) ASSUME_YES=1; shift ;;
|
||||||
|
-h|--help) usage; exit 0 ;;
|
||||||
|
*) fail "unbekannte Option: $1" ;;
|
||||||
|
esac
|
||||||
|
done
|
||||||
|
|
||||||
|
[[ -n "$APP" ]] || { usage; fail "--app fehlt"; }
|
||||||
|
[[ "$APP" == "tesm" || "$APP" == "tesm-license" ]] || fail "--app muss tesm oder tesm-license sein"
|
||||||
|
[[ -z "$INSTANCE" || "$INSTANCE" =~ ^[a-z0-9-]{1,16}$ ]] || fail "ungueltiger Instanzname"
|
||||||
|
# Root braucht es wegen /srv, /etc und systemd. Wer die Pfade umbiegt (Test),
|
||||||
|
# fasst nichts davon an -- dann ist die Huerde keine Sicherheit, sondern nur im
|
||||||
|
# Weg, und ein Rueckbau, der sich nicht pruefen laesst, ist ein Risiko fuer sich.
|
||||||
|
if [[ $DRY_RUN -eq 0 && "$SRV_DIR" == "/srv" && "$(id -u)" -ne 0 ]]; then
|
||||||
|
fail "bitte mit sudo ausfuehren"
|
||||||
|
fi
|
||||||
|
|
||||||
|
NAME="$APP"
|
||||||
|
[[ -n "$INSTANCE" ]] && NAME="$APP-$INSTANCE"
|
||||||
|
ROOT="$SRV_DIR/$NAME"
|
||||||
|
|
||||||
|
# Das Datenverzeichnis kann verschoben sein -- dann steht es in der
|
||||||
|
# Umgebungsdatei, und nur dort. Ein Rueckbau, der stattdessen "$ROOT/data"
|
||||||
|
# annimmt, sichert ein leeres Verzeichnis und laesst die echte Datenbank liegen.
|
||||||
|
ENV_FILE="$ETC_TESM/$NAME.env"
|
||||||
|
DATEN_DIR="$(sed -n 's/^TESM_DATA_DIR=//p' "$ENV_FILE" 2>/dev/null | tail -1 || true)"
|
||||||
|
DATEN_DIR="${DATEN_DIR:-$ROOT/data}"
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Wer teilt sich was?
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
|
||||||
|
#: Gibt es neben dieser noch eine weitere Installation **derselben** Anwendung?
|
||||||
|
#: Daran haengen sudoers-Datei und Dienstbenutzer -- beide tragen den Namen der
|
||||||
|
#: Anwendung, nicht den der Instanz.
|
||||||
|
weitere_dieser_anwendung() {
|
||||||
|
local eintrag name
|
||||||
|
for eintrag in "$ETC_TESM/$APP.env" "$ETC_TESM/$APP-"*.env; do
|
||||||
|
[[ -f "$eintrag" ]] || continue
|
||||||
|
name="$(basename "$eintrag" .env)"
|
||||||
|
[[ "$name" == "$NAME" ]] && continue
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
#: Gibt es ueberhaupt noch irgendeine Installation? Daran haengen der Helfer
|
||||||
|
#: und das Banner-Programm unter /usr/local/lib/tesm -- die gehoeren allen.
|
||||||
|
weitere_ueberhaupt() {
|
||||||
|
local eintrag name
|
||||||
|
for eintrag in "$ETC_TESM"/*.env; do
|
||||||
|
[[ -f "$eintrag" ]] || continue
|
||||||
|
name="$(basename "$eintrag" .env)"
|
||||||
|
[[ "$name" == "$NAME" ]] && continue
|
||||||
|
return 0
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Der Plan -- erst zeigen, dann tun
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
UNITS=("$NAME.service" "$NAME-monitor.service" "$NAME-cleanup.service" "$NAME-cleanup.timer")
|
||||||
|
DATEIEN=(
|
||||||
|
"$NGINX_DIR/sites-enabled/$NAME"
|
||||||
|
"$NGINX_DIR/sites-available/$NAME"
|
||||||
|
"$MOTD_DIR/50-$NAME"
|
||||||
|
"$BIN_DIR/$NAME-admin"
|
||||||
|
"$LOGROTATE_DIR/$NAME"
|
||||||
|
"$ENV_FILE"
|
||||||
|
)
|
||||||
|
VERZEICHNISSE=("$ETC_TESM/certs/$NAME" "$LOG_BASE/$NAME")
|
||||||
|
|
||||||
|
gefunden=0
|
||||||
|
step "Rueckbau von $NAME"
|
||||||
|
info "Anwendung : $APP${INSTANCE:+ (Instanz $INSTANCE)}"
|
||||||
|
info "Verzeichnis : $ROOT"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Dienste:"
|
||||||
|
for unit in "${UNITS[@]}"; do
|
||||||
|
if [[ -f "$UNIT_DIR/$unit" ]]; then info "- $unit"; gefunden=1; fi
|
||||||
|
done
|
||||||
|
[[ $gefunden -eq 1 ]] || info "(keine)"
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Dateien und Verzeichnisse:"
|
||||||
|
for pfad in "${DATEIEN[@]}" "${VERZEICHNISSE[@]}"; do
|
||||||
|
if [[ -e "$pfad" ]]; then info "- $pfad"; gefunden=1; fi
|
||||||
|
done
|
||||||
|
if [[ -d "$ROOT" ]]; then info "- $ROOT"; gefunden=1; fi
|
||||||
|
|
||||||
|
if [[ $gefunden -eq 0 ]]; then
|
||||||
|
echo
|
||||||
|
info "Von '$NAME' ist nichts mehr da -- nichts zu tun."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Daten (Datenbank, Schluessel, Lizenz):"
|
||||||
|
if [[ -d "$DATEN_DIR" ]]; then
|
||||||
|
if [[ $PURGE -eq 1 ]]; then
|
||||||
|
warn "$DATEN_DIR wird GELOESCHT (--purge)"
|
||||||
|
else
|
||||||
|
info "$DATEN_DIR wird vorher gesichert"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info "(kein Datenverzeichnis gefunden)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo " Bleibt stehen:"
|
||||||
|
if weitere_dieser_anwendung; then
|
||||||
|
info "sudoers und Dienstbenutzer '$APP' -- eine weitere Installation braucht sie"
|
||||||
|
else
|
||||||
|
info "- (sudoers und Dienstbenutzer '$APP' werden entfernt)"
|
||||||
|
fi
|
||||||
|
if weitere_ueberhaupt; then
|
||||||
|
info "Helfer unter $LIB_DIR -- andere Installationen brauchen ihn"
|
||||||
|
else
|
||||||
|
info "- (Helfer unter $LIB_DIR wird entfernt)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $DRY_RUN -eq 1 ]]; then
|
||||||
|
echo
|
||||||
|
info "Probelauf -- es wurde nichts geaendert."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $ASSUME_YES -eq 0 ]]; then
|
||||||
|
echo
|
||||||
|
read -r -p "Fortfahren? [j/N] " antwort
|
||||||
|
[[ "$antwort" =~ ^[jJyY]$ ]] || fail "abgebrochen"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
# Ausfuehren
|
||||||
|
# --------------------------------------------------------------------------- #
|
||||||
|
step "Daten"
|
||||||
|
if [[ -d "$DATEN_DIR" && $PURGE -eq 0 ]]; then
|
||||||
|
ARCHIV="$SRV_DIR/$NAME-daten-$(date +%Y%m%d-%H%M%S).tar.gz"
|
||||||
|
tar -czf "$ARCHIV" -C "$(dirname "$DATEN_DIR")" "$(basename "$DATEN_DIR")"
|
||||||
|
chmod 600 "$ARCHIV"
|
||||||
|
info "gesichert: $ARCHIV"
|
||||||
|
elif [[ -d "$DATEN_DIR" ]]; then
|
||||||
|
info "wird mit dem Verzeichnis geloescht (--purge)"
|
||||||
|
else
|
||||||
|
info "nichts zu sichern"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Dienste"
|
||||||
|
for unit in "${UNITS[@]}"; do
|
||||||
|
[[ -f "$UNIT_DIR/$unit" ]] || continue
|
||||||
|
systemctl disable --now "$unit" >/dev/null 2>&1 || true
|
||||||
|
rm -f "$UNIT_DIR/$unit"
|
||||||
|
# Ohne das bleibt eine entfernte Unit als "not-found" im Fehlerzustand
|
||||||
|
# stehen und faerbt "systemctl --failed" rot.
|
||||||
|
systemctl reset-failed "$unit" >/dev/null 2>&1 || true
|
||||||
|
info "entfernt: $unit"
|
||||||
|
done
|
||||||
|
systemctl daemon-reload >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
step "Webserver"
|
||||||
|
if [[ -e "$NGINX_DIR/sites-enabled/$NAME" || -e "$NGINX_DIR/sites-available/$NAME" ]]; then
|
||||||
|
rm -f "$NGINX_DIR/sites-enabled/$NAME" "$NGINX_DIR/sites-available/$NAME"
|
||||||
|
# Drei Faelle, und sie bedeuten Verschiedenes: kein nginx da (dann gibt es
|
||||||
|
# nichts neu zu laden), nginx da und zufrieden, nginx da und unzufrieden.
|
||||||
|
# Sie in einen Zweig zu werfen hiesse, auf einer Maschine ohne nginx eine
|
||||||
|
# Warnung ueber eine kaputte Konfiguration auszugeben, die es nicht gibt.
|
||||||
|
if ! command -v nginx >/dev/null 2>&1; then
|
||||||
|
info "Site entfernt (nginx ist hier nicht installiert)"
|
||||||
|
elif nginx -t >/dev/null 2>&1; then
|
||||||
|
systemctl reload nginx >/dev/null 2>&1 || true
|
||||||
|
info "Site entfernt, nginx neu geladen"
|
||||||
|
else
|
||||||
|
warn "Site entfernt -- nginx beanstandet die Konfiguration, bitte 'nginx -t' ansehen"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
info "keine Site vorhanden"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Dateien"
|
||||||
|
for pfad in "${DATEIEN[@]}"; do
|
||||||
|
[[ -e "$pfad" ]] && { rm -f "$pfad"; info "entfernt: $pfad"; }
|
||||||
|
done
|
||||||
|
for pfad in "${VERZEICHNISSE[@]}"; do
|
||||||
|
[[ -d "$pfad" ]] && { rm -rf "$pfad"; info "entfernt: $pfad"; }
|
||||||
|
done
|
||||||
|
if [[ -d "$ROOT" ]]; then
|
||||||
|
rm -rf "$ROOT"
|
||||||
|
info "entfernt: $ROOT"
|
||||||
|
fi
|
||||||
|
if [[ $PURGE -eq 1 ]]; then
|
||||||
|
# Die Rueckwege aelterer Aktualisierungen. Sie enthalten dieselben Daten wie
|
||||||
|
# die Installation -- deshalb nur hier, nie im Normalfall.
|
||||||
|
for rest in "$SRV_DIR/$NAME-backup-"* "$SRV_DIR/$NAME-failed-"*; do
|
||||||
|
[[ -d "$rest" ]] && { rm -rf "$rest"; info "entfernt: $rest"; }
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Geteiltes"
|
||||||
|
if weitere_dieser_anwendung; then
|
||||||
|
info "sudoers und Dienstbenutzer '$APP' bleiben -- eine weitere Installation braucht sie"
|
||||||
|
else
|
||||||
|
rm -f "$SUDOERS_DIR/$APP"
|
||||||
|
info "entfernt: $SUDOERS_DIR/$APP"
|
||||||
|
if getent passwd "$APP" >/dev/null 2>&1; then
|
||||||
|
# Ein laufender Prozess haelt den Benutzer fest. Das ist kein Grund,
|
||||||
|
# den Rueckbau scheitern zu lassen -- der Dienst ist oben schon gestoppt,
|
||||||
|
# und was sonst noch unter diesem Namen laeuft, gehoert angesehen.
|
||||||
|
if userdel "$APP" >/dev/null 2>&1; then
|
||||||
|
info "entfernt: Dienstbenutzer $APP"
|
||||||
|
else
|
||||||
|
warn "Dienstbenutzer $APP liess sich nicht entfernen -- laeuft noch etwas unter ihm?"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
if weitere_ueberhaupt; then
|
||||||
|
info "Helfer unter $LIB_DIR bleibt -- andere Installationen brauchen ihn"
|
||||||
|
else
|
||||||
|
rm -rf "$LIB_DIR"
|
||||||
|
rmdir "$ETC_TESM/certs" "$ETC_TESM" >/dev/null 2>&1 || true
|
||||||
|
info "entfernt: $LIB_DIR"
|
||||||
|
fi
|
||||||
|
|
||||||
|
step "Fertig"
|
||||||
|
info "'$NAME' ist zurueckgebaut."
|
||||||
|
if [[ $PURGE -eq 0 && -n "${ARCHIV:-}" ]]; then
|
||||||
|
info "Die Daten liegen unter $ARCHIV -- dort und sonst nirgends."
|
||||||
|
fi
|
||||||
@@ -33,6 +33,8 @@ EXTRA=("$@")
|
|||||||
echo "Verwendung: update.sh <tesm|tesm-license> [tag] [install.sh-Argumente ...]" >&2
|
echo "Verwendung: update.sh <tesm|tesm-license> [tag] [install.sh-Argumente ...]" >&2
|
||||||
echo "Beispiel : update.sh tesm latest --instance test" >&2
|
echo "Beispiel : update.sh tesm latest --instance test" >&2
|
||||||
echo "Ohne Rueckfrage: --yes anhaengen (ohne Terminal zwingend)." >&2
|
echo "Ohne Rueckfrage: --yes anhaengen (ohne Terminal zwingend)." >&2
|
||||||
|
echo "Laeuft die Fassung schon, endet der Aufruf ohne Arbeit; --force setzt" >&2
|
||||||
|
echo "sie trotzdem neu auf." >&2
|
||||||
exit 2
|
exit 2
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +175,32 @@ release_holen() {
|
|||||||
|
|
||||||
release_holen
|
release_holen
|
||||||
|
|
||||||
|
# Schon installiert? Dann gar nicht erst fragen.
|
||||||
|
#
|
||||||
|
# Dieselbe Pruefung steht in install.sh und gilt dort endgueltig (sie
|
||||||
|
# vergleicht auch den Kern). Hier steht sie noch einmal, weil ein Update, das
|
||||||
|
# ohne Wirkung bleibt, gar nicht erst nach einer Bestaetigung fragen soll --
|
||||||
|
# und weil "es lief durch" sonst wie eine Aktualisierung aussieht.
|
||||||
|
ERZWINGEN=0
|
||||||
|
for ((i = 0; i < ${#EXTRA[@]}; i++)); do
|
||||||
|
[[ "${EXTRA[i]}" == "--force" ]] && ERZWINGEN=1
|
||||||
|
done
|
||||||
|
INSTALLATION="$APP"
|
||||||
|
for ((i = 0; i < ${#EXTRA[@]}; i++)); do
|
||||||
|
[[ "${EXTRA[i]}" == "--instance" ]] && INSTALLATION="$APP-${EXTRA[i + 1]:-}"
|
||||||
|
done
|
||||||
|
MODUL="tesm"
|
||||||
|
[[ "$APP" == "tesm-license" ]] && MODUL="tesm_license"
|
||||||
|
if [[ $ERZWINGEN -eq 0 && -x "/srv/$INSTALLATION/venv/bin/python" ]]; then
|
||||||
|
LAEUFT="$("/srv/$INSTALLATION/venv/bin/python" -c \
|
||||||
|
"import $MODUL; print($MODUL.__version__)" 2>/dev/null || true)"
|
||||||
|
if [[ -n "$LAEUFT" && "v$LAEUFT" == "$TAG" ]]; then
|
||||||
|
echo "==> $APP $LAEUFT laeuft hier bereits -- nichts zu tun."
|
||||||
|
echo " Trotzdem neu einrichten: --force"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
# --yes darf auch hier stehen, nicht nur bei install.sh und upgrade.sh. Es
|
# --yes darf auch hier stehen, nicht nur bei install.sh und upgrade.sh. Es
|
||||||
# bleibt in EXTRA: install.sh bekommt es ohnehin, doppelt schadet nicht.
|
# bleibt in EXTRA: install.sh bekommt es ohnehin, doppelt schadet nicht.
|
||||||
OHNE_RUECKFRAGE=0
|
OHNE_RUECKFRAGE=0
|
||||||
|
|||||||
@@ -120,6 +120,14 @@ sudo bash /srv/tesm/deploy/update.sh tesm
|
|||||||
sudo bash /srv/tesm-license/deploy/update.sh tesm-license
|
sudo bash /srv/tesm-license/deploy/update.sh tesm-license
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Läuft die Fassung schon, passiert nichts.** `update.sh` vergleicht den
|
||||||
|
aufgelösten Tag mit dem, was installiert ist, und endet mit `0`, bevor es eine
|
||||||
|
Rückfrage stellt; `install.sh` prüft es noch einmal genauer (Anwendung **und**
|
||||||
|
Kern) und bricht vor dem ersten Eingriff ab. Ein zweiter Lauf auf dieselbe
|
||||||
|
Fassung baut sonst die Python-Umgebung neu, kopiert dieselben Dateien und
|
||||||
|
startet den Dienst neu -- ein paar Sekunden Ausfall ohne jede Wirkung. Wer das
|
||||||
|
trotzdem will (eine beschädigte Installation etwa), hängt `--force` an.
|
||||||
|
|
||||||
Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg,
|
Alles nach dem Tag geht unverändert an `install.sh` weiter -- das ist der Weg,
|
||||||
eine **benannte Instanz** zu aktualisieren:
|
eine **benannte Instanz** zu aktualisieren:
|
||||||
|
|
||||||
@@ -375,6 +383,87 @@ sudo bash /srv/tesm/deploy/update.sh tesm
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## 4bb. Release-Upgrade des Betriebssystems
|
||||||
|
|
||||||
|
Ein `do-release-upgrade` tauscht die Minor-Fassung von Python aus (24.04 bringt
|
||||||
|
3.12, 26.04 bringt 3.14). Der venv einer Installation hängt daran: seine Pakete
|
||||||
|
liegen unter `lib/python3.12`, während `venv/bin/python3` auf `/usr/bin/python3`
|
||||||
|
zeigt — also nach dem Upgrade auf den **neuen** Interpreter. Er ist ausführbar
|
||||||
|
und trotzdem tot; jeder Dienst scheitert mit `No module named gunicorn`, nginx
|
||||||
|
meldet 502.
|
||||||
|
|
||||||
|
**Das repariert TESM seit 2.8.33 von selbst.** Vor jedem Dienststart läuft
|
||||||
|
|
||||||
|
```
|
||||||
|
ExecStartPre=+/usr/local/lib/tesm/tesm-venv /srv/tesm tesm
|
||||||
|
```
|
||||||
|
|
||||||
|
Ist die Umgebung heil, kostet das Millisekunden. Sonst wird sie neu gebaut und
|
||||||
|
mit denselben Paketen bestückt, die auch die Installation einspielt — es ist
|
||||||
|
dasselbe Programm, `install.sh` ruft es mit `--immer` auf. Nach dem Neustart,
|
||||||
|
der auf jedes Release-Upgrade folgt, kommt der Dienst also von allein zurück;
|
||||||
|
im Live-Protokoll steht dann eine Zeile als Hinweis.
|
||||||
|
|
||||||
|
Von Hand prüfen oder anstoßen:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/usr/local/lib/tesm/tesm-venv /srv/tesm tesm --pruefen # 0 heil, 1 kaputt
|
||||||
|
/usr/local/lib/tesm/tesm-venv /srv/tesm tesm # reparieren
|
||||||
|
```
|
||||||
|
|
||||||
|
**Grenzen.** Der Neuaufbau lädt Pakete und braucht Zugriff auf das
|
||||||
|
Paketverzeichnis. Ohne Netz kann er nicht gelingen: dann wird **ein** Versuch je
|
||||||
|
Systemstart unternommen (Merker unter `/run`), und es bleibt bei einer klaren
|
||||||
|
Zeile im Journal und im Live-Protokoll. Ohne diese Bremse startete
|
||||||
|
`Restart=always` alle fünf Sekunden einen neuen `pip`-Lauf.
|
||||||
|
|
||||||
|
Die alten Pakete unter `lib/python3.<alt>` werden nicht übernommen —
|
||||||
|
`cryptography`, `argon2-cffi` und `bcrypt` sind kompiliert und für die alte
|
||||||
|
Fassung gebaut.
|
||||||
|
|
||||||
|
Empfohlene Reihenfolge bleibt trotzdem: erst TESM aktualisieren, dann
|
||||||
|
`do-release-upgrade`, dann prüfen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4c. Installation entfernen
|
||||||
|
|
||||||
|
Das Gegenstück zu `install.sh`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo bash /srv/tesm/deploy/uninstall.sh --app tesm --dry-run
|
||||||
|
sudo bash /srv/tesm/deploy/uninstall.sh --app tesm
|
||||||
|
sudo bash /srv/tesm/deploy/uninstall.sh --app tesm-license --instance test --yes
|
||||||
|
```
|
||||||
|
|
||||||
|
Entfernt werden Dienste (`.service`, `-monitor`, `-cleanup` samt Timer), die
|
||||||
|
nginx-Site, der Begrüßungshaken, der Shim unter `/usr/local/bin`, die
|
||||||
|
Umgebungsdatei, das Zertifikatsverzeichnis, das Protokollverzeichnis und die
|
||||||
|
Installation selbst.
|
||||||
|
|
||||||
|
**Die Daten bleiben.** Datenbank, Schlüssel und Lizenz gehen vorher in ein
|
||||||
|
Archiv (`/srv/<name>-daten-<zeit>.tar.gz`, nur root). Sie sind das einzige an
|
||||||
|
einer Installation, was sich nicht wiederherstellen lässt. Wer sie wirklich
|
||||||
|
los sein will, nimmt `--purge` — das löscht zusätzlich die Rückwege älterer
|
||||||
|
Aktualisierungen (`/srv/<name>-backup-*`).
|
||||||
|
|
||||||
|
**Geteiltes bleibt, solange es jemand braucht.** Die sudoers-Datei und der
|
||||||
|
Dienstbenutzer tragen den Namen der *Anwendung*, nicht den der Instanz; der
|
||||||
|
Helfer unter `/usr/local/lib/tesm` gehört allen Installationen beider
|
||||||
|
Anwendungen. Sie verschwinden erst mit der letzten. Sonst legte der Rückbau
|
||||||
|
einer Testinstanz die Produktion daneben lahm.
|
||||||
|
|
||||||
|
`--dry-run` zeigt den Plan, ohne etwas zu ändern. Ein zweiter Lauf auf eine
|
||||||
|
bereits entfernte Installation sagt das und endet mit 0.
|
||||||
|
|
||||||
|
Warum es das gibt: wer eine Installation von Hand löscht (`rm -rf /srv/…`),
|
||||||
|
lässt Unit, Begrüßungshaken, Shim, sudoers-Datei und nginx-Site zurück.
|
||||||
|
systemd versucht die Unit danach in einer Dauerschleife zu starten, und der
|
||||||
|
Haken druckt bei jeder Anmeldung seinen Banner. Auf dem Testserver standen so
|
||||||
|
neun tote Dienste und vierzehn Banner.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 5. Dienste
|
## 5. Dienste
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.8.4"
|
version = "2.8.18"
|
||||||
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
description = "Gemeinsames Anwendungsgeruest (Auth, RBAC, Navigation, Design-System, Migrationen) fuer TESM und TESM-Lizenzserver"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
|||||||
@@ -266,6 +266,35 @@ def _bitte_bestaetigen(permission: str) -> Any:
|
|||||||
return redirect(url_for("auth.reauth"))
|
return redirect(url_for("auth.reauth"))
|
||||||
|
|
||||||
|
|
||||||
|
def _heikel(permissions: tuple[str, ...], mode: str) -> str:
|
||||||
|
"""Welches Recht verlangt hier eine frische Bestaetigung? Sonst leer.
|
||||||
|
|
||||||
|
Bei ``mode="all"`` ist die Antwort einfach: jedes genannte Recht wird
|
||||||
|
gebraucht, also zaehlt jedes heikle darunter.
|
||||||
|
|
||||||
|
Bei ``mode="any"`` nicht. Diese Seiten nennen mehrere **unabhaengige**
|
||||||
|
Wege hinein, und welcher genommen wird, entscheidet das Konto. Der
|
||||||
|
Protokollverlauf etwa nennt "logs_history.read" und "logs_history.export":
|
||||||
|
lesen ist harmlos, mitnehmen nicht. Gezaehlt wurde aber ueber alle
|
||||||
|
genannten Rechte -- und so verlangte das blosse **Ansehen** des Verlaufs
|
||||||
|
nach fuenfzehn Minuten eine Passworteingabe, mit dem Hinweis, man greife
|
||||||
|
auf einen Datenexport zu. Das stimmte nicht, und wer es dreimal liest,
|
||||||
|
glaubt die Meldung beim vierten Mal auch dann nicht, wenn sie zutrifft.
|
||||||
|
|
||||||
|
Gefragt wird deshalb: haette dieses Konto die Seite auch ohne das heikle
|
||||||
|
Recht geoeffnet? Dann gibt es nichts zu bestaetigen. Die Ausfuhr selbst
|
||||||
|
haengt an ihrer eigenen Route, und die nennt nur ein Recht.
|
||||||
|
"""
|
||||||
|
heikle = [p for p in permissions if _verlangt_wiederanmeldung(p)]
|
||||||
|
if not heikle:
|
||||||
|
return ""
|
||||||
|
if mode == "any":
|
||||||
|
harmlos = [p for p in permissions if p not in heikle]
|
||||||
|
if any(current_user.has(p) for p in harmlos):
|
||||||
|
return ""
|
||||||
|
return heikle[0]
|
||||||
|
|
||||||
|
|
||||||
def permission_required(*permissions: str, mode: str = "all") -> Callable[..., Any]:
|
def permission_required(*permissions: str, mode: str = "all") -> Callable[..., Any]:
|
||||||
"""Erzwingt Rechte serverseitig.
|
"""Erzwingt Rechte serverseitig.
|
||||||
|
|
||||||
@@ -289,9 +318,9 @@ def permission_required(*permissions: str, mode: str = "all") -> Callable[..., A
|
|||||||
# in jeder Route einzeln. Vorher hing es daran, dass jemand an den
|
# in jeder Route einzeln. Vorher hing es daran, dass jemand an den
|
||||||
# Dekorator dachte: dieselbe Lizenz herunterzuladen fragte nach,
|
# Dekorator dachte: dieselbe Lizenz herunterzuladen fragte nach,
|
||||||
# sie per Mail zu verschicken nicht.
|
# sie per Mail zu verschicken nicht.
|
||||||
heikel = [p for p in permissions if _verlangt_wiederanmeldung(p)]
|
heikel = _heikel(permissions, mode)
|
||||||
if heikel and not _wiederanmeldung_frisch():
|
if heikel and not _wiederanmeldung_frisch():
|
||||||
return _bitte_bestaetigen(heikel[0])
|
return _bitte_bestaetigen(heikel)
|
||||||
return view(*args, **kwargs)
|
return view(*args, **kwargs)
|
||||||
|
|
||||||
wrapper._required_permissions = permissions # type: ignore[attr-defined]
|
wrapper._required_permissions = permissions # type: ignore[attr-defined]
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ def _config_from_settings(extension: Any, conn: Any) -> webserver.WebConfig:
|
|||||||
],
|
],
|
||||||
)
|
)
|
||||||
return webserver.WebConfig(
|
return webserver.WebConfig(
|
||||||
|
# Die neue Schreibweise, sobald der Webserver sie kennt.
|
||||||
|
http2_directive=webserver.nginx_kann_http2(),
|
||||||
app_key=extension.config.site_key,
|
app_key=extension.config.site_key,
|
||||||
upstream=str(extension.registry.get("upstream", "127.0.0.1:5000")),
|
upstream=str(extension.registry.get("upstream", "127.0.0.1:5000")),
|
||||||
static_root=str(
|
static_root=str(
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ DIAGNOSTICS = Resource(
|
|||||||
|
|
||||||
LOGS_LIVE = Resource(
|
LOGS_LIVE = Resource(
|
||||||
key="logs_live",
|
key="logs_live",
|
||||||
label="Laufendes Protokoll",
|
label="Live-Protokoll",
|
||||||
actions=(A.READ,),
|
actions=(A.READ,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -1133,6 +1133,16 @@
|
|||||||
}
|
}
|
||||||
.log-view mark { background: var(--warning-soft); color: var(--warning); border-radius: 3px; }
|
.log-view mark { background: var(--warning-soft); color: var(--warning); border-radius: 3px; }
|
||||||
|
|
||||||
|
/* Stufen im laufenden Protokoll. Die Zuordnung der Farbrollen ist Absicht:
|
||||||
|
--warning ist bernsteinfarben und traegt den HINWEIS, --danger ist rot und
|
||||||
|
traegt die WARNUNG. Wer das nach den Namen "aufraeumt", dreht die Bedeutung
|
||||||
|
um: ein Geraet, das nicht antwortet, waere dann orange und eines, das neu
|
||||||
|
startet, rot. */
|
||||||
|
.log-view__zeile--info { color: var(--success); }
|
||||||
|
.log-view__zeile--hinweis { color: var(--warning); }
|
||||||
|
.log-view__zeile--warnung { color: var(--danger); }
|
||||||
|
.log-view__zeile--fehler { color: var(--danger); font-weight: 600; }
|
||||||
|
|
||||||
.kv { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-2) var(--sp-4); font-size: var(--fs-base); }
|
.kv { display: grid; grid-template-columns: auto 1fr; gap: var(--sp-2) var(--sp-4); font-size: var(--fs-base); }
|
||||||
.kv dt { color: var(--text-faint); white-space: nowrap; }
|
.kv dt { color: var(--text-faint); white-space: nowrap; }
|
||||||
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
.kv dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
|
||||||
@@ -1275,6 +1285,55 @@
|
|||||||
color: var(--text-faint);
|
color: var(--text-faint);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Mitlesen auf der Wartungsseite: die Ausgabe eines laufenden Auftrags,
|
||||||
|
direkt unter seinem Geraet. Dunkler Grund wie in jeder Konsole -- was
|
||||||
|
dort steht, kommt von einer fremden Maschine und nicht aus dieser
|
||||||
|
Oberflaeche. */
|
||||||
|
.job-output {
|
||||||
|
margin: 0;
|
||||||
|
max-height: 220px;
|
||||||
|
overflow: auto;
|
||||||
|
padding: var(--sp-3) var(--sp-4);
|
||||||
|
background: #101216;
|
||||||
|
color: #d7dbe0;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
line-height: 1.5;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------------------------------------------------------------------- #
|
||||||
|
Bildschirmsitzung (VNC)
|
||||||
|
|
||||||
|
Die Leinwand bekommt einen dunklen Grund -- unabhaengig vom Thema der
|
||||||
|
Oberflaeche: was darauf erscheint, ist der Bildschirm eines fremden
|
||||||
|
Geraets, und ein heller Rahmen darum laesst ihn wie einen Teil dieser
|
||||||
|
Seite aussehen. Er ist keiner.
|
||||||
|
---------------------------------------------------------------------- */
|
||||||
|
.screen {
|
||||||
|
border: 1px solid var(--border-subtle);
|
||||||
|
border-radius: var(--r-md);
|
||||||
|
background: #101216;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.screen__canvas {
|
||||||
|
width: 100%;
|
||||||
|
/* Genug Hoehe, dass ein Bildschirm erkennbar ist, ohne die Seite zu
|
||||||
|
sprengen; noVNC skaliert das Bild hinein. */
|
||||||
|
height: min(72vh, 760px);
|
||||||
|
}
|
||||||
|
/* Im Fenster ist weniger Platz -- und das Fenster soll nicht ueber den
|
||||||
|
Bildschirmrand hinauswachsen, nur weil das Geraet hochaufloesend ist. */
|
||||||
|
dialog.modal .screen__canvas { height: min(62vh, 620px); }
|
||||||
|
.screen__status {
|
||||||
|
font-size: var(--fs-sm);
|
||||||
|
color: var(--text-faint);
|
||||||
|
}
|
||||||
|
.screen__status--ok { color: var(--success); }
|
||||||
|
.screen__status--warnung { color: var(--warning); }
|
||||||
|
.screen__status--fehler { color: var(--danger); }
|
||||||
|
|
||||||
/* Fusszeile der Kachel: links der Bildschirm, rechts der Neustart.
|
/* Fusszeile der Kachel: links der Bildschirm, rechts der Neustart.
|
||||||
``margin-top: auto`` schiebt sie an den unteren Rand -- unabhaengig
|
``margin-top: auto`` schiebt sie an den unteren Rand -- unabhaengig
|
||||||
davon, wie viel darueber steht. Die beiden Eingriffe stehen damit in
|
davon, wie viel darueber steht. Die beiden Eingriffe stehen damit in
|
||||||
|
|||||||
@@ -0,0 +1,468 @@
|
|||||||
|
// Bildschirmsitzung im Browser: noVNC an die WebSocket-Strecke von TESM.
|
||||||
|
//
|
||||||
|
// Der Client handelt das RFB-Protokoll unmittelbar mit dem Geraet aus; TESM
|
||||||
|
// reicht nur Bytes durch. Alles, was diese Datei tut, ist: verbinden, den
|
||||||
|
// Zustand in Worte fassen und nach Zugangsdaten fragen, wenn keine hinterlegt
|
||||||
|
// sind.
|
||||||
|
//
|
||||||
|
// Die Meldungen sind der eigentliche Wert. Eine Sitzung, die schwarz bleibt,
|
||||||
|
// laesst offen, ob es an der Berechtigung, am Netz, am Server oder am
|
||||||
|
// Passwort liegt -- und wer das nicht sieht, sucht den Fehler in TESM.
|
||||||
|
//
|
||||||
|
// **Ein Bauteil, zwei Orte.** Dieselbe Sitzung laeuft auf der Bildschirmseite
|
||||||
|
// und im Fenster der Statusuebersicht. Sie holt alles, was sie braucht, aus
|
||||||
|
// einer JSON-Auskunft des Servers -- damit gibt es keine zweite Fassung, die
|
||||||
|
// man beim naechsten Fund vergisst nachzuziehen.
|
||||||
|
|
||||||
|
import RFB from "../vendor/novnc/core/rfb.js";
|
||||||
|
|
||||||
|
// Drei Eingriffe in private Methoden der mitgelieferten Bibliothek. Bewusst
|
||||||
|
// hier und nicht in der Bibliothek selbst: eine neue Fassung von noVNC laesst
|
||||||
|
// sich dann austauschen, ohne eine Aenderung nachzupflegen -- faellt eine
|
||||||
|
// Methode weg, entfaellt nur die Auskunft, nicht die Sitzung.
|
||||||
|
//
|
||||||
|
// Zwei davon holen nur Auskunft (_fail, _socketClose). Der dritte greift in
|
||||||
|
// die Wahl des Sicherheitsverfahrens ein -- aber erst, **nachdem** eines
|
||||||
|
// nachweislich nicht getragen hat, oder weil eines schon einmal getragen hat.
|
||||||
|
//
|
||||||
|
// Die Vorgeschichte gehoert dazu, damit niemand den Schritt zurueckgeht: hier
|
||||||
|
// stand einmal eine Vorauswahl, die ohne hinterlegte Zugangsdaten ein
|
||||||
|
// Verfahren ohne Anmeldung erzwang, weil RealVNC RA2ne zuerst nennt. Das war
|
||||||
|
// geraten und falsch. Was jetzt hier steht, ist das Gegenteil einer Vermutung:
|
||||||
|
// ein Befund aus einer vorherigen Sitzung.
|
||||||
|
const istUnterstuetzt = RFB.prototype._isSupportedSecurityType;
|
||||||
|
|
||||||
|
//: Den *Grund* eines Fehlschlags kennt nur ``_fail`` -- das Ereignis
|
||||||
|
//: "disconnect" traegt ihn nicht, und der Weg ueber die Protokollausgabe
|
||||||
|
//: funktioniert nicht: die Bibliothek bindet ``console.error`` beim Laden,
|
||||||
|
//: also bevor eine Seite etwas daran aendern koennte.
|
||||||
|
let letzterGrund = "";
|
||||||
|
const originalFail = RFB.prototype._fail;
|
||||||
|
RFB.prototype._fail = function (details) {
|
||||||
|
letzterGrund = String(details);
|
||||||
|
return originalFail.call(this, details);
|
||||||
|
};
|
||||||
|
|
||||||
|
//: Und der Abschluss der Leitung selbst: der Schliesscode sagt, ob TESM
|
||||||
|
//: abgewiesen hat (1008), ob es ein Serverfehler war (1011) -- oder ob die
|
||||||
|
//: Verbindung nie zustande kam (1006).
|
||||||
|
let letzterCode = 0;
|
||||||
|
const originalSocketClose = RFB.prototype._socketClose;
|
||||||
|
RFB.prototype._socketClose = function (ereignis) {
|
||||||
|
letzterCode = (ereignis && ereignis.code) || 0;
|
||||||
|
if (ereignis && ereignis.reason) {
|
||||||
|
letzterGrund = letzterGrund || String(ereignis.reason);
|
||||||
|
}
|
||||||
|
return originalSocketClose.call(this, ereignis);
|
||||||
|
};
|
||||||
|
|
||||||
|
//: Hoechstens ein zweiter Anlauf. Jeder Versuch ist eine Verbindung, und ein
|
||||||
|
//: RealVNC Server zaehlt abgebrochene Versuche mit -- wer dreimal anklopft,
|
||||||
|
//: sperrt sich aus.
|
||||||
|
const HOECHSTENS = 2;
|
||||||
|
|
||||||
|
//: So lange darf "wird aufgebaut" stehen, bevor die Seite selbst nachsieht.
|
||||||
|
//: Kommt die WebSocket-Verbindung gar nicht erst zustande, wartet noVNC ohne
|
||||||
|
//: jedes Ereignis, und die Seite stuende stumm bis zum Seitenwechsel.
|
||||||
|
const AUFBAU_GRENZE = 12000;
|
||||||
|
|
||||||
|
//: Verfahren, die im Browser keinen Schluesseltausch kosten.
|
||||||
|
//:
|
||||||
|
//: Der Grund steht in der Bibliothek, `core/ra2.js`: fuer RA2ne erzeugt der
|
||||||
|
//: **Browser** bei jeder Sitzung ein eigenes 2048-Bit-RSA-Schluesselpaar, in
|
||||||
|
//: JavaScript. Die Primzahlsuche dafuer dauert ein bis drei Minuten -- genau
|
||||||
|
//: die Zeit, die zwischen Klick und Bild lag. "Ohne Authentifizierung" (1)
|
||||||
|
//: kostet davon nichts.
|
||||||
|
//:
|
||||||
|
//: Deshalb kommt es zuerst, **wenn der Server es anbietet** und wir nicht
|
||||||
|
//: ohnehin schon wissen, was hier traegt. Das ist nicht dieselbe Vorauswahl
|
||||||
|
//: wie in 2.8.14: die war geraten, diese steht auf einer Messung. Traegt sie
|
||||||
|
//: nicht, nimmt der zweite Anlauf die Reihenfolge des Servers -- dann dauert
|
||||||
|
//: es eben.
|
||||||
|
const OHNE_SCHLUESSELTAUSCH = new Set([1]);
|
||||||
|
|
||||||
|
//: RA2ne. Wird es ausgehandelt, ist die Wartezeit erklaerbar und kein Fehler.
|
||||||
|
const RA2NE = 6;
|
||||||
|
|
||||||
|
//: Tastenkombinationen, die der Arbeitsplatz selbst abfaengt und die deshalb
|
||||||
|
//: einen Knopf brauchen. X11-Keysyms.
|
||||||
|
const ALT_LINKS = 0xffe9;
|
||||||
|
const F4 = 0xffc1;
|
||||||
|
const F5 = 0xffc2;
|
||||||
|
|
||||||
|
const schema = window.location.protocol === "https:" ? "wss:" : "ws:";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Baut eine Sitzung in diesem Behaelter auf.
|
||||||
|
*
|
||||||
|
* ``quelle`` ist die Adresse der JSON-Auskunft; alles Weitere steht dort.
|
||||||
|
* Rueckgabe: eine Funktion, die die Sitzung wieder abbaut.
|
||||||
|
*/
|
||||||
|
export function starteSitzung(wurzel, quelle, { beiEnde = null } = {}) {
|
||||||
|
const teil = (rolle) => wurzel.querySelector(`[data-rolle="${rolle}"]`);
|
||||||
|
const leinwand = teil("leinwand");
|
||||||
|
const zeile = teil("status");
|
||||||
|
const abfrage = teil("zugangsdaten");
|
||||||
|
const feldBenutzer = teil("benutzer");
|
||||||
|
const feldPasswort = teil("passwort");
|
||||||
|
const knopfSenden = teil("anmelden");
|
||||||
|
const knopfTrennen = teil("trennen");
|
||||||
|
const knoepfeSitzung = [...wurzel.querySelectorAll("[data-rolle^='taste-']"), knopfTrennen];
|
||||||
|
|
||||||
|
const gescheitert = new Set();
|
||||||
|
let rfb = null;
|
||||||
|
let aufbauUhr = 0;
|
||||||
|
let versuche = 0;
|
||||||
|
let stand = false;
|
||||||
|
let daten = null;
|
||||||
|
let letzterZugang = {};
|
||||||
|
let beendet = false;
|
||||||
|
|
||||||
|
function melden(text, art = "") {
|
||||||
|
if (!zeile) return;
|
||||||
|
zeile.textContent = text;
|
||||||
|
zeile.className = `screen__status${art ? " screen__status--" + art : ""}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sitzungsknoepfe(sichtbar) {
|
||||||
|
knoepfeSitzung.forEach((knopf) => {
|
||||||
|
if (knopf) knopf.hidden = !sichtbar;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function grund() {
|
||||||
|
const teile = [];
|
||||||
|
if (letzterGrund) {
|
||||||
|
teile.push(letzterGrund.replace(/^Failed (when|while) \w+: /, ""));
|
||||||
|
}
|
||||||
|
if (rfb && rfb._rfbAuthScheme > 0) {
|
||||||
|
teile.push(`gewähltes Verfahren: ${rfb._rfbAuthScheme}`);
|
||||||
|
}
|
||||||
|
if (gescheitert.size) {
|
||||||
|
teile.push(`erfolglos: ${[...gescheitert].join(", ")}`);
|
||||||
|
}
|
||||||
|
return teile.join(" · ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Steht die WebSocket-Strecke zu TESM überhaupt? Zwei sehr verschiedene
|
||||||
|
// Ursachen sehen von aussen gleich aus: der VNC-Server mag nicht, oder die
|
||||||
|
// Verbindung kommt nie zustande, weil ein Proxy zwischen Browser und TESM
|
||||||
|
// "Upgrade" nicht durchreicht. Diese Probe trennt sie.
|
||||||
|
function streckeTesten() {
|
||||||
|
return new Promise((fertig) => {
|
||||||
|
let beantwortet = false;
|
||||||
|
const pruefling = new WebSocket(`${schema}//${window.location.host}/ws/probe`);
|
||||||
|
const schliessen = (ergebnis) => {
|
||||||
|
if (beantwortet) return;
|
||||||
|
beantwortet = true;
|
||||||
|
try {
|
||||||
|
pruefling.close();
|
||||||
|
} catch (fehler) {
|
||||||
|
/* schon zu */
|
||||||
|
}
|
||||||
|
fertig(ergebnis);
|
||||||
|
};
|
||||||
|
pruefling.onmessage = () => schliessen(true);
|
||||||
|
pruefling.onerror = () => schliessen(false);
|
||||||
|
pruefling.onclose = () => schliessen(false);
|
||||||
|
window.setTimeout(() => schliessen(false), 4000);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hat das gewählte Verfahren die Verbindung abreissen lassen? Dann ist das
|
||||||
|
// ein Befund und keine Vermutung -- der naechste Anlauf laesst es aus. Nur
|
||||||
|
// solange nie ein Bild stand: bricht eine **laufende** Sitzung ab, liegt es
|
||||||
|
// nicht am Anmeldeverfahren.
|
||||||
|
function naechsterVersuch() {
|
||||||
|
const verfahren = rfb && rfb._rfbAuthScheme > 0 ? rfb._rfbAuthScheme : 0;
|
||||||
|
if (!verfahren || versuche >= HOECHSTENS || gescheitert.has(verfahren)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
gescheitert.add(verfahren);
|
||||||
|
melden(
|
||||||
|
`Verfahren ${verfahren} hat die Verbindung nicht getragen – nächster Anlauf ` +
|
||||||
|
"mit dem nächsten, das der Server anbietet …",
|
||||||
|
"warnung",
|
||||||
|
);
|
||||||
|
const zugang = letzterZugang;
|
||||||
|
rfb = null;
|
||||||
|
window.setTimeout(() => verbinden(zugang), 400);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function verfahrenMelden(gewaehlt) {
|
||||||
|
if (!gewaehlt || gewaehlt === daten.bewaehrt || !daten.meldeweg) return;
|
||||||
|
fetch(daten.meldeweg, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "same-origin",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"X-Requested-With": "fetch",
|
||||||
|
"X-CSRF-Token": document.body?.dataset.csrf || "",
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ verfahren: gewaehlt }),
|
||||||
|
}).catch(() => {
|
||||||
|
/* Eine Merkhilfe, kein Teil der Sitzung. */
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function verbinden(zugang) {
|
||||||
|
if (beendet) return;
|
||||||
|
melden(versuche ? "Nächster Versuch …" : "Verbindung wird aufgebaut …");
|
||||||
|
letzterGrund = "";
|
||||||
|
letzterCode = 0;
|
||||||
|
letzterZugang = zugang;
|
||||||
|
versuche += 1;
|
||||||
|
window.clearTimeout(aufbauUhr);
|
||||||
|
aufbauUhr = window.setTimeout(() => {
|
||||||
|
// Ein ausgehandeltes RA2ne ist kein Fehler, sondern Rechenzeit: der
|
||||||
|
// Browser erzeugt dafuer gerade einen 2048-Bit-Schluessel. Das zu
|
||||||
|
// melden ist das Gegenteil einer Warnung -- es nimmt die Sorge.
|
||||||
|
if (rfb && rfb._rfbAuthScheme === RA2NE) {
|
||||||
|
melden(
|
||||||
|
"Der Server verlangt RA2ne (RSA-AES). Dafür erzeugt der Browser ein " +
|
||||||
|
"2048-Bit-Schlüsselpaar – das dauert ein bis drei Minuten. Bitte warten.",
|
||||||
|
"warnung",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
melden("Es kommt keine Antwort. Wird geprüft, woran es liegt …", "warnung");
|
||||||
|
streckeTesten().then((steht) => {
|
||||||
|
melden(
|
||||||
|
steht
|
||||||
|
? "Die WebSocket-Strecke zu TESM steht, aber die Sitzung kommt nicht " +
|
||||||
|
"zustande. Antwortet der VNC-Server auf dem Gerät noch? Ein Neustart " +
|
||||||
|
"des VNC-Dienstes räumt eine Sperre ab."
|
||||||
|
: "Die WebSocket-Verbindung zu TESM kommt nicht zustande – auch eine " +
|
||||||
|
"einfache Probe scheitert. Ein Proxy zwischen Browser und TESM muss " +
|
||||||
|
"die Kopfzeilen Upgrade und Connection durchreichen (nginx: " +
|
||||||
|
"location /ws/ mit proxy_set_header Upgrade $http_upgrade).",
|
||||||
|
"fehler",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}, AUFBAU_GRENZE);
|
||||||
|
|
||||||
|
// Drei Regeln, alle aus Messung und keine aus Vermutung:
|
||||||
|
//
|
||||||
|
// 1. Was schon einmal getragen hat, kommt zuerst.
|
||||||
|
// 2. Sonst das Verfahren ohne Schluesseltausch -- aber nur, wenn der
|
||||||
|
// Server es nachweislich anbietet (sonst faende noVNC gar keines und
|
||||||
|
// scheiterte sofort). RA2ne kostet sonst ein bis drei Minuten
|
||||||
|
// Rechenzeit im Browser, jedes Mal.
|
||||||
|
// 3. Was abgerissen ist, wird nicht noch einmal gewaehlt.
|
||||||
|
//
|
||||||
|
// Beides gilt nur fuer den **ersten** Anlauf; danach zaehlt die
|
||||||
|
// Reihenfolge des Servers, abzueglich dessen, was schon abgerissen ist.
|
||||||
|
const bewaehrt = daten.bewaehrt || 0;
|
||||||
|
const angeboten = daten.verfahren || [];
|
||||||
|
const schnell = angeboten.filter((typ) => OHNE_SCHLUESSELTAUSCH.has(typ));
|
||||||
|
let bevorzugt = 0;
|
||||||
|
if (versuche === 1) {
|
||||||
|
if (bewaehrt > 0 && !gescheitert.has(bewaehrt)) bevorzugt = bewaehrt;
|
||||||
|
else if (schnell.length && !gescheitert.has(schnell[0])) bevorzugt = schnell[0];
|
||||||
|
}
|
||||||
|
RFB.prototype._isSupportedSecurityType = function (typ) {
|
||||||
|
if (gescheitert.has(typ)) return false;
|
||||||
|
if (bevorzugt) return typ === bevorzugt;
|
||||||
|
return istUnterstuetzt.call(this, typ);
|
||||||
|
};
|
||||||
|
|
||||||
|
rfb = new RFB(leinwand, `${schema}//${window.location.host}${daten.socket}`, {
|
||||||
|
credentials: zugang,
|
||||||
|
// Eine Bildschirmsitzung soll nicht nebenbei die Zwischenablage des
|
||||||
|
// Arbeitsplatzes mit dem Geraet teilen -- das ist eine eigene
|
||||||
|
// Entscheidung, und niemand hat sie hier getroffen.
|
||||||
|
clipboardTarget: null,
|
||||||
|
});
|
||||||
|
rfb.scaleViewport = true;
|
||||||
|
rfb.resizeSession = false;
|
||||||
|
rfb.focusOnClick = true;
|
||||||
|
|
||||||
|
rfb.addEventListener("connect", () => {
|
||||||
|
window.clearTimeout(aufbauUhr);
|
||||||
|
stand = true;
|
||||||
|
verfahrenMelden(rfb && rfb._rfbAuthScheme > 0 ? rfb._rfbAuthScheme : 0);
|
||||||
|
melden(`Verbunden mit ${daten.ziel}`, "ok");
|
||||||
|
if (abfrage) abfrage.hidden = true;
|
||||||
|
leinwand.hidden = false;
|
||||||
|
sitzungsknoepfe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
rfb.addEventListener("disconnect", (ereignis) => {
|
||||||
|
window.clearTimeout(aufbauUhr);
|
||||||
|
leinwand.hidden = true;
|
||||||
|
sitzungsknoepfe(false);
|
||||||
|
if (ereignis.detail && ereignis.detail.clean) {
|
||||||
|
// Beim sauberen Schluss steht der Grund im Schliessrahmen -- etwa,
|
||||||
|
// dass TESM wegen Untätigkeit getrennt hat.
|
||||||
|
melden(letzterGrund ? `Sitzung beendet – ${letzterGrund}` : "Sitzung beendet.");
|
||||||
|
// Im Fenster gibt es nach dem Trennen nichts mehr zu sehen: ein leerer
|
||||||
|
// Rahmen, den man wegklicken muss. Bei einem **Abbruch** bleibt es
|
||||||
|
// stehen -- dort steht der Grund, und der ist der ganze Zweck.
|
||||||
|
beiEnde?.();
|
||||||
|
} else if (!stand && naechsterVersuch()) {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
const erklaerung = grund();
|
||||||
|
melden(
|
||||||
|
erklaerung
|
||||||
|
? `Verbindung abgebrochen – ${erklaerung}`
|
||||||
|
: "Verbindung abgebrochen. Wird geprüft, woran es liegt …",
|
||||||
|
"fehler",
|
||||||
|
);
|
||||||
|
if (letzterCode === 1006 || !erklaerung) {
|
||||||
|
streckeTesten().then((steht) => {
|
||||||
|
melden(
|
||||||
|
steht
|
||||||
|
? `${erklaerung || "Verbindung abgebrochen"} – die WebSocket-Strecke zu ` +
|
||||||
|
"TESM selbst steht (Probe erfolgreich), der Abbruch kommt also von " +
|
||||||
|
"der VNC-Seite."
|
||||||
|
: "Die WebSocket-Verbindung zu TESM kommt nicht zustande – auch eine " +
|
||||||
|
"einfache Probe scheitert. Ein Proxy zwischen Browser und TESM muss " +
|
||||||
|
"die Kopfzeilen Upgrade und Connection durchreichen (nginx: " +
|
||||||
|
"location /ws/ mit proxy_set_header Upgrade $http_upgrade). Dasselbe " +
|
||||||
|
"gilt für die Browser-Konsole.",
|
||||||
|
"fehler",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rfb = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
rfb.addEventListener("credentialsrequired", (ereignis) => {
|
||||||
|
window.clearTimeout(aufbauUhr);
|
||||||
|
const gefragt = ereignis.detail.types || ["password"];
|
||||||
|
if (abfrage) abfrage.hidden = false;
|
||||||
|
if (feldBenutzer) feldBenutzer.closest("label").hidden = !gefragt.includes("username");
|
||||||
|
melden("Der VNC-Server verlangt Zugangsdaten.", "warnung");
|
||||||
|
(gefragt.includes("username") ? feldBenutzer : feldPasswort)?.focus();
|
||||||
|
});
|
||||||
|
|
||||||
|
rfb.addEventListener("securityfailure", (ereignis) => {
|
||||||
|
window.clearTimeout(aufbauUhr);
|
||||||
|
melden(
|
||||||
|
`Anmeldung abgelehnt: ${ereignis.detail.reason || "kein Grund genannt"}`,
|
||||||
|
"fehler",
|
||||||
|
);
|
||||||
|
if (abfrage) abfrage.hidden = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
knopfSenden?.addEventListener("click", () => {
|
||||||
|
const zugang = {};
|
||||||
|
if (feldBenutzer?.value) zugang.username = feldBenutzer.value;
|
||||||
|
if (feldPasswort?.value) zugang.password = feldPasswort.value;
|
||||||
|
if (rfb) rfb.sendCredentials(zugang);
|
||||||
|
else verbinden(zugang);
|
||||||
|
});
|
||||||
|
|
||||||
|
[feldBenutzer, feldPasswort].forEach((feld) =>
|
||||||
|
feld?.addEventListener("keydown", (ereignis) => {
|
||||||
|
if (ereignis.key === "Enter") {
|
||||||
|
ereignis.preventDefault();
|
||||||
|
knopfSenden?.click();
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
knopfTrennen?.addEventListener("click", () => rfb && rfb.disconnect());
|
||||||
|
teil("taste-cad")?.addEventListener("click", () => rfb && rfb.sendCtrlAltDel());
|
||||||
|
// Alt+F4 kennt noVNC nicht als fertigen Griff: der Arbeitsplatz faengt die
|
||||||
|
// Kombination selbst ab, bevor sie in der Leinwand ankommt. Also einzeln --
|
||||||
|
// Alt halten, F4 tippen, Alt loslassen -- wie es auf der Tastatur auch
|
||||||
|
// geschieht.
|
||||||
|
teil("taste-altf4")?.addEventListener("click", () => {
|
||||||
|
if (!rfb) return;
|
||||||
|
rfb.sendKey(ALT_LINKS, "AltLeft", true);
|
||||||
|
rfb.sendKey(F4, "F4", true);
|
||||||
|
rfb.sendKey(F4, "F4", false);
|
||||||
|
rfb.sendKey(ALT_LINKS, "AltLeft", false);
|
||||||
|
});
|
||||||
|
// F5 faengt der Browser des Arbeitsplatzes ab -- er wuerde diese Seite neu
|
||||||
|
// laden statt das Bild auf dem Geraet.
|
||||||
|
teil("taste-f5")?.addEventListener("click", () => {
|
||||||
|
if (!rfb) return;
|
||||||
|
rfb.sendKey(F5, "F5", true);
|
||||||
|
rfb.sendKey(F5, "F5", false);
|
||||||
|
});
|
||||||
|
|
||||||
|
sitzungsknoepfe(false);
|
||||||
|
melden("Wird vorbereitet …");
|
||||||
|
|
||||||
|
fetch(quelle, { credentials: "same-origin", headers: { "X-Requested-With": "fetch" } })
|
||||||
|
.then((antwort) => antwort.json().then((inhalt) => ({ status: antwort.status, inhalt })))
|
||||||
|
.then(({ status, inhalt }) => {
|
||||||
|
if (beendet) return;
|
||||||
|
if (status === 409 && inhalt.reauth) {
|
||||||
|
melden(
|
||||||
|
"Für den Bildschirm ist eine frische Passwortbestätigung nötig – bitte über " +
|
||||||
|
"die Geräteseite öffnen.",
|
||||||
|
"warnung",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (status !== 200) {
|
||||||
|
melden(inhalt.fehler || "Diese Sitzung lässt sich nicht öffnen.", "fehler");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
daten = inhalt;
|
||||||
|
if (daten.hinderungsgrund) {
|
||||||
|
melden(daten.hinderungsgrund, "fehler");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const zugang = {};
|
||||||
|
if (daten.vorgaben?.username) zugang.username = daten.vorgaben.username;
|
||||||
|
if (daten.vorgaben?.password) zugang.password = daten.vorgaben.password;
|
||||||
|
verbinden(zugang);
|
||||||
|
})
|
||||||
|
.catch(() => melden("Die Sitzungsdaten sind nicht abrufbar.", "fehler"));
|
||||||
|
|
||||||
|
return function beenden() {
|
||||||
|
beendet = true;
|
||||||
|
window.clearTimeout(aufbauUhr);
|
||||||
|
try {
|
||||||
|
rfb?.disconnect();
|
||||||
|
} catch (fehler) {
|
||||||
|
/* schon zu */
|
||||||
|
}
|
||||||
|
rfb = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// -- Die beiden Orte -------------------------------------------------------- #
|
||||||
|
|
||||||
|
// 1. Die Bildschirmseite: ein Behaelter, der seine Quelle selbst mitbringt.
|
||||||
|
const seite = document.getElementById("screen");
|
||||||
|
if (seite?.dataset.sitzung) {
|
||||||
|
starteSitzung(seite, seite.dataset.sitzung);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Die Statusuebersicht: ein Fenster, das jede Kachel oeffnen kann. Ein
|
||||||
|
// Dialog fuer alle -- er bekommt beim Oeffnen, welches Geraet gemeint ist.
|
||||||
|
const fenster = document.getElementById("vnc-fenster");
|
||||||
|
if (fenster) {
|
||||||
|
let beenden = null;
|
||||||
|
const behaelter = fenster.querySelector("[data-rolle='behaelter']");
|
||||||
|
const titel = fenster.querySelector("[data-rolle='titel']");
|
||||||
|
|
||||||
|
document.addEventListener("click", (ereignis) => {
|
||||||
|
const knopf = ereignis.target.closest("[data-vnc-oeffnen]");
|
||||||
|
if (!knopf) return;
|
||||||
|
ereignis.preventDefault();
|
||||||
|
beenden?.();
|
||||||
|
titel.textContent = knopf.dataset.vncName || "Bildschirm";
|
||||||
|
if (typeof fenster.showModal === "function") fenster.showModal();
|
||||||
|
else fenster.setAttribute("open", "");
|
||||||
|
beenden = starteSitzung(behaelter, knopf.dataset.vncOeffnen, {
|
||||||
|
beiEnde: () => fenster.close(),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Zu heisst zu: eine Sitzung, die im Hintergrund weiterlaeuft, haelt die
|
||||||
|
// Verbindung zum Geraet und bei RealVNC den einen Sitzungsplatz.
|
||||||
|
fenster.addEventListener("close", () => {
|
||||||
|
beenden?.();
|
||||||
|
beenden = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -960,15 +960,22 @@ export function initFilePreview(wurzel = document) {
|
|||||||
const ersetzenFeld = stueck("ersetzen");
|
const ersetzenFeld = stueck("ersetzen");
|
||||||
const ersetzenKasten = stueck("ersetzenfeld");
|
const ersetzenKasten = stueck("ersetzenfeld");
|
||||||
const pruefzeile = stueck("pruefung");
|
const pruefzeile = stueck("pruefung");
|
||||||
// Die Ordner der geoeffneten Freigabe kommen mit der Seite; die einer
|
// Die Ordner der geoeffneten Freigabe stehen bereits in der Auswahl -- der
|
||||||
// anderen holt das Auswahlfeld bei Bedarf nach.
|
// Server hat sie dort hineingeschrieben. Gemerkt werden sie trotzdem, denn
|
||||||
const eigeneOrdner = () => {
|
// wer zwischendurch eine andere Freigabe waehlt, ersetzt die Liste; die
|
||||||
try {
|
// einer anderen Freigabe holt das Auswahlfeld dann bei Bedarf nach.
|
||||||
return JSON.parse(rumpf.dataset.viewerOrdnerliste || "[]");
|
//
|
||||||
} catch {
|
// Vorher stand dieselbe Liste ein zweites Mal daneben, als JSON in einem
|
||||||
return [];
|
// Attribut -- und kam dort nie an: ``tojson`` laesst doppelte
|
||||||
}
|
// Anfuehrungszeichen stehen, und in einem doppelt gequoteten Attribut endet
|
||||||
};
|
// der Wert damit nach der oeffnenden Klammer. Die Auswahl zeigte danach nur
|
||||||
|
// noch das Wurzelverzeichnis, und in einen Unterordner liess sich nichts
|
||||||
|
// mehr speichern. Zwei Fassungen derselben Liste waren die eigentliche
|
||||||
|
// Ursache; jetzt gibt es nur noch eine.
|
||||||
|
const gelieferteOrdner = ordnerFeld
|
||||||
|
? [...ordnerFeld.options].map((eintrag) => eintrag.value).filter(Boolean)
|
||||||
|
: [];
|
||||||
|
const eigeneOrdner = () => gelieferteOrdner;
|
||||||
const jetzt = stueck("jetzt");
|
const jetzt = stueck("jetzt");
|
||||||
const abbrechen = stueck("abbrechen");
|
const abbrechen = stueck("abbrechen");
|
||||||
const modusFeld = stueck("modus");
|
const modusFeld = stueck("modus");
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
maintainers:
|
||||||
|
- Samuel Mannehed for Cendio AB (@samhed)
|
||||||
|
- Pierre Ossman for Cendio AB (@CendioOssman)
|
||||||
|
maintainersEmeritus:
|
||||||
|
- Joel Martin (@kanaka)
|
||||||
|
- Solly Ross (@directxman12)
|
||||||
|
- @astrand
|
||||||
|
contributors:
|
||||||
|
# There are a bunch of people that should be here.
|
||||||
|
# If you want to be on this list, feel free send a PR
|
||||||
|
# to add yourself.
|
||||||
|
- jalf <git@jalf.dk>
|
||||||
|
- NTT corp.
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
# noVNC 1.6.0 -- mitgeliefert, nicht nachgeladen
|
||||||
|
|
||||||
|
Quelle: <https://github.com/novnc/noVNC>, Fassung **1.6.0**, uebernommen am
|
||||||
|
13.09.2026. Enthalten sind `core/` (die Bibliothek) und `vendor/pako` (die
|
||||||
|
Entpackroutine, die `core/inflator.js` braucht) -- ohne `app/`, `tests/` und
|
||||||
|
die mitgelieferte Oberflaeche: die Bedienung stellt TESM selbst
|
||||||
|
(`apps/tesm/src/tesm/templates/tesm/screen.html`).
|
||||||
|
|
||||||
|
**Lizenz:** MPL-2.0 fuer die Kernbibliothek, siehe `LICENSE.txt` und `AUTHORS`
|
||||||
|
(beide unveraendert uebernommen). pako steht unter MIT, siehe
|
||||||
|
`vendor/pako/LICENSE`.
|
||||||
|
|
||||||
|
**Warum mitgeliefert und nicht von einem CDN:** dieselbe Regel wie bei pdf.js
|
||||||
|
und pdf-lib nebenan. Die Anwendung laeuft in Netzen ohne Internetzugang, die
|
||||||
|
Inhaltsrichtlinie (CSP) erlaubt ausschliesslich `'self'`, und eine
|
||||||
|
Bibliothek, die sich zur Laufzeit aendern kann, gehoert nicht in einen Pfad,
|
||||||
|
der einen Bildschirm fernsteuert.
|
||||||
|
|
||||||
|
**Unveraendert.** Wird eine Anpassung noetig, gehoert sie hierher in diese
|
||||||
|
Datei -- mit Grund und Fundstelle, damit ein Wechsel auf eine neuere Fassung
|
||||||
|
nicht stillschweigend darueberlaeuft.
|
||||||
|
|
||||||
|
**Was TESM von aussen umbiegt** (also *nicht* in diesen Dateien steht, sondern
|
||||||
|
in `js/screen.js`):
|
||||||
|
|
||||||
|
* `RFB.prototype._fail` und `_socketClose` -- nur, um den Grund eines
|
||||||
|
Abbruchs zu erfahren. noVNC meldet ihn sonst ausschliesslich in die
|
||||||
|
Entwicklerkonsole, und `console.error` mitzulesen traegt nicht: die
|
||||||
|
Bibliothek bindet es beim Laden.
|
||||||
|
* `RFB.prototype._isSupportedSecurityType` -- **erst nachdem** ein Verfahren
|
||||||
|
die Verbindung nachweislich hat abreissen lassen. Dann wird es fuer den
|
||||||
|
zweiten Anlauf ausgelassen, damit noVNC nicht wieder dasselbe zuerst findet.
|
||||||
|
Hoechstens zwei Anlaeufe, denn jeder ist eine Verbindung, und ein RealVNC
|
||||||
|
Server zaehlt abgebrochene mit.
|
||||||
|
|
||||||
|
Hier stand einmal das Gegenteil: eine **Vorauswahl**, die ohne hinterlegte
|
||||||
|
Zugangsdaten ein Verfahren ohne Anmeldung erzwang. Das war geraten und hat
|
||||||
|
eine laufende Sitzung gebrochen. Der Unterschied ist nicht der Code, sondern
|
||||||
|
der Anlass: eine Vermutung vorher gegen einen Befund hinterher.
|
||||||
|
|
||||||
|
Faellt eine dieser Methoden in einer neueren Fassung weg, entfaellt nur die
|
||||||
|
Auskunft bzw. der zweite Anlauf -- die Sitzung laeuft wie ohne sie.
|
||||||
|
|
||||||
|
**Was noVNC an Sicherheitsverfahren spricht** (`core/rfb.js`,
|
||||||
|
`_isSupportedSecurityType`): `1` None, `2` VNC-Passwort, `6` RA2ne (RSA-AES
|
||||||
|
ohne Streckenverschluesselung), `16` Tight, `19` VeNCrypt, `22` xvp, `30` ARD,
|
||||||
|
`113` MS-Logon II. **Nicht** dabei: `5` (RA2 mit Streckenverschluesselung) und
|
||||||
|
alles aus dem RealVNC-eigenen Bereich `7`-`15`. Ein RealVNC Server, der nur
|
||||||
|
`13` und `5` anbietet, kommt mit dieser Bibliothek also nicht zusammen -- er
|
||||||
|
muss `Encryption=AlwaysOff` (dann `6`) oder `Authentication=VncAuth` (dann
|
||||||
|
`2`) angeboten bekommen. TESM misst das und schreibt es auf die Seite, statt
|
||||||
|
den Benutzer vor eine schwarze Flaeche zu setzen.
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
noVNC is Copyright (C) 2022 The noVNC authors
|
||||||
|
(./AUTHORS)
|
||||||
|
|
||||||
|
The noVNC core library files are licensed under the MPL 2.0 (Mozilla
|
||||||
|
Public License 2.0). The noVNC core library is composed of the
|
||||||
|
Javascript code necessary for full noVNC operation. This includes (but
|
||||||
|
is not limited to):
|
||||||
|
|
||||||
|
core/**/*.js
|
||||||
|
app/*.js
|
||||||
|
test/playback.js
|
||||||
|
|
||||||
|
The HTML, CSS, font and images files that included with the noVNC
|
||||||
|
source distibution (or repository) are not considered part of the
|
||||||
|
noVNC core library and are licensed under more permissive licenses.
|
||||||
|
The intent is to allow easy integration of noVNC into existing web
|
||||||
|
sites and web applications.
|
||||||
|
|
||||||
|
The HTML, CSS, font and image files are licensed as follows:
|
||||||
|
|
||||||
|
*.html : 2-Clause BSD license
|
||||||
|
|
||||||
|
app/styles/*.css : 2-Clause BSD license
|
||||||
|
|
||||||
|
app/styles/Orbitron* : SIL Open Font License 1.1
|
||||||
|
(Copyright 2009 Matt McInerney)
|
||||||
|
|
||||||
|
app/images/ : Creative Commons Attribution-ShareAlike
|
||||||
|
http://creativecommons.org/licenses/by-sa/3.0/
|
||||||
|
|
||||||
|
Some portions of noVNC are copyright to their individual authors.
|
||||||
|
Please refer to the individual source files and/or to the noVNC commit
|
||||||
|
history: https://github.com/novnc/noVNC/commits/master
|
||||||
|
|
||||||
|
The are several files and projects that have been incorporated into
|
||||||
|
the noVNC core library. Here is a list of those files and the original
|
||||||
|
licenses (all MPL 2.0 compatible):
|
||||||
|
|
||||||
|
core/base64.js : MPL 2.0
|
||||||
|
|
||||||
|
core/des.js : Various BSD style licenses
|
||||||
|
|
||||||
|
vendor/pako/ : MIT
|
||||||
|
|
||||||
|
Any other files not mentioned above are typically marked with
|
||||||
|
a copyright/license header at the top of the file. The default noVNC
|
||||||
|
license is MPL-2.0.
|
||||||
|
|
||||||
|
The following license texts are included:
|
||||||
|
|
||||||
|
docs/LICENSE.MPL-2.0
|
||||||
|
docs/LICENSE.OFL-1.1
|
||||||
|
docs/LICENSE.BSD-3-Clause (New BSD)
|
||||||
|
docs/LICENSE.BSD-2-Clause (Simplified BSD / FreeBSD)
|
||||||
|
vendor/pako/LICENSE (MIT)
|
||||||
|
|
||||||
|
Or alternatively the license texts may be found here:
|
||||||
|
|
||||||
|
http://www.mozilla.org/MPL/2.0/
|
||||||
|
http://scripts.sil.org/OFL
|
||||||
|
http://en.wikipedia.org/wiki/BSD_licenses
|
||||||
|
https://opensource.org/licenses/MIT
|
||||||
+104
@@ -0,0 +1,104 @@
|
|||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
// From: http://hg.mozilla.org/mozilla-central/raw-file/ec10630b1a54/js/src/devtools/jint/sunspider/string-base64.js
|
||||||
|
|
||||||
|
import * as Log from './util/logging.js';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
/* Convert data (an array of integers) to a Base64 string. */
|
||||||
|
toBase64Table: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''),
|
||||||
|
base64Pad: '=',
|
||||||
|
|
||||||
|
encode(data) {
|
||||||
|
"use strict";
|
||||||
|
let result = '';
|
||||||
|
const length = data.length;
|
||||||
|
const lengthpad = (length % 3);
|
||||||
|
// Convert every three bytes to 4 ascii characters.
|
||||||
|
|
||||||
|
for (let i = 0; i < (length - 2); i += 3) {
|
||||||
|
result += this.toBase64Table[data[i] >> 2];
|
||||||
|
result += this.toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)];
|
||||||
|
result += this.toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)];
|
||||||
|
result += this.toBase64Table[data[i + 2] & 0x3f];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert the remaining 1 or 2 bytes, pad out to 4 characters.
|
||||||
|
const j = length - lengthpad;
|
||||||
|
if (lengthpad === 2) {
|
||||||
|
result += this.toBase64Table[data[j] >> 2];
|
||||||
|
result += this.toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)];
|
||||||
|
result += this.toBase64Table[(data[j + 1] & 0x0f) << 2];
|
||||||
|
result += this.toBase64Table[64];
|
||||||
|
} else if (lengthpad === 1) {
|
||||||
|
result += this.toBase64Table[data[j] >> 2];
|
||||||
|
result += this.toBase64Table[(data[j] & 0x03) << 4];
|
||||||
|
result += this.toBase64Table[64];
|
||||||
|
result += this.toBase64Table[64];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Convert Base64 data to a string */
|
||||||
|
/* eslint-disable comma-spacing */
|
||||||
|
toBinaryTable: [
|
||||||
|
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
|
||||||
|
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1,
|
||||||
|
-1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63,
|
||||||
|
52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1,
|
||||||
|
-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9,10, 11,12,13,14,
|
||||||
|
15,16,17,18, 19,20,21,22, 23,24,25,-1, -1,-1,-1,-1,
|
||||||
|
-1,26,27,28, 29,30,31,32, 33,34,35,36, 37,38,39,40,
|
||||||
|
41,42,43,44, 45,46,47,48, 49,50,51,-1, -1,-1,-1,-1
|
||||||
|
],
|
||||||
|
/* eslint-enable comma-spacing */
|
||||||
|
|
||||||
|
decode(data, offset = 0) {
|
||||||
|
let dataLength = data.indexOf('=') - offset;
|
||||||
|
if (dataLength < 0) { dataLength = data.length - offset; }
|
||||||
|
|
||||||
|
/* Every four characters is 3 resulting numbers */
|
||||||
|
const resultLength = (dataLength >> 2) * 3 + Math.floor((dataLength % 4) / 1.5);
|
||||||
|
const result = new Array(resultLength);
|
||||||
|
|
||||||
|
// Convert one by one.
|
||||||
|
|
||||||
|
let leftbits = 0; // number of bits decoded, but yet to be appended
|
||||||
|
let leftdata = 0; // bits decoded, but yet to be appended
|
||||||
|
for (let idx = 0, i = offset; i < data.length; i++) {
|
||||||
|
const c = this.toBinaryTable[data.charCodeAt(i) & 0x7f];
|
||||||
|
const padding = (data.charAt(i) === this.base64Pad);
|
||||||
|
// Skip illegal characters and whitespace
|
||||||
|
if (c === -1) {
|
||||||
|
Log.Error("Illegal character code " + data.charCodeAt(i) + " at position " + i);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect data into leftdata, update bitcount
|
||||||
|
leftdata = (leftdata << 6) | c;
|
||||||
|
leftbits += 6;
|
||||||
|
|
||||||
|
// If we have 8 or more bits, append 8 bits to the result
|
||||||
|
if (leftbits >= 8) {
|
||||||
|
leftbits -= 8;
|
||||||
|
// Append if not padding.
|
||||||
|
if (!padding) {
|
||||||
|
result[idx++] = (leftdata >> leftbits) & 0xff;
|
||||||
|
}
|
||||||
|
leftdata &= (1 << leftbits) - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If there are any bits left, the base64 string was corrupted
|
||||||
|
if (leftbits) {
|
||||||
|
const err = new Error('Corrupted base64 string');
|
||||||
|
err.name = 'Base64-Error';
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}; /* End of Base64 namespace */
|
||||||
+178
@@ -0,0 +1,178 @@
|
|||||||
|
export class AESECBCipher {
|
||||||
|
constructor() {
|
||||||
|
this._key = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "AES-ECB" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importKey(key, _algorithm, extractable, keyUsages) {
|
||||||
|
const cipher = new AESECBCipher;
|
||||||
|
await cipher._importKey(key, extractable, keyUsages);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _importKey(key, extractable, keyUsages) {
|
||||||
|
this._key = await window.crypto.subtle.importKey(
|
||||||
|
"raw", key, {name: "AES-CBC"}, extractable, keyUsages);
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(_algorithm, plaintext) {
|
||||||
|
const x = new Uint8Array(plaintext);
|
||||||
|
if (x.length % 16 !== 0 || this._key === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = x.length / 16;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
const y = new Uint8Array(await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: new Uint8Array(16),
|
||||||
|
}, this._key, x.slice(i * 16, i * 16 + 16))).slice(0, 16);
|
||||||
|
x.set(y, i * 16);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AESEAXCipher {
|
||||||
|
constructor() {
|
||||||
|
this._rawKey = null;
|
||||||
|
this._ctrKey = null;
|
||||||
|
this._cbcKey = null;
|
||||||
|
this._zeroBlock = new Uint8Array(16);
|
||||||
|
this._prefixBlock0 = this._zeroBlock;
|
||||||
|
this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
|
||||||
|
this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "AES-EAX" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async _encryptBlock(block) {
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: this._zeroBlock,
|
||||||
|
}, this._cbcKey, block);
|
||||||
|
return new Uint8Array(encrypted).slice(0, 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _initCMAC() {
|
||||||
|
const k1 = await this._encryptBlock(this._zeroBlock);
|
||||||
|
const k2 = new Uint8Array(16);
|
||||||
|
const v = k1[0] >>> 6;
|
||||||
|
for (let i = 0; i < 15; i++) {
|
||||||
|
k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
|
||||||
|
k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
|
||||||
|
}
|
||||||
|
const lut = [0x0, 0x87, 0x0e, 0x89];
|
||||||
|
k2[14] ^= v >>> 1;
|
||||||
|
k2[15] = (k1[15] << 2) ^ lut[v];
|
||||||
|
k1[15] = (k1[15] << 1) ^ lut[v >> 1];
|
||||||
|
this._k1 = k1;
|
||||||
|
this._k2 = k2;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _encryptCTR(data, counter) {
|
||||||
|
const encrypted = await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CTR",
|
||||||
|
counter: counter,
|
||||||
|
length: 128
|
||||||
|
}, this._ctrKey, data);
|
||||||
|
return new Uint8Array(encrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _decryptCTR(data, counter) {
|
||||||
|
const decrypted = await window.crypto.subtle.decrypt({
|
||||||
|
name: "AES-CTR",
|
||||||
|
counter: counter,
|
||||||
|
length: 128
|
||||||
|
}, this._ctrKey, data);
|
||||||
|
return new Uint8Array(decrypted);
|
||||||
|
}
|
||||||
|
|
||||||
|
async _computeCMAC(data, prefixBlock) {
|
||||||
|
if (prefixBlock.length !== 16) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = Math.floor(data.length / 16);
|
||||||
|
const m = Math.ceil(data.length / 16);
|
||||||
|
const r = data.length - n * 16;
|
||||||
|
const cbcData = new Uint8Array((m + 1) * 16);
|
||||||
|
cbcData.set(prefixBlock);
|
||||||
|
cbcData.set(data, 16);
|
||||||
|
if (r === 0) {
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
cbcData[n * 16 + i] ^= this._k1[i];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
cbcData[(n + 1) * 16 + r] = 0x80;
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
cbcData[(n + 1) * 16 + i] ^= this._k2[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cbcEncrypted = await window.crypto.subtle.encrypt({
|
||||||
|
name: "AES-CBC",
|
||||||
|
iv: this._zeroBlock,
|
||||||
|
}, this._cbcKey, cbcData);
|
||||||
|
|
||||||
|
cbcEncrypted = new Uint8Array(cbcEncrypted);
|
||||||
|
const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
|
||||||
|
return mac;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importKey(key, _algorithm, _extractable, _keyUsages) {
|
||||||
|
const cipher = new AESEAXCipher;
|
||||||
|
await cipher._importKey(key);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _importKey(key) {
|
||||||
|
this._rawKey = key;
|
||||||
|
this._ctrKey = await window.crypto.subtle.importKey(
|
||||||
|
"raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]);
|
||||||
|
this._cbcKey = await window.crypto.subtle.importKey(
|
||||||
|
"raw", key, {name: "AES-CBC"}, false, ["encrypt"]);
|
||||||
|
await this._initCMAC();
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(algorithm, message) {
|
||||||
|
const ad = algorithm.additionalData;
|
||||||
|
const nonce = algorithm.iv;
|
||||||
|
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
||||||
|
const encrypted = await this._encryptCTR(message, nCMAC);
|
||||||
|
const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
|
||||||
|
const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
mac[i] ^= nCMAC[i] ^ adCMAC[i];
|
||||||
|
}
|
||||||
|
const res = new Uint8Array(16 + encrypted.length);
|
||||||
|
res.set(encrypted);
|
||||||
|
res.set(mac, encrypted.length);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(algorithm, data) {
|
||||||
|
const encrypted = data.slice(0, data.length - 16);
|
||||||
|
const ad = algorithm.additionalData;
|
||||||
|
const nonce = algorithm.iv;
|
||||||
|
const mac = data.slice(data.length - 16);
|
||||||
|
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
|
||||||
|
const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
|
||||||
|
const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
|
||||||
|
for (let i = 0; i < 16; i++) {
|
||||||
|
computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
|
||||||
|
}
|
||||||
|
if (computedMac.length !== mac.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < mac.length; i++) {
|
||||||
|
if (computedMac[i] !== mac[i]) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const res = await this._decryptCTR(encrypted, nCMAC);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
export function modPow(b, e, m) {
|
||||||
|
let r = 1n;
|
||||||
|
b = b % m;
|
||||||
|
while (e > 0n) {
|
||||||
|
if ((e & 1n) === 1n) {
|
||||||
|
r = (r * b) % m;
|
||||||
|
}
|
||||||
|
e = e >> 1n;
|
||||||
|
b = (b * b) % m;
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function bigIntToU8Array(bigint, padLength=0) {
|
||||||
|
let hex = bigint.toString(16);
|
||||||
|
if (padLength === 0) {
|
||||||
|
padLength = Math.ceil(hex.length / 2);
|
||||||
|
}
|
||||||
|
hex = hex.padStart(padLength * 2, '0');
|
||||||
|
const length = hex.length / 2;
|
||||||
|
const arr = new Uint8Array(length);
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
arr[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function u8ArrayToBigInt(arr) {
|
||||||
|
let hex = '0x';
|
||||||
|
for (let i = 0; i < arr.length; i++) {
|
||||||
|
hex += arr[i].toString(16).padStart(2, '0');
|
||||||
|
}
|
||||||
|
return BigInt(hex);
|
||||||
|
}
|
||||||
+90
@@ -0,0 +1,90 @@
|
|||||||
|
import { AESECBCipher, AESEAXCipher } from "./aes.js";
|
||||||
|
import { DESCBCCipher, DESECBCipher } from "./des.js";
|
||||||
|
import { RSACipher } from "./rsa.js";
|
||||||
|
import { DHCipher } from "./dh.js";
|
||||||
|
import { MD5 } from "./md5.js";
|
||||||
|
|
||||||
|
// A single interface for the cryptographic algorithms not supported by SubtleCrypto.
|
||||||
|
// Both synchronous and asynchronous implmentations are allowed.
|
||||||
|
class LegacyCrypto {
|
||||||
|
constructor() {
|
||||||
|
this._algorithms = {
|
||||||
|
"AES-ECB": AESECBCipher,
|
||||||
|
"AES-EAX": AESEAXCipher,
|
||||||
|
"DES-ECB": DESECBCipher,
|
||||||
|
"DES-CBC": DESCBCCipher,
|
||||||
|
"RSA-PKCS1-v1_5": RSACipher,
|
||||||
|
"DH": DHCipher,
|
||||||
|
"MD5": MD5,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(algorithm, key, data) {
|
||||||
|
if (key.algorithm.name !== algorithm.name) {
|
||||||
|
throw new Error("algorithm does not match");
|
||||||
|
}
|
||||||
|
if (typeof key.encrypt !== "function") {
|
||||||
|
throw new Error("key does not support encryption");
|
||||||
|
}
|
||||||
|
return key.encrypt(algorithm, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
decrypt(algorithm, key, data) {
|
||||||
|
if (key.algorithm.name !== algorithm.name) {
|
||||||
|
throw new Error("algorithm does not match");
|
||||||
|
}
|
||||||
|
if (typeof key.decrypt !== "function") {
|
||||||
|
throw new Error("key does not support encryption");
|
||||||
|
}
|
||||||
|
return key.decrypt(algorithm, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
importKey(format, keyData, algorithm, extractable, keyUsages) {
|
||||||
|
if (format !== "raw") {
|
||||||
|
throw new Error("key format is not supported");
|
||||||
|
}
|
||||||
|
const alg = this._algorithms[algorithm.name];
|
||||||
|
if (typeof alg === "undefined" || typeof alg.importKey !== "function") {
|
||||||
|
throw new Error("algorithm is not supported");
|
||||||
|
}
|
||||||
|
return alg.importKey(keyData, algorithm, extractable, keyUsages);
|
||||||
|
}
|
||||||
|
|
||||||
|
generateKey(algorithm, extractable, keyUsages) {
|
||||||
|
const alg = this._algorithms[algorithm.name];
|
||||||
|
if (typeof alg === "undefined" || typeof alg.generateKey !== "function") {
|
||||||
|
throw new Error("algorithm is not supported");
|
||||||
|
}
|
||||||
|
return alg.generateKey(algorithm, extractable, keyUsages);
|
||||||
|
}
|
||||||
|
|
||||||
|
exportKey(format, key) {
|
||||||
|
if (format !== "raw") {
|
||||||
|
throw new Error("key format is not supported");
|
||||||
|
}
|
||||||
|
if (typeof key.exportKey !== "function") {
|
||||||
|
throw new Error("key does not support exportKey");
|
||||||
|
}
|
||||||
|
return key.exportKey();
|
||||||
|
}
|
||||||
|
|
||||||
|
digest(algorithm, data) {
|
||||||
|
const alg = this._algorithms[algorithm];
|
||||||
|
if (typeof alg !== "function") {
|
||||||
|
throw new Error("algorithm is not supported");
|
||||||
|
}
|
||||||
|
return alg(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
deriveBits(algorithm, key, length) {
|
||||||
|
if (key.algorithm.name !== algorithm.name) {
|
||||||
|
throw new Error("algorithm does not match");
|
||||||
|
}
|
||||||
|
if (typeof key.deriveBits !== "function") {
|
||||||
|
throw new Error("key does not support deriveBits");
|
||||||
|
}
|
||||||
|
return key.deriveBits(algorithm, length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default new LegacyCrypto;
|
||||||
+330
@@ -0,0 +1,330 @@
|
|||||||
|
/*
|
||||||
|
* Ported from Flashlight VNC ActionScript implementation:
|
||||||
|
* http://www.wizhelp.com/flashlight-vnc/
|
||||||
|
*
|
||||||
|
* Full attribution follows:
|
||||||
|
*
|
||||||
|
* -------------------------------------------------------------------------
|
||||||
|
*
|
||||||
|
* This DES class has been extracted from package Acme.Crypto for use in VNC.
|
||||||
|
* The unnecessary odd parity code has been removed.
|
||||||
|
*
|
||||||
|
* These changes are:
|
||||||
|
* Copyright (C) 1999 AT&T Laboratories Cambridge. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* This software 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.
|
||||||
|
*
|
||||||
|
|
||||||
|
* DesCipher - the DES encryption method
|
||||||
|
*
|
||||||
|
* The meat of this code is by Dave Zimmerman <dzimm@widget.com>, and is:
|
||||||
|
*
|
||||||
|
* Copyright (c) 1996 Widget Workshop, Inc. All Rights Reserved.
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and distribute this software
|
||||||
|
* and its documentation for NON-COMMERCIAL or COMMERCIAL purposes and
|
||||||
|
* without fee is hereby granted, provided that this copyright notice is kept
|
||||||
|
* intact.
|
||||||
|
*
|
||||||
|
* WIDGET WORKSHOP MAKES NO REPRESENTATIONS OR WARRANTIES ABOUT THE SUITABILITY
|
||||||
|
* OF THE SOFTWARE, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
|
||||||
|
* TO THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
|
||||||
|
* PARTICULAR PURPOSE, OR NON-INFRINGEMENT. WIDGET WORKSHOP SHALL NOT BE LIABLE
|
||||||
|
* FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF USING, MODIFYING OR
|
||||||
|
* DISTRIBUTING THIS SOFTWARE OR ITS DERIVATIVES.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS NOT DESIGNED OR INTENDED FOR USE OR RESALE AS ON-LINE
|
||||||
|
* CONTROL EQUIPMENT IN HAZARDOUS ENVIRONMENTS REQUIRING FAIL-SAFE
|
||||||
|
* PERFORMANCE, SUCH AS IN THE OPERATION OF NUCLEAR FACILITIES, AIRCRAFT
|
||||||
|
* NAVIGATION OR COMMUNICATION SYSTEMS, AIR TRAFFIC CONTROL, DIRECT LIFE
|
||||||
|
* SUPPORT MACHINES, OR WEAPONS SYSTEMS, IN WHICH THE FAILURE OF THE
|
||||||
|
* SOFTWARE COULD LEAD DIRECTLY TO DEATH, PERSONAL INJURY, OR SEVERE
|
||||||
|
* PHYSICAL OR ENVIRONMENTAL DAMAGE ("HIGH RISK ACTIVITIES"). WIDGET WORKSHOP
|
||||||
|
* SPECIFICALLY DISCLAIMS ANY EXPRESS OR IMPLIED WARRANTY OF FITNESS FOR
|
||||||
|
* HIGH RISK ACTIVITIES.
|
||||||
|
*
|
||||||
|
*
|
||||||
|
* The rest is:
|
||||||
|
*
|
||||||
|
* Copyright (C) 1996 by Jef Poskanzer <jef@acme.com>. All rights reserved.
|
||||||
|
*
|
||||||
|
* Redistribution and use in source and binary forms, with or without
|
||||||
|
* modification, are permitted provided that the following conditions
|
||||||
|
* are met:
|
||||||
|
* 1. Redistributions of source code must retain the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer.
|
||||||
|
* 2. Redistributions in binary form must reproduce the above copyright
|
||||||
|
* notice, this list of conditions and the following disclaimer in the
|
||||||
|
* documentation and/or other materials provided with the distribution.
|
||||||
|
*
|
||||||
|
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
||||||
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||||
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
||||||
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
||||||
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||||
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
||||||
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
||||||
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
||||||
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
||||||
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
||||||
|
* SUCH DAMAGE.
|
||||||
|
*
|
||||||
|
* Visit the ACME Labs Java page for up-to-date versions of this and other
|
||||||
|
* fine Java utilities: http://www.acme.com/java/
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable comma-spacing */
|
||||||
|
|
||||||
|
// Tables, permutations, S-boxes, etc.
|
||||||
|
const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3,
|
||||||
|
25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39,
|
||||||
|
50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ],
|
||||||
|
totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28];
|
||||||
|
|
||||||
|
const z = 0x0;
|
||||||
|
let a,b,c,d,e,f;
|
||||||
|
a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e;
|
||||||
|
const SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d,
|
||||||
|
z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z,
|
||||||
|
a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f,
|
||||||
|
c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d];
|
||||||
|
a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e;
|
||||||
|
const SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d,
|
||||||
|
a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f,
|
||||||
|
z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z,
|
||||||
|
z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e];
|
||||||
|
a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e;
|
||||||
|
const SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f,
|
||||||
|
b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z,
|
||||||
|
c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d,
|
||||||
|
b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e];
|
||||||
|
a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e;
|
||||||
|
const SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d,
|
||||||
|
z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f,
|
||||||
|
b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e,
|
||||||
|
c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e];
|
||||||
|
a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e;
|
||||||
|
const SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z,
|
||||||
|
a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f,
|
||||||
|
z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e,
|
||||||
|
c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d];
|
||||||
|
a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e;
|
||||||
|
const SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f,
|
||||||
|
z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z,
|
||||||
|
b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z,
|
||||||
|
a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f];
|
||||||
|
a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e;
|
||||||
|
const SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f,
|
||||||
|
b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e,
|
||||||
|
b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e,
|
||||||
|
z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d];
|
||||||
|
a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e;
|
||||||
|
const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d,
|
||||||
|
c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z,
|
||||||
|
a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f,
|
||||||
|
z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e];
|
||||||
|
|
||||||
|
/* eslint-enable comma-spacing */
|
||||||
|
|
||||||
|
class DES {
|
||||||
|
constructor(password) {
|
||||||
|
this.keys = [];
|
||||||
|
|
||||||
|
// Set the key.
|
||||||
|
const pc1m = [], pcr = [], kn = [];
|
||||||
|
|
||||||
|
for (let j = 0, l = 56; j < 56; ++j, l -= 8) {
|
||||||
|
l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1
|
||||||
|
const m = l & 0x7;
|
||||||
|
pc1m[j] = ((password[l >>> 3] & (1<<m)) !== 0) ? 1: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < 16; ++i) {
|
||||||
|
const m = i << 1;
|
||||||
|
const n = m + 1;
|
||||||
|
kn[m] = kn[n] = 0;
|
||||||
|
for (let o = 28; o < 59; o += 28) {
|
||||||
|
for (let j = o - 28; j < o; ++j) {
|
||||||
|
const l = j + totrot[i];
|
||||||
|
pcr[j] = l < o ? pc1m[l] : pc1m[l - 28];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (let j = 0; j < 24; ++j) {
|
||||||
|
if (pcr[PC2[j]] !== 0) {
|
||||||
|
kn[m] |= 1 << (23 - j);
|
||||||
|
}
|
||||||
|
if (pcr[PC2[j + 24]] !== 0) {
|
||||||
|
kn[n] |= 1 << (23 - j);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// cookey
|
||||||
|
for (let i = 0, rawi = 0, KnLi = 0; i < 16; ++i) {
|
||||||
|
const raw0 = kn[rawi++];
|
||||||
|
const raw1 = kn[rawi++];
|
||||||
|
this.keys[KnLi] = (raw0 & 0x00fc0000) << 6;
|
||||||
|
this.keys[KnLi] |= (raw0 & 0x00000fc0) << 10;
|
||||||
|
this.keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10;
|
||||||
|
this.keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6;
|
||||||
|
++KnLi;
|
||||||
|
this.keys[KnLi] = (raw0 & 0x0003f000) << 12;
|
||||||
|
this.keys[KnLi] |= (raw0 & 0x0000003f) << 16;
|
||||||
|
this.keys[KnLi] |= (raw1 & 0x0003f000) >>> 4;
|
||||||
|
this.keys[KnLi] |= (raw1 & 0x0000003f);
|
||||||
|
++KnLi;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt 8 bytes of text
|
||||||
|
enc8(text) {
|
||||||
|
const b = text.slice();
|
||||||
|
let i = 0, l, r, x; // left, right, accumulator
|
||||||
|
|
||||||
|
// Squash 8 bytes to 2 ints
|
||||||
|
l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
|
||||||
|
r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++];
|
||||||
|
|
||||||
|
x = ((l >>> 4) ^ r) & 0x0f0f0f0f;
|
||||||
|
r ^= x;
|
||||||
|
l ^= (x << 4);
|
||||||
|
x = ((l >>> 16) ^ r) & 0x0000ffff;
|
||||||
|
r ^= x;
|
||||||
|
l ^= (x << 16);
|
||||||
|
x = ((r >>> 2) ^ l) & 0x33333333;
|
||||||
|
l ^= x;
|
||||||
|
r ^= (x << 2);
|
||||||
|
x = ((r >>> 8) ^ l) & 0x00ff00ff;
|
||||||
|
l ^= x;
|
||||||
|
r ^= (x << 8);
|
||||||
|
r = (r << 1) | ((r >>> 31) & 1);
|
||||||
|
x = (l ^ r) & 0xaaaaaaaa;
|
||||||
|
l ^= x;
|
||||||
|
r ^= x;
|
||||||
|
l = (l << 1) | ((l >>> 31) & 1);
|
||||||
|
|
||||||
|
for (let i = 0, keysi = 0; i < 8; ++i) {
|
||||||
|
x = (r << 28) | (r >>> 4);
|
||||||
|
x ^= this.keys[keysi++];
|
||||||
|
let fval = SP7[x & 0x3f];
|
||||||
|
fval |= SP5[(x >>> 8) & 0x3f];
|
||||||
|
fval |= SP3[(x >>> 16) & 0x3f];
|
||||||
|
fval |= SP1[(x >>> 24) & 0x3f];
|
||||||
|
x = r ^ this.keys[keysi++];
|
||||||
|
fval |= SP8[x & 0x3f];
|
||||||
|
fval |= SP6[(x >>> 8) & 0x3f];
|
||||||
|
fval |= SP4[(x >>> 16) & 0x3f];
|
||||||
|
fval |= SP2[(x >>> 24) & 0x3f];
|
||||||
|
l ^= fval;
|
||||||
|
x = (l << 28) | (l >>> 4);
|
||||||
|
x ^= this.keys[keysi++];
|
||||||
|
fval = SP7[x & 0x3f];
|
||||||
|
fval |= SP5[(x >>> 8) & 0x3f];
|
||||||
|
fval |= SP3[(x >>> 16) & 0x3f];
|
||||||
|
fval |= SP1[(x >>> 24) & 0x3f];
|
||||||
|
x = l ^ this.keys[keysi++];
|
||||||
|
fval |= SP8[x & 0x0000003f];
|
||||||
|
fval |= SP6[(x >>> 8) & 0x3f];
|
||||||
|
fval |= SP4[(x >>> 16) & 0x3f];
|
||||||
|
fval |= SP2[(x >>> 24) & 0x3f];
|
||||||
|
r ^= fval;
|
||||||
|
}
|
||||||
|
|
||||||
|
r = (r << 31) | (r >>> 1);
|
||||||
|
x = (l ^ r) & 0xaaaaaaaa;
|
||||||
|
l ^= x;
|
||||||
|
r ^= x;
|
||||||
|
l = (l << 31) | (l >>> 1);
|
||||||
|
x = ((l >>> 8) ^ r) & 0x00ff00ff;
|
||||||
|
r ^= x;
|
||||||
|
l ^= (x << 8);
|
||||||
|
x = ((l >>> 2) ^ r) & 0x33333333;
|
||||||
|
r ^= x;
|
||||||
|
l ^= (x << 2);
|
||||||
|
x = ((r >>> 16) ^ l) & 0x0000ffff;
|
||||||
|
l ^= x;
|
||||||
|
r ^= (x << 16);
|
||||||
|
x = ((r >>> 4) ^ l) & 0x0f0f0f0f;
|
||||||
|
l ^= x;
|
||||||
|
r ^= (x << 4);
|
||||||
|
|
||||||
|
// Spread ints to bytes
|
||||||
|
x = [r, l];
|
||||||
|
for (i = 0; i < 8; i++) {
|
||||||
|
b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256;
|
||||||
|
if (b[i] < 0) { b[i] += 256; } // unsigned
|
||||||
|
}
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DESECBCipher {
|
||||||
|
constructor() {
|
||||||
|
this._cipher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DES-ECB" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static importKey(key, _algorithm, _extractable, _keyUsages) {
|
||||||
|
const cipher = new DESECBCipher;
|
||||||
|
cipher._importKey(key);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
_importKey(key, _extractable, _keyUsages) {
|
||||||
|
this._cipher = new DES(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(_algorithm, plaintext) {
|
||||||
|
const x = new Uint8Array(plaintext);
|
||||||
|
if (x.length % 8 !== 0 || this._cipher === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = x.length / 8;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
x.set(this._cipher.enc8(x.slice(i * 8, i * 8 + 8)), i * 8);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DESCBCCipher {
|
||||||
|
constructor() {
|
||||||
|
this._cipher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DES-CBC" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static importKey(key, _algorithm, _extractable, _keyUsages) {
|
||||||
|
const cipher = new DESCBCCipher;
|
||||||
|
cipher._importKey(key);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
_importKey(key) {
|
||||||
|
this._cipher = new DES(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
encrypt(algorithm, plaintext) {
|
||||||
|
const x = new Uint8Array(plaintext);
|
||||||
|
let y = new Uint8Array(algorithm.iv);
|
||||||
|
if (x.length % 8 !== 0 || this._cipher === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const n = x.length / 8;
|
||||||
|
for (let i = 0; i < n; i++) {
|
||||||
|
for (let j = 0; j < 8; j++) {
|
||||||
|
y[j] ^= plaintext[i * 8 + j];
|
||||||
|
}
|
||||||
|
y = this._cipher.enc8(y);
|
||||||
|
x.set(y, i * 8);
|
||||||
|
}
|
||||||
|
return x;
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
|
||||||
|
|
||||||
|
class DHPublicKey {
|
||||||
|
constructor(key) {
|
||||||
|
this._key = key;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DH" };
|
||||||
|
}
|
||||||
|
|
||||||
|
exportKey() {
|
||||||
|
return this._key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class DHCipher {
|
||||||
|
constructor() {
|
||||||
|
this._g = null;
|
||||||
|
this._p = null;
|
||||||
|
this._gBigInt = null;
|
||||||
|
this._pBigInt = null;
|
||||||
|
this._privateKey = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "DH" };
|
||||||
|
}
|
||||||
|
|
||||||
|
static generateKey(algorithm, _extractable) {
|
||||||
|
const cipher = new DHCipher;
|
||||||
|
cipher._generateKey(algorithm);
|
||||||
|
return { privateKey: cipher, publicKey: new DHPublicKey(cipher._publicKey) };
|
||||||
|
}
|
||||||
|
|
||||||
|
_generateKey(algorithm) {
|
||||||
|
const g = algorithm.g;
|
||||||
|
const p = algorithm.p;
|
||||||
|
this._keyBytes = p.length;
|
||||||
|
this._gBigInt = u8ArrayToBigInt(g);
|
||||||
|
this._pBigInt = u8ArrayToBigInt(p);
|
||||||
|
this._privateKey = window.crypto.getRandomValues(new Uint8Array(this._keyBytes));
|
||||||
|
this._privateKeyBigInt = u8ArrayToBigInt(this._privateKey);
|
||||||
|
this._publicKey = bigIntToU8Array(modPow(
|
||||||
|
this._gBigInt, this._privateKeyBigInt, this._pBigInt), this._keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
deriveBits(algorithm, length) {
|
||||||
|
const bytes = Math.ceil(length / 8);
|
||||||
|
const pkey = new Uint8Array(algorithm.public);
|
||||||
|
const len = bytes > this._keyBytes ? bytes : this._keyBytes;
|
||||||
|
const secret = modPow(u8ArrayToBigInt(pkey), this._privateKeyBigInt, this._pBigInt);
|
||||||
|
return bigIntToU8Array(secret, len).slice(0, len);
|
||||||
|
}
|
||||||
|
}
|
||||||
+82
@@ -0,0 +1,82 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2021 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Performs MD5 hashing on an array of bytes, returns an array of bytes
|
||||||
|
*/
|
||||||
|
|
||||||
|
export async function MD5(d) {
|
||||||
|
let s = "";
|
||||||
|
for (let i = 0; i < d.length; i++) {
|
||||||
|
s += String.fromCharCode(d[i]);
|
||||||
|
}
|
||||||
|
return M(V(Y(X(s), 8 * s.length)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function M(d) {
|
||||||
|
let f = new Uint8Array(d.length);
|
||||||
|
for (let i=0;i<d.length;i++) {
|
||||||
|
f[i] = d.charCodeAt(i);
|
||||||
|
}
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
function X(d) {
|
||||||
|
let r = Array(d.length >> 2);
|
||||||
|
for (let m = 0; m < r.length; m++) r[m] = 0;
|
||||||
|
for (let m = 0; m < 8 * d.length; m += 8) r[m >> 5] |= (255 & d.charCodeAt(m / 8)) << m % 32;
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function V(d) {
|
||||||
|
let r = "";
|
||||||
|
for (let m = 0; m < 32 * d.length; m += 8) r += String.fromCharCode(d[m >> 5] >>> m % 32 & 255);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Y(d, g) {
|
||||||
|
d[g >> 5] |= 128 << g % 32, d[14 + (g + 64 >>> 9 << 4)] = g;
|
||||||
|
let m = 1732584193, f = -271733879, r = -1732584194, i = 271733878;
|
||||||
|
for (let n = 0; n < d.length; n += 16) {
|
||||||
|
let h = m,
|
||||||
|
t = f,
|
||||||
|
g = r,
|
||||||
|
e = i;
|
||||||
|
f = ii(f = ii(f = ii(f = ii(f = hh(f = hh(f = hh(f = hh(f = gg(f = gg(f = gg(f = gg(f = ff(f = ff(f = ff(f = ff(f, r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 0], 7, -680876936), f, r, d[n + 1], 12, -389564586), m, f, d[n + 2], 17, 606105819), i, m, d[n + 3], 22, -1044525330), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 4], 7, -176418897), f, r, d[n + 5], 12, 1200080426), m, f, d[n + 6], 17, -1473231341), i, m, d[n + 7], 22, -45705983), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 8], 7, 1770035416), f, r, d[n + 9], 12, -1958414417), m, f, d[n + 10], 17, -42063), i, m, d[n + 11], 22, -1990404162), r = ff(r, i = ff(i, m = ff(m, f, r, i, d[n + 12], 7, 1804603682), f, r, d[n + 13], 12, -40341101), m, f, d[n + 14], 17, -1502002290), i, m, d[n + 15], 22, 1236535329), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 1], 5, -165796510), f, r, d[n + 6], 9, -1069501632), m, f, d[n + 11], 14, 643717713), i, m, d[n + 0], 20, -373897302), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 5], 5, -701558691), f, r, d[n + 10], 9, 38016083), m, f, d[n + 15], 14, -660478335), i, m, d[n + 4], 20, -405537848), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 9], 5, 568446438), f, r, d[n + 14], 9, -1019803690), m, f, d[n + 3], 14, -187363961), i, m, d[n + 8], 20, 1163531501), r = gg(r, i = gg(i, m = gg(m, f, r, i, d[n + 13], 5, -1444681467), f, r, d[n + 2], 9, -51403784), m, f, d[n + 7], 14, 1735328473), i, m, d[n + 12], 20, -1926607734), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 5], 4, -378558), f, r, d[n + 8], 11, -2022574463), m, f, d[n + 11], 16, 1839030562), i, m, d[n + 14], 23, -35309556), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 1], 4, -1530992060), f, r, d[n + 4], 11, 1272893353), m, f, d[n + 7], 16, -155497632), i, m, d[n + 10], 23, -1094730640), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 13], 4, 681279174), f, r, d[n + 0], 11, -358537222), m, f, d[n + 3], 16, -722521979), i, m, d[n + 6], 23, 76029189), r = hh(r, i = hh(i, m = hh(m, f, r, i, d[n + 9], 4, -640364487), f, r, d[n + 12], 11, -421815835), m, f, d[n + 15], 16, 530742520), i, m, d[n + 2], 23, -995338651), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 0], 6, -198630844), f, r, d[n + 7], 10, 1126891415), m, f, d[n + 14], 15, -1416354905), i, m, d[n + 5], 21, -57434055), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 12], 6, 1700485571), f, r, d[n + 3], 10, -1894986606), m, f, d[n + 10], 15, -1051523), i, m, d[n + 1], 21, -2054922799), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 8], 6, 1873313359), f, r, d[n + 15], 10, -30611744), m, f, d[n + 6], 15, -1560198380), i, m, d[n + 13], 21, 1309151649), r = ii(r, i = ii(i, m = ii(m, f, r, i, d[n + 4], 6, -145523070), f, r, d[n + 11], 10, -1120210379), m, f, d[n + 2], 15, 718787259), i, m, d[n + 9], 21, -343485551), m = add(m, h), f = add(f, t), r = add(r, g), i = add(i, e);
|
||||||
|
}
|
||||||
|
return Array(m, f, r, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cmn(d, g, m, f, r, i) {
|
||||||
|
return add(rol(add(add(g, d), add(f, i)), r), m);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ff(d, g, m, f, r, i, n) {
|
||||||
|
return cmn(g & m | ~g & f, d, g, r, i, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function gg(d, g, m, f, r, i, n) {
|
||||||
|
return cmn(g & f | m & ~f, d, g, r, i, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hh(d, g, m, f, r, i, n) {
|
||||||
|
return cmn(g ^ m ^ f, d, g, r, i, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ii(d, g, m, f, r, i, n) {
|
||||||
|
return cmn(m ^ (g | ~f), d, g, r, i, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(d, g) {
|
||||||
|
let m = (65535 & d) + (65535 & g);
|
||||||
|
return (d >> 16) + (g >> 16) + (m >> 16) << 16 | 65535 & m;
|
||||||
|
}
|
||||||
|
|
||||||
|
function rol(d, g) {
|
||||||
|
return d << g | d >>> 32 - g;
|
||||||
|
}
|
||||||
+132
@@ -0,0 +1,132 @@
|
|||||||
|
import Base64 from "../base64.js";
|
||||||
|
import { modPow, bigIntToU8Array, u8ArrayToBigInt } from "./bigint.js";
|
||||||
|
|
||||||
|
export class RSACipher {
|
||||||
|
constructor() {
|
||||||
|
this._keyLength = 0;
|
||||||
|
this._keyBytes = 0;
|
||||||
|
this._n = null;
|
||||||
|
this._e = null;
|
||||||
|
this._d = null;
|
||||||
|
this._nBigInt = null;
|
||||||
|
this._eBigInt = null;
|
||||||
|
this._dBigInt = null;
|
||||||
|
this._extractable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
get algorithm() {
|
||||||
|
return { name: "RSA-PKCS1-v1_5" };
|
||||||
|
}
|
||||||
|
|
||||||
|
_base64urlDecode(data) {
|
||||||
|
data = data.replace(/-/g, "+").replace(/_/g, "/");
|
||||||
|
data = data.padEnd(Math.ceil(data.length / 4) * 4, "=");
|
||||||
|
return Base64.decode(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
_padArray(arr, length) {
|
||||||
|
const res = new Uint8Array(length);
|
||||||
|
res.set(arr, length - arr.length);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async generateKey(algorithm, extractable, _keyUsages) {
|
||||||
|
const cipher = new RSACipher;
|
||||||
|
await cipher._generateKey(algorithm, extractable);
|
||||||
|
return { privateKey: cipher };
|
||||||
|
}
|
||||||
|
|
||||||
|
async _generateKey(algorithm, extractable) {
|
||||||
|
this._keyLength = algorithm.modulusLength;
|
||||||
|
this._keyBytes = Math.ceil(this._keyLength / 8);
|
||||||
|
const key = await window.crypto.subtle.generateKey(
|
||||||
|
{
|
||||||
|
name: "RSA-OAEP",
|
||||||
|
modulusLength: algorithm.modulusLength,
|
||||||
|
publicExponent: algorithm.publicExponent,
|
||||||
|
hash: {name: "SHA-256"},
|
||||||
|
},
|
||||||
|
true, ["encrypt", "decrypt"]);
|
||||||
|
const privateKey = await window.crypto.subtle.exportKey("jwk", key.privateKey);
|
||||||
|
this._n = this._padArray(this._base64urlDecode(privateKey.n), this._keyBytes);
|
||||||
|
this._nBigInt = u8ArrayToBigInt(this._n);
|
||||||
|
this._e = this._padArray(this._base64urlDecode(privateKey.e), this._keyBytes);
|
||||||
|
this._eBigInt = u8ArrayToBigInt(this._e);
|
||||||
|
this._d = this._padArray(this._base64urlDecode(privateKey.d), this._keyBytes);
|
||||||
|
this._dBigInt = u8ArrayToBigInt(this._d);
|
||||||
|
this._extractable = extractable;
|
||||||
|
}
|
||||||
|
|
||||||
|
static async importKey(key, _algorithm, extractable, keyUsages) {
|
||||||
|
if (keyUsages.length !== 1 || keyUsages[0] !== "encrypt") {
|
||||||
|
throw new Error("only support importing RSA public key");
|
||||||
|
}
|
||||||
|
const cipher = new RSACipher;
|
||||||
|
await cipher._importKey(key, extractable);
|
||||||
|
return cipher;
|
||||||
|
}
|
||||||
|
|
||||||
|
async _importKey(key, extractable) {
|
||||||
|
const n = key.n;
|
||||||
|
const e = key.e;
|
||||||
|
if (n.length !== e.length) {
|
||||||
|
throw new Error("the sizes of modulus and public exponent do not match");
|
||||||
|
}
|
||||||
|
this._keyBytes = n.length;
|
||||||
|
this._keyLength = this._keyBytes * 8;
|
||||||
|
this._n = new Uint8Array(this._keyBytes);
|
||||||
|
this._e = new Uint8Array(this._keyBytes);
|
||||||
|
this._n.set(n);
|
||||||
|
this._e.set(e);
|
||||||
|
this._nBigInt = u8ArrayToBigInt(this._n);
|
||||||
|
this._eBigInt = u8ArrayToBigInt(this._e);
|
||||||
|
this._extractable = extractable;
|
||||||
|
}
|
||||||
|
|
||||||
|
async encrypt(_algorithm, message) {
|
||||||
|
if (message.length > this._keyBytes - 11) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const ps = new Uint8Array(this._keyBytes - message.length - 3);
|
||||||
|
window.crypto.getRandomValues(ps);
|
||||||
|
for (let i = 0; i < ps.length; i++) {
|
||||||
|
ps[i] = Math.floor(ps[i] * 254 / 255 + 1);
|
||||||
|
}
|
||||||
|
const em = new Uint8Array(this._keyBytes);
|
||||||
|
em[1] = 0x02;
|
||||||
|
em.set(ps, 2);
|
||||||
|
em.set(message, ps.length + 3);
|
||||||
|
const emBigInt = u8ArrayToBigInt(em);
|
||||||
|
const c = modPow(emBigInt, this._eBigInt, this._nBigInt);
|
||||||
|
return bigIntToU8Array(c, this._keyBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
async decrypt(_algorithm, message) {
|
||||||
|
if (message.length !== this._keyBytes) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const msgBigInt = u8ArrayToBigInt(message);
|
||||||
|
const emBigInt = modPow(msgBigInt, this._dBigInt, this._nBigInt);
|
||||||
|
const em = bigIntToU8Array(emBigInt, this._keyBytes);
|
||||||
|
if (em[0] !== 0x00 || em[1] !== 0x02) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let i = 2;
|
||||||
|
for (; i < em.length; i++) {
|
||||||
|
if (em[i] === 0x00) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (i === em.length) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return em.slice(i + 1, em.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
async exportKey() {
|
||||||
|
if (!this._extractable) {
|
||||||
|
throw new Error("key is not extractable");
|
||||||
|
}
|
||||||
|
return { n: this._n, e: this._e, d: this._d };
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class CopyRectDecoder {
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
|
if (sock.rQwait("COPYRECT", 4)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let deltaX = sock.rQshift16();
|
||||||
|
let deltaY = sock.rQshift16();
|
||||||
|
|
||||||
|
if ((width === 0) || (height === 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.copyImage(deltaX, deltaY, x, y, width, height);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+321
@@ -0,0 +1,321 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2024 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Log from '../util/logging.js';
|
||||||
|
|
||||||
|
export class H264Parser {
|
||||||
|
constructor(data) {
|
||||||
|
this._data = data;
|
||||||
|
this._index = 0;
|
||||||
|
this.profileIdc = null;
|
||||||
|
this.constraintSet = null;
|
||||||
|
this.levelIdc = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getStartSequenceLen(index) {
|
||||||
|
let data = this._data;
|
||||||
|
if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 0 && data[index + 3] == 1) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
if (data[index + 0] == 0 && data[index + 1] == 0 && data[index + 2] == 1) {
|
||||||
|
return 3;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
_indexOfNextNalUnit(index) {
|
||||||
|
let data = this._data;
|
||||||
|
for (let i = index; i < data.length; ++i) {
|
||||||
|
if (this._getStartSequenceLen(i) != 0) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseSps(index) {
|
||||||
|
this.profileIdc = this._data[index];
|
||||||
|
this.constraintSet = this._data[index + 1];
|
||||||
|
this.levelIdc = this._data[index + 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
_parseNalUnit(index) {
|
||||||
|
const firstByte = this._data[index];
|
||||||
|
if (firstByte & 0x80) {
|
||||||
|
throw new Error('H264 parsing sanity check failed, forbidden zero bit is set');
|
||||||
|
}
|
||||||
|
const unitType = firstByte & 0x1f;
|
||||||
|
|
||||||
|
switch (unitType) {
|
||||||
|
case 1: // coded slice, non-idr
|
||||||
|
return { slice: true };
|
||||||
|
case 5: // coded slice, idr
|
||||||
|
return { slice: true, key: true };
|
||||||
|
case 6: // sei
|
||||||
|
return {};
|
||||||
|
case 7: // sps
|
||||||
|
this._parseSps(index + 1);
|
||||||
|
return {};
|
||||||
|
case 8: // pps
|
||||||
|
return {};
|
||||||
|
default:
|
||||||
|
Log.Warn("Unhandled unit type: ", unitType);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
parse() {
|
||||||
|
const startIndex = this._index;
|
||||||
|
let isKey = false;
|
||||||
|
|
||||||
|
while (this._index < this._data.length) {
|
||||||
|
const startSequenceLen = this._getStartSequenceLen(this._index);
|
||||||
|
if (startSequenceLen == 0) {
|
||||||
|
throw new Error('Invalid start sequence in bit stream');
|
||||||
|
}
|
||||||
|
|
||||||
|
const { slice, key } = this._parseNalUnit(this._index + startSequenceLen);
|
||||||
|
|
||||||
|
let nextIndex = this._indexOfNextNalUnit(this._index + startSequenceLen);
|
||||||
|
if (nextIndex == -1) {
|
||||||
|
this._index = this._data.length;
|
||||||
|
} else {
|
||||||
|
this._index = nextIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key) {
|
||||||
|
isKey = true;
|
||||||
|
}
|
||||||
|
if (slice) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startIndex === this._index) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
frame: this._data.subarray(startIndex, this._index),
|
||||||
|
key: isKey,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class H264Context {
|
||||||
|
constructor(width, height) {
|
||||||
|
this.lastUsed = 0;
|
||||||
|
this._width = width;
|
||||||
|
this._height = height;
|
||||||
|
this._profileIdc = null;
|
||||||
|
this._constraintSet = null;
|
||||||
|
this._levelIdc = null;
|
||||||
|
this._decoder = null;
|
||||||
|
this._pendingFrames = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleFrame(frame) {
|
||||||
|
let pending = this._pendingFrames.shift();
|
||||||
|
if (pending === undefined) {
|
||||||
|
throw new Error("Pending frame queue empty when receiving frame from decoder");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pending.timestamp != frame.timestamp) {
|
||||||
|
throw new Error("Video frame timestamp mismatch. Expected " +
|
||||||
|
frame.timestamp + " but but got " + pending.timestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
pending.frame = frame;
|
||||||
|
pending.ready = true;
|
||||||
|
pending.resolve();
|
||||||
|
|
||||||
|
if (!pending.keep) {
|
||||||
|
frame.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleError(e) {
|
||||||
|
throw new Error("Failed to decode frame: " + e.message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_configureDecoder(profileIdc, constraintSet, levelIdc) {
|
||||||
|
if (this._decoder === null || this._decoder.state === 'closed') {
|
||||||
|
this._decoder = new VideoDecoder({
|
||||||
|
output: frame => this._handleFrame(frame),
|
||||||
|
error: e => this._handleError(e),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const codec = 'avc1.' +
|
||||||
|
profileIdc.toString(16).padStart(2, '0') +
|
||||||
|
constraintSet.toString(16).padStart(2, '0') +
|
||||||
|
levelIdc.toString(16).padStart(2, '0');
|
||||||
|
this._decoder.configure({
|
||||||
|
codec: codec,
|
||||||
|
codedWidth: this._width,
|
||||||
|
codedHeight: this._height,
|
||||||
|
optimizeForLatency: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_preparePendingFrame(timestamp) {
|
||||||
|
let pending = {
|
||||||
|
timestamp: timestamp,
|
||||||
|
promise: null,
|
||||||
|
resolve: null,
|
||||||
|
frame: null,
|
||||||
|
ready: false,
|
||||||
|
keep: false,
|
||||||
|
};
|
||||||
|
pending.promise = new Promise((resolve) => {
|
||||||
|
pending.resolve = resolve;
|
||||||
|
});
|
||||||
|
this._pendingFrames.push(pending);
|
||||||
|
|
||||||
|
return pending;
|
||||||
|
}
|
||||||
|
|
||||||
|
decode(payload) {
|
||||||
|
let parser = new H264Parser(payload);
|
||||||
|
let result = null;
|
||||||
|
|
||||||
|
// Ideally, this timestamp should come from the server, but we'll just
|
||||||
|
// approximate it instead.
|
||||||
|
let timestamp = Math.round(window.performance.now() * 1e3);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
let encodedFrame = parser.parse();
|
||||||
|
if (encodedFrame === null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parser.profileIdc !== null) {
|
||||||
|
self._profileIdc = parser.profileIdc;
|
||||||
|
self._constraintSet = parser.constraintSet;
|
||||||
|
self._levelIdc = parser.levelIdc;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._decoder === null || this._decoder.state !== 'configured') {
|
||||||
|
if (!encodedFrame.key) {
|
||||||
|
Log.Warn("Missing key frame. Can't decode until one arrives");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (self._profileIdc === null) {
|
||||||
|
Log.Warn('Cannot config decoder. Have not received SPS and PPS yet.');
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this._configureDecoder(self._profileIdc, self._constraintSet,
|
||||||
|
self._levelIdc);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = this._preparePendingFrame(timestamp);
|
||||||
|
|
||||||
|
const chunk = new EncodedVideoChunk({
|
||||||
|
timestamp: timestamp,
|
||||||
|
type: encodedFrame.key ? 'key' : 'delta',
|
||||||
|
data: encodedFrame.frame,
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
this._decoder.decode(chunk);
|
||||||
|
} catch (e) {
|
||||||
|
Log.Warn("Failed to decode:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only keep last frame of each payload
|
||||||
|
if (result !== null) {
|
||||||
|
result.keep = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class H264Decoder {
|
||||||
|
constructor() {
|
||||||
|
this._tick = 0;
|
||||||
|
this._contexts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
_contextId(x, y, width, height) {
|
||||||
|
return [x, y, width, height].join(',');
|
||||||
|
}
|
||||||
|
|
||||||
|
_findOldestContextId() {
|
||||||
|
let oldestTick = Number.MAX_VALUE;
|
||||||
|
let oldestKey = undefined;
|
||||||
|
for (const [key, value] of Object.entries(this._contexts)) {
|
||||||
|
if (value.lastUsed < oldestTick) {
|
||||||
|
oldestTick = value.lastUsed;
|
||||||
|
oldestKey = key;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oldestKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
_createContext(x, y, width, height) {
|
||||||
|
const maxContexts = 64;
|
||||||
|
if (Object.keys(this._contexts).length >= maxContexts) {
|
||||||
|
let oldestContextId = this._findOldestContextId();
|
||||||
|
delete this._contexts[oldestContextId];
|
||||||
|
}
|
||||||
|
let context = new H264Context(width, height);
|
||||||
|
this._contexts[this._contextId(x, y, width, height)] = context;
|
||||||
|
return context;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getContext(x, y, width, height) {
|
||||||
|
let context = this._contexts[this._contextId(x, y, width, height)];
|
||||||
|
return context !== undefined ? context : this._createContext(x, y, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
_resetContext(x, y, width, height) {
|
||||||
|
delete this._contexts[this._contextId(x, y, width, height)];
|
||||||
|
}
|
||||||
|
|
||||||
|
_resetAllContexts() {
|
||||||
|
this._contexts = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
|
const resetContextFlag = 1;
|
||||||
|
const resetAllContextsFlag = 2;
|
||||||
|
|
||||||
|
if (sock.rQwait("h264 header", 8)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const length = sock.rQshift32();
|
||||||
|
const flags = sock.rQshift32();
|
||||||
|
|
||||||
|
if (sock.rQwait("h264 payload", length, 8)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flags & resetAllContextsFlag) {
|
||||||
|
this._resetAllContexts();
|
||||||
|
} else if (flags & resetContextFlag) {
|
||||||
|
this._resetContext(x, y, width, height);
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = this._getContext(x, y, width, height);
|
||||||
|
context.lastUsed = this._tick++;
|
||||||
|
|
||||||
|
if (length !== 0) {
|
||||||
|
let payload = sock.rQshiftBytes(length, false);
|
||||||
|
let frame = context.decode(payload);
|
||||||
|
if (frame !== null) {
|
||||||
|
display.videoFrame(x, y, width, height, frame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+181
@@ -0,0 +1,181 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Log from '../util/logging.js';
|
||||||
|
|
||||||
|
export default class HextileDecoder {
|
||||||
|
constructor() {
|
||||||
|
this._tiles = 0;
|
||||||
|
this._lastsubencoding = 0;
|
||||||
|
this._tileBuffer = new Uint8Array(16 * 16 * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
|
if (this._tiles === 0) {
|
||||||
|
this._tilesX = Math.ceil(width / 16);
|
||||||
|
this._tilesY = Math.ceil(height / 16);
|
||||||
|
this._totalTiles = this._tilesX * this._tilesY;
|
||||||
|
this._tiles = this._totalTiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (this._tiles > 0) {
|
||||||
|
let bytes = 1;
|
||||||
|
|
||||||
|
if (sock.rQwait("HEXTILE", bytes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let subencoding = sock.rQpeek8();
|
||||||
|
if (subencoding > 30) { // Raw
|
||||||
|
throw new Error("Illegal hextile subencoding (subencoding: " +
|
||||||
|
subencoding + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
const currTile = this._totalTiles - this._tiles;
|
||||||
|
const tileX = currTile % this._tilesX;
|
||||||
|
const tileY = Math.floor(currTile / this._tilesX);
|
||||||
|
const tx = x + tileX * 16;
|
||||||
|
const ty = y + tileY * 16;
|
||||||
|
const tw = Math.min(16, (x + width) - tx);
|
||||||
|
const th = Math.min(16, (y + height) - ty);
|
||||||
|
|
||||||
|
// Figure out how much we are expecting
|
||||||
|
if (subencoding & 0x01) { // Raw
|
||||||
|
bytes += tw * th * 4;
|
||||||
|
} else {
|
||||||
|
if (subencoding & 0x02) { // Background
|
||||||
|
bytes += 4;
|
||||||
|
}
|
||||||
|
if (subencoding & 0x04) { // Foreground
|
||||||
|
bytes += 4;
|
||||||
|
}
|
||||||
|
if (subencoding & 0x08) { // AnySubrects
|
||||||
|
bytes++; // Since we aren't shifting it off
|
||||||
|
|
||||||
|
if (sock.rQwait("HEXTILE", bytes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let subrects = sock.rQpeekBytes(bytes).at(-1);
|
||||||
|
if (subencoding & 0x10) { // SubrectsColoured
|
||||||
|
bytes += subrects * (4 + 2);
|
||||||
|
} else {
|
||||||
|
bytes += subrects * 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sock.rQwait("HEXTILE", bytes)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We know the encoding and have a whole tile
|
||||||
|
sock.rQshift8();
|
||||||
|
if (subencoding === 0) {
|
||||||
|
if (this._lastsubencoding & 0x01) {
|
||||||
|
// Weird: ignore blanks are RAW
|
||||||
|
Log.Debug(" Ignoring blank after RAW");
|
||||||
|
} else {
|
||||||
|
display.fillRect(tx, ty, tw, th, this._background);
|
||||||
|
}
|
||||||
|
} else if (subencoding & 0x01) { // Raw
|
||||||
|
let pixels = tw * th;
|
||||||
|
let data = sock.rQshiftBytes(pixels * 4, false);
|
||||||
|
// Max sure the image is fully opaque
|
||||||
|
for (let i = 0;i < pixels;i++) {
|
||||||
|
data[i * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
display.blitImage(tx, ty, tw, th, data, 0);
|
||||||
|
} else {
|
||||||
|
if (subencoding & 0x02) { // Background
|
||||||
|
this._background = new Uint8Array(sock.rQshiftBytes(4));
|
||||||
|
}
|
||||||
|
if (subencoding & 0x04) { // Foreground
|
||||||
|
this._foreground = new Uint8Array(sock.rQshiftBytes(4));
|
||||||
|
}
|
||||||
|
|
||||||
|
this._startTile(tx, ty, tw, th, this._background);
|
||||||
|
if (subencoding & 0x08) { // AnySubrects
|
||||||
|
let subrects = sock.rQshift8();
|
||||||
|
|
||||||
|
for (let s = 0; s < subrects; s++) {
|
||||||
|
let color;
|
||||||
|
if (subencoding & 0x10) { // SubrectsColoured
|
||||||
|
color = sock.rQshiftBytes(4);
|
||||||
|
} else {
|
||||||
|
color = this._foreground;
|
||||||
|
}
|
||||||
|
const xy = sock.rQshift8();
|
||||||
|
const sx = (xy >> 4);
|
||||||
|
const sy = (xy & 0x0f);
|
||||||
|
|
||||||
|
const wh = sock.rQshift8();
|
||||||
|
const sw = (wh >> 4) + 1;
|
||||||
|
const sh = (wh & 0x0f) + 1;
|
||||||
|
|
||||||
|
this._subTile(sx, sy, sw, sh, color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._finishTile(display);
|
||||||
|
}
|
||||||
|
this._lastsubencoding = subencoding;
|
||||||
|
this._tiles--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// start updating a tile
|
||||||
|
_startTile(x, y, width, height, color) {
|
||||||
|
this._tileX = x;
|
||||||
|
this._tileY = y;
|
||||||
|
this._tileW = width;
|
||||||
|
this._tileH = height;
|
||||||
|
|
||||||
|
const red = color[0];
|
||||||
|
const green = color[1];
|
||||||
|
const blue = color[2];
|
||||||
|
|
||||||
|
const data = this._tileBuffer;
|
||||||
|
for (let i = 0; i < width * height * 4; i += 4) {
|
||||||
|
data[i] = red;
|
||||||
|
data[i + 1] = green;
|
||||||
|
data[i + 2] = blue;
|
||||||
|
data[i + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// update sub-rectangle of the current tile
|
||||||
|
_subTile(x, y, w, h, color) {
|
||||||
|
const red = color[0];
|
||||||
|
const green = color[1];
|
||||||
|
const blue = color[2];
|
||||||
|
const xend = x + w;
|
||||||
|
const yend = y + h;
|
||||||
|
|
||||||
|
const data = this._tileBuffer;
|
||||||
|
const width = this._tileW;
|
||||||
|
for (let j = y; j < yend; j++) {
|
||||||
|
for (let i = x; i < xend; i++) {
|
||||||
|
const p = (i + (j * width)) * 4;
|
||||||
|
data[p] = red;
|
||||||
|
data[p + 1] = green;
|
||||||
|
data[p + 2] = blue;
|
||||||
|
data[p + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw the current tile to the screen
|
||||||
|
_finishTile(display) {
|
||||||
|
display.blitImage(this._tileX, this._tileY,
|
||||||
|
this._tileW, this._tileH,
|
||||||
|
this._tileBuffer, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class JPEGDecoder {
|
||||||
|
constructor() {
|
||||||
|
// RealVNC will reuse the quantization tables
|
||||||
|
// and Huffman tables, so we need to cache them.
|
||||||
|
this._cachedQuantTables = [];
|
||||||
|
this._cachedHuffmanTables = [];
|
||||||
|
|
||||||
|
this._segments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
|
// A rect of JPEG encodings is simply a JPEG file
|
||||||
|
while (true) {
|
||||||
|
let segment = this._readSegment(sock);
|
||||||
|
if (segment === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._segments.push(segment);
|
||||||
|
// End of image?
|
||||||
|
if (segment[1] === 0xD9) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let huffmanTables = [];
|
||||||
|
let quantTables = [];
|
||||||
|
for (let segment of this._segments) {
|
||||||
|
let type = segment[1];
|
||||||
|
if (type === 0xC4) {
|
||||||
|
// Huffman tables
|
||||||
|
huffmanTables.push(segment);
|
||||||
|
} else if (type === 0xDB) {
|
||||||
|
// Quantization tables
|
||||||
|
quantTables.push(segment);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sofIndex = this._segments.findIndex(
|
||||||
|
x => x[1] == 0xC0 || x[1] == 0xC2
|
||||||
|
);
|
||||||
|
if (sofIndex == -1) {
|
||||||
|
throw new Error("Illegal JPEG image without SOF");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (quantTables.length === 0) {
|
||||||
|
this._segments.splice(sofIndex+1, 0,
|
||||||
|
...this._cachedQuantTables);
|
||||||
|
}
|
||||||
|
if (huffmanTables.length === 0) {
|
||||||
|
this._segments.splice(sofIndex+1, 0,
|
||||||
|
...this._cachedHuffmanTables);
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = 0;
|
||||||
|
for (let segment of this._segments) {
|
||||||
|
length += segment.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = new Uint8Array(length);
|
||||||
|
length = 0;
|
||||||
|
for (let segment of this._segments) {
|
||||||
|
data.set(segment, length);
|
||||||
|
length += segment.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.imageRect(x, y, width, height, "image/jpeg", data);
|
||||||
|
|
||||||
|
if (huffmanTables.length !== 0) {
|
||||||
|
this._cachedHuffmanTables = huffmanTables;
|
||||||
|
}
|
||||||
|
if (quantTables.length !== 0) {
|
||||||
|
this._cachedQuantTables = quantTables;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._segments = [];
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_readSegment(sock) {
|
||||||
|
if (sock.rQwait("JPEG", 2)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let marker = sock.rQshift8();
|
||||||
|
if (marker != 0xFF) {
|
||||||
|
throw new Error("Illegal JPEG marker received (byte: " +
|
||||||
|
marker + ")");
|
||||||
|
}
|
||||||
|
let type = sock.rQshift8();
|
||||||
|
if (type >= 0xD0 && type <= 0xD9 || type == 0x01) {
|
||||||
|
// No length after marker
|
||||||
|
return new Uint8Array([marker, type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sock.rQwait("JPEG", 2, 2)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let length = sock.rQshift16();
|
||||||
|
if (length < 2) {
|
||||||
|
throw new Error("Illegal JPEG length received (length: " +
|
||||||
|
length + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sock.rQwait("JPEG", length-2, 4)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let extra = 0;
|
||||||
|
if (type === 0xDA) {
|
||||||
|
// start of scan
|
||||||
|
extra += 2;
|
||||||
|
while (true) {
|
||||||
|
if (sock.rQwait("JPEG", length-2+extra, 4)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let data = sock.rQpeekBytes(length-2+extra, false);
|
||||||
|
if (data.at(-2) === 0xFF && data.at(-1) !== 0x00 &&
|
||||||
|
!(data.at(-1) >= 0xD0 && data.at(-1) <= 0xD7)) {
|
||||||
|
extra -= 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
extra++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let segment = new Uint8Array(2 + length + extra);
|
||||||
|
segment[0] = marker;
|
||||||
|
segment[1] = type;
|
||||||
|
segment[2] = length >> 8;
|
||||||
|
segment[3] = length;
|
||||||
|
segment.set(sock.rQshiftBytes(length-2+extra, false), 4);
|
||||||
|
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class RawDecoder {
|
||||||
|
constructor() {
|
||||||
|
this._lines = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
|
if ((width === 0) || (height === 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._lines === 0) {
|
||||||
|
this._lines = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pixelSize = depth == 8 ? 1 : 4;
|
||||||
|
const bytesPerLine = width * pixelSize;
|
||||||
|
|
||||||
|
while (this._lines > 0) {
|
||||||
|
if (sock.rQwait("RAW", bytesPerLine)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const curY = y + (height - this._lines);
|
||||||
|
|
||||||
|
let data = sock.rQshiftBytes(bytesPerLine, false);
|
||||||
|
|
||||||
|
// Convert data if needed
|
||||||
|
if (depth == 8) {
|
||||||
|
const newdata = new Uint8Array(width * 4);
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
newdata[i * 4 + 0] = ((data[i] >> 0) & 0x3) * 255 / 3;
|
||||||
|
newdata[i * 4 + 1] = ((data[i] >> 2) & 0x3) * 255 / 3;
|
||||||
|
newdata[i * 4 + 2] = ((data[i] >> 4) & 0x3) * 255 / 3;
|
||||||
|
newdata[i * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
data = newdata;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Max sure the image is fully opaque
|
||||||
|
for (let i = 0; i < width; i++) {
|
||||||
|
data[i * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.blitImage(x, curY, width, 1, data, 0);
|
||||||
|
this._lines--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class RREDecoder {
|
||||||
|
constructor() {
|
||||||
|
this._subrects = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
|
if (this._subrects === 0) {
|
||||||
|
if (sock.rQwait("RRE", 4 + 4)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._subrects = sock.rQshift32();
|
||||||
|
|
||||||
|
let color = sock.rQshiftBytes(4); // Background
|
||||||
|
display.fillRect(x, y, width, height, color);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (this._subrects > 0) {
|
||||||
|
if (sock.rQwait("RRE", 4 + 8)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let color = sock.rQshiftBytes(4);
|
||||||
|
let sx = sock.rQshift16();
|
||||||
|
let sy = sock.rQshift16();
|
||||||
|
let swidth = sock.rQshift16();
|
||||||
|
let sheight = sock.rQshift16();
|
||||||
|
display.fillRect(x + sx, y + sy, swidth, sheight, color);
|
||||||
|
|
||||||
|
this._subrects--;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+393
@@ -0,0 +1,393 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* (c) 2012 Michael Tinglof, Joe Balaz, Les Piech (Mercuri.ca)
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Log from '../util/logging.js';
|
||||||
|
import Inflator from "../inflator.js";
|
||||||
|
|
||||||
|
export default class TightDecoder {
|
||||||
|
constructor() {
|
||||||
|
this._ctl = null;
|
||||||
|
this._filter = null;
|
||||||
|
this._numColors = 0;
|
||||||
|
this._palette = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel)
|
||||||
|
this._len = 0;
|
||||||
|
|
||||||
|
this._zlibs = [];
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
this._zlibs[i] = new Inflator();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
|
if (this._ctl === null) {
|
||||||
|
if (sock.rQwait("TIGHT compression-control", 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._ctl = sock.rQshift8();
|
||||||
|
|
||||||
|
// Reset streams if the server requests it
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
if ((this._ctl >> i) & 1) {
|
||||||
|
this._zlibs[i].reset();
|
||||||
|
Log.Info("Reset zlib stream " + i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Figure out filter
|
||||||
|
this._ctl = this._ctl >> 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ret;
|
||||||
|
|
||||||
|
if (this._ctl === 0x08) {
|
||||||
|
ret = this._fillRect(x, y, width, height,
|
||||||
|
sock, display, depth);
|
||||||
|
} else if (this._ctl === 0x09) {
|
||||||
|
ret = this._jpegRect(x, y, width, height,
|
||||||
|
sock, display, depth);
|
||||||
|
} else if (this._ctl === 0x0A) {
|
||||||
|
ret = this._pngRect(x, y, width, height,
|
||||||
|
sock, display, depth);
|
||||||
|
} else if ((this._ctl & 0x08) == 0) {
|
||||||
|
ret = this._basicRect(this._ctl, x, y, width, height,
|
||||||
|
sock, display, depth);
|
||||||
|
} else {
|
||||||
|
throw new Error("Illegal tight compression received (ctl: " +
|
||||||
|
this._ctl + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret) {
|
||||||
|
this._ctl = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
_fillRect(x, y, width, height, sock, display, depth) {
|
||||||
|
if (sock.rQwait("TIGHT", 3)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pixel = sock.rQshiftBytes(3);
|
||||||
|
display.fillRect(x, y, width, height, pixel, false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_jpegRect(x, y, width, height, sock, display, depth) {
|
||||||
|
let data = this._readData(sock);
|
||||||
|
if (data === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.imageRect(x, y, width, height, "image/jpeg", data);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pngRect(x, y, width, height, sock, display, depth) {
|
||||||
|
throw new Error("PNG received in standard Tight rect");
|
||||||
|
}
|
||||||
|
|
||||||
|
_basicRect(ctl, x, y, width, height, sock, display, depth) {
|
||||||
|
if (this._filter === null) {
|
||||||
|
if (ctl & 0x4) {
|
||||||
|
if (sock.rQwait("TIGHT", 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._filter = sock.rQshift8();
|
||||||
|
} else {
|
||||||
|
// Implicit CopyFilter
|
||||||
|
this._filter = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let streamId = ctl & 0x3;
|
||||||
|
|
||||||
|
let ret;
|
||||||
|
|
||||||
|
switch (this._filter) {
|
||||||
|
case 0: // CopyFilter
|
||||||
|
ret = this._copyFilter(streamId, x, y, width, height,
|
||||||
|
sock, display, depth);
|
||||||
|
break;
|
||||||
|
case 1: // PaletteFilter
|
||||||
|
ret = this._paletteFilter(streamId, x, y, width, height,
|
||||||
|
sock, display, depth);
|
||||||
|
break;
|
||||||
|
case 2: // GradientFilter
|
||||||
|
ret = this._gradientFilter(streamId, x, y, width, height,
|
||||||
|
sock, display, depth);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Illegal tight filter received (ctl: " +
|
||||||
|
this._filter + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ret) {
|
||||||
|
this._filter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
_copyFilter(streamId, x, y, width, height, sock, display, depth) {
|
||||||
|
const uncompressedSize = width * height * 3;
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (uncompressedSize === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uncompressedSize < 12) {
|
||||||
|
if (sock.rQwait("TIGHT", uncompressedSize)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = sock.rQshiftBytes(uncompressedSize);
|
||||||
|
} else {
|
||||||
|
data = this._readData(sock);
|
||||||
|
if (data === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._zlibs[streamId].setInput(data);
|
||||||
|
data = this._zlibs[streamId].inflate(uncompressedSize);
|
||||||
|
this._zlibs[streamId].setInput(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rgbx = new Uint8Array(width * height * 4);
|
||||||
|
for (let i = 0, j = 0; i < width * height * 4; i += 4, j += 3) {
|
||||||
|
rgbx[i] = data[j];
|
||||||
|
rgbx[i + 1] = data[j + 1];
|
||||||
|
rgbx[i + 2] = data[j + 2];
|
||||||
|
rgbx[i + 3] = 255; // Alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
display.blitImage(x, y, width, height, rgbx, 0, false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_paletteFilter(streamId, x, y, width, height, sock, display, depth) {
|
||||||
|
if (this._numColors === 0) {
|
||||||
|
if (sock.rQwait("TIGHT palette", 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const numColors = sock.rQpeek8() + 1;
|
||||||
|
const paletteSize = numColors * 3;
|
||||||
|
|
||||||
|
if (sock.rQwait("TIGHT palette", 1 + paletteSize)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._numColors = numColors;
|
||||||
|
sock.rQskipBytes(1);
|
||||||
|
|
||||||
|
sock.rQshiftTo(this._palette, paletteSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bpp = (this._numColors <= 2) ? 1 : 8;
|
||||||
|
const rowSize = Math.floor((width * bpp + 7) / 8);
|
||||||
|
const uncompressedSize = rowSize * height;
|
||||||
|
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (uncompressedSize === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uncompressedSize < 12) {
|
||||||
|
if (sock.rQwait("TIGHT", uncompressedSize)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = sock.rQshiftBytes(uncompressedSize);
|
||||||
|
} else {
|
||||||
|
data = this._readData(sock);
|
||||||
|
if (data === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._zlibs[streamId].setInput(data);
|
||||||
|
data = this._zlibs[streamId].inflate(uncompressedSize);
|
||||||
|
this._zlibs[streamId].setInput(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert indexed (palette based) image data to RGB
|
||||||
|
if (this._numColors == 2) {
|
||||||
|
this._monoRect(x, y, width, height, data, this._palette, display);
|
||||||
|
} else {
|
||||||
|
this._paletteRect(x, y, width, height, data, this._palette, display);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._numColors = 0;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_monoRect(x, y, width, height, data, palette, display) {
|
||||||
|
// Convert indexed (palette based) image data to RGB
|
||||||
|
// TODO: reduce number of calculations inside loop
|
||||||
|
const dest = this._getScratchBuffer(width * height * 4);
|
||||||
|
const w = Math.floor((width + 7) / 8);
|
||||||
|
const w1 = Math.floor(width / 8);
|
||||||
|
|
||||||
|
for (let y = 0; y < height; y++) {
|
||||||
|
let dp, sp, x;
|
||||||
|
for (x = 0; x < w1; x++) {
|
||||||
|
for (let b = 7; b >= 0; b--) {
|
||||||
|
dp = (y * width + x * 8 + 7 - b) * 4;
|
||||||
|
sp = (data[y * w + x] >> b & 1) * 3;
|
||||||
|
dest[dp] = palette[sp];
|
||||||
|
dest[dp + 1] = palette[sp + 1];
|
||||||
|
dest[dp + 2] = palette[sp + 2];
|
||||||
|
dest[dp + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let b = 7; b >= 8 - width % 8; b--) {
|
||||||
|
dp = (y * width + x * 8 + 7 - b) * 4;
|
||||||
|
sp = (data[y * w + x] >> b & 1) * 3;
|
||||||
|
dest[dp] = palette[sp];
|
||||||
|
dest[dp + 1] = palette[sp + 1];
|
||||||
|
dest[dp + 2] = palette[sp + 2];
|
||||||
|
dest[dp + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display.blitImage(x, y, width, height, dest, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_paletteRect(x, y, width, height, data, palette, display) {
|
||||||
|
// Convert indexed (palette based) image data to RGB
|
||||||
|
const dest = this._getScratchBuffer(width * height * 4);
|
||||||
|
const total = width * height * 4;
|
||||||
|
for (let i = 0, j = 0; i < total; i += 4, j++) {
|
||||||
|
const sp = data[j] * 3;
|
||||||
|
dest[i] = palette[sp];
|
||||||
|
dest[i + 1] = palette[sp + 1];
|
||||||
|
dest[i + 2] = palette[sp + 2];
|
||||||
|
dest[i + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.blitImage(x, y, width, height, dest, 0, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
_gradientFilter(streamId, x, y, width, height, sock, display, depth) {
|
||||||
|
// assume the TPIXEL is 3 bytes long
|
||||||
|
const uncompressedSize = width * height * 3;
|
||||||
|
let data;
|
||||||
|
|
||||||
|
if (uncompressedSize === 0) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uncompressedSize < 12) {
|
||||||
|
if (sock.rQwait("TIGHT", uncompressedSize)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
data = sock.rQshiftBytes(uncompressedSize);
|
||||||
|
} else {
|
||||||
|
data = this._readData(sock);
|
||||||
|
if (data === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._zlibs[streamId].setInput(data);
|
||||||
|
data = this._zlibs[streamId].inflate(uncompressedSize);
|
||||||
|
this._zlibs[streamId].setInput(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
let rgbx = new Uint8Array(4 * width * height);
|
||||||
|
|
||||||
|
let rgbxIndex = 0, dataIndex = 0;
|
||||||
|
let left = new Uint8Array(3);
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
const prediction = left[c];
|
||||||
|
const value = data[dataIndex++] + prediction;
|
||||||
|
rgbx[rgbxIndex++] = value;
|
||||||
|
left[c] = value;
|
||||||
|
}
|
||||||
|
rgbx[rgbxIndex++] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
let upperIndex = 0;
|
||||||
|
let upper = new Uint8Array(3),
|
||||||
|
upperleft = new Uint8Array(3);
|
||||||
|
for (let y = 1; y < height; y++) {
|
||||||
|
left.fill(0);
|
||||||
|
upperleft.fill(0);
|
||||||
|
for (let x = 0; x < width; x++) {
|
||||||
|
for (let c = 0; c < 3; c++) {
|
||||||
|
upper[c] = rgbx[upperIndex++];
|
||||||
|
let prediction = left[c] + upper[c] - upperleft[c];
|
||||||
|
if (prediction < 0) {
|
||||||
|
prediction = 0;
|
||||||
|
} else if (prediction > 255) {
|
||||||
|
prediction = 255;
|
||||||
|
}
|
||||||
|
const value = data[dataIndex++] + prediction;
|
||||||
|
rgbx[rgbxIndex++] = value;
|
||||||
|
upperleft[c] = upper[c];
|
||||||
|
left[c] = value;
|
||||||
|
}
|
||||||
|
rgbx[rgbxIndex++] = 255;
|
||||||
|
upperIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
display.blitImage(x, y, width, height, rgbx, 0, false);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_readData(sock) {
|
||||||
|
if (this._len === 0) {
|
||||||
|
if (sock.rQwait("TIGHT", 3)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let byte;
|
||||||
|
|
||||||
|
byte = sock.rQshift8();
|
||||||
|
this._len = byte & 0x7f;
|
||||||
|
if (byte & 0x80) {
|
||||||
|
byte = sock.rQshift8();
|
||||||
|
this._len |= (byte & 0x7f) << 7;
|
||||||
|
if (byte & 0x80) {
|
||||||
|
byte = sock.rQshift8();
|
||||||
|
this._len |= byte << 14;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sock.rQwait("TIGHT", this._len)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = sock.rQshiftBytes(this._len, false);
|
||||||
|
this._len = 0;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getScratchBuffer(size) {
|
||||||
|
if (!this._scratchBuffer || (this._scratchBuffer.length < size)) {
|
||||||
|
this._scratchBuffer = new Uint8Array(size);
|
||||||
|
}
|
||||||
|
return this._scratchBuffer;
|
||||||
|
}
|
||||||
|
}
|
||||||
+27
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import TightDecoder from './tight.js';
|
||||||
|
|
||||||
|
export default class TightPNGDecoder extends TightDecoder {
|
||||||
|
_pngRect(x, y, width, height, sock, display, depth) {
|
||||||
|
let data = this._readData(sock);
|
||||||
|
if (data === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.imageRect(x, y, width, height, "image/png", data);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_basicRect(ctl, x, y, width, height, sock, display, depth) {
|
||||||
|
throw new Error("BasicCompression received in TightPNG rect");
|
||||||
|
}
|
||||||
|
}
|
||||||
+51
@@ -0,0 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2024 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Inflator from "../inflator.js";
|
||||||
|
|
||||||
|
export default class ZlibDecoder {
|
||||||
|
constructor() {
|
||||||
|
this._zlib = new Inflator();
|
||||||
|
this._length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
|
if ((width === 0) || (height === 0)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._length === 0) {
|
||||||
|
if (sock.rQwait("ZLIB", 4)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._length = sock.rQshift32();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sock.rQwait("ZLIB", this._length)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
let data = new Uint8Array(sock.rQshiftBytes(this._length, false));
|
||||||
|
this._length = 0;
|
||||||
|
|
||||||
|
this._zlib.setInput(data);
|
||||||
|
data = this._zlib.inflate(width * height * 4);
|
||||||
|
this._zlib.setInput(null);
|
||||||
|
|
||||||
|
// Max sure the image is fully opaque
|
||||||
|
for (let i = 0; i < width * height; i++) {
|
||||||
|
data[i * 4 + 3] = 255;
|
||||||
|
}
|
||||||
|
|
||||||
|
display.blitImage(x, y, width, height, data, 0);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
+185
@@ -0,0 +1,185 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2021 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import Inflate from "../inflator.js";
|
||||||
|
|
||||||
|
const ZRLE_TILE_WIDTH = 64;
|
||||||
|
const ZRLE_TILE_HEIGHT = 64;
|
||||||
|
|
||||||
|
export default class ZRLEDecoder {
|
||||||
|
constructor() {
|
||||||
|
this._length = 0;
|
||||||
|
this._inflator = new Inflate();
|
||||||
|
|
||||||
|
this._pixelBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
|
||||||
|
this._tileBuffer = new Uint8Array(ZRLE_TILE_WIDTH * ZRLE_TILE_HEIGHT * 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
decodeRect(x, y, width, height, sock, display, depth) {
|
||||||
|
if (this._length === 0) {
|
||||||
|
if (sock.rQwait("ZLib data length", 4)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this._length = sock.rQshift32();
|
||||||
|
}
|
||||||
|
if (sock.rQwait("Zlib data", this._length)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = sock.rQshiftBytes(this._length, false);
|
||||||
|
|
||||||
|
this._inflator.setInput(data);
|
||||||
|
|
||||||
|
for (let ty = y; ty < y + height; ty += ZRLE_TILE_HEIGHT) {
|
||||||
|
let th = Math.min(ZRLE_TILE_HEIGHT, y + height - ty);
|
||||||
|
|
||||||
|
for (let tx = x; tx < x + width; tx += ZRLE_TILE_WIDTH) {
|
||||||
|
let tw = Math.min(ZRLE_TILE_WIDTH, x + width - tx);
|
||||||
|
|
||||||
|
const tileSize = tw * th;
|
||||||
|
const subencoding = this._inflator.inflate(1)[0];
|
||||||
|
if (subencoding === 0) {
|
||||||
|
// raw data
|
||||||
|
const data = this._readPixels(tileSize);
|
||||||
|
display.blitImage(tx, ty, tw, th, data, 0, false);
|
||||||
|
} else if (subencoding === 1) {
|
||||||
|
// solid
|
||||||
|
const background = this._readPixels(1);
|
||||||
|
display.fillRect(tx, ty, tw, th, [background[0], background[1], background[2]]);
|
||||||
|
} else if (subencoding >= 2 && subencoding <= 16) {
|
||||||
|
const data = this._decodePaletteTile(subencoding, tileSize, tw, th);
|
||||||
|
display.blitImage(tx, ty, tw, th, data, 0, false);
|
||||||
|
} else if (subencoding === 128) {
|
||||||
|
const data = this._decodeRLETile(tileSize);
|
||||||
|
display.blitImage(tx, ty, tw, th, data, 0, false);
|
||||||
|
} else if (subencoding >= 130 && subencoding <= 255) {
|
||||||
|
const data = this._decodeRLEPaletteTile(subencoding - 128, tileSize);
|
||||||
|
display.blitImage(tx, ty, tw, th, data, 0, false);
|
||||||
|
} else {
|
||||||
|
throw new Error('Unknown subencoding: ' + subencoding);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._length = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_getBitsPerPixelInPalette(paletteSize) {
|
||||||
|
if (paletteSize <= 2) {
|
||||||
|
return 1;
|
||||||
|
} else if (paletteSize <= 4) {
|
||||||
|
return 2;
|
||||||
|
} else if (paletteSize <= 16) {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_readPixels(pixels) {
|
||||||
|
let data = this._pixelBuffer;
|
||||||
|
const buffer = this._inflator.inflate(3*pixels);
|
||||||
|
for (let i = 0, j = 0; i < pixels*4; i += 4, j += 3) {
|
||||||
|
data[i] = buffer[j];
|
||||||
|
data[i + 1] = buffer[j + 1];
|
||||||
|
data[i + 2] = buffer[j + 2];
|
||||||
|
data[i + 3] = 255; // Add the Alpha
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
_decodePaletteTile(paletteSize, tileSize, tilew, tileh) {
|
||||||
|
const data = this._tileBuffer;
|
||||||
|
const palette = this._readPixels(paletteSize);
|
||||||
|
const bitsPerPixel = this._getBitsPerPixelInPalette(paletteSize);
|
||||||
|
const mask = (1 << bitsPerPixel) - 1;
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
let encoded = this._inflator.inflate(1)[0];
|
||||||
|
|
||||||
|
for (let y=0; y<tileh; y++) {
|
||||||
|
let shift = 8-bitsPerPixel;
|
||||||
|
for (let x=0; x<tilew; x++) {
|
||||||
|
if (shift<0) {
|
||||||
|
shift=8-bitsPerPixel;
|
||||||
|
encoded = this._inflator.inflate(1)[0];
|
||||||
|
}
|
||||||
|
let indexInPalette = (encoded>>shift) & mask;
|
||||||
|
|
||||||
|
data[offset] = palette[indexInPalette * 4];
|
||||||
|
data[offset + 1] = palette[indexInPalette * 4 + 1];
|
||||||
|
data[offset + 2] = palette[indexInPalette * 4 + 2];
|
||||||
|
data[offset + 3] = palette[indexInPalette * 4 + 3];
|
||||||
|
offset += 4;
|
||||||
|
shift-=bitsPerPixel;
|
||||||
|
}
|
||||||
|
if (shift<8-bitsPerPixel && y<tileh-1) {
|
||||||
|
encoded = this._inflator.inflate(1)[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
_decodeRLETile(tileSize) {
|
||||||
|
const data = this._tileBuffer;
|
||||||
|
let i = 0;
|
||||||
|
while (i < tileSize) {
|
||||||
|
const pixel = this._readPixels(1);
|
||||||
|
const length = this._readRLELength();
|
||||||
|
for (let j = 0; j < length; j++) {
|
||||||
|
data[i * 4] = pixel[0];
|
||||||
|
data[i * 4 + 1] = pixel[1];
|
||||||
|
data[i * 4 + 2] = pixel[2];
|
||||||
|
data[i * 4 + 3] = pixel[3];
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
_decodeRLEPaletteTile(paletteSize, tileSize) {
|
||||||
|
const data = this._tileBuffer;
|
||||||
|
|
||||||
|
// palette
|
||||||
|
const palette = this._readPixels(paletteSize);
|
||||||
|
|
||||||
|
let offset = 0;
|
||||||
|
while (offset < tileSize) {
|
||||||
|
let indexInPalette = this._inflator.inflate(1)[0];
|
||||||
|
let length = 1;
|
||||||
|
if (indexInPalette >= 128) {
|
||||||
|
indexInPalette -= 128;
|
||||||
|
length = this._readRLELength();
|
||||||
|
}
|
||||||
|
if (indexInPalette > paletteSize) {
|
||||||
|
throw new Error('Too big index in palette: ' + indexInPalette + ', palette size: ' + paletteSize);
|
||||||
|
}
|
||||||
|
if (offset + length > tileSize) {
|
||||||
|
throw new Error('Too big rle length in palette mode: ' + length + ', allowed length is: ' + (tileSize - offset));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let j = 0; j < length; j++) {
|
||||||
|
data[offset * 4] = palette[indexInPalette * 4];
|
||||||
|
data[offset * 4 + 1] = palette[indexInPalette * 4 + 1];
|
||||||
|
data[offset * 4 + 2] = palette[indexInPalette * 4 + 2];
|
||||||
|
data[offset * 4 + 3] = palette[indexInPalette * 4 + 3];
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
_readRLELength() {
|
||||||
|
let length = 0;
|
||||||
|
let current = 0;
|
||||||
|
do {
|
||||||
|
current = this._inflator.inflate(1)[0];
|
||||||
|
length += current;
|
||||||
|
} while (current === 255);
|
||||||
|
return length + 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
+84
@@ -0,0 +1,84 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2020 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { deflateInit, deflate } from "../vendor/pako/lib/zlib/deflate.js";
|
||||||
|
import { Z_FULL_FLUSH, Z_DEFAULT_COMPRESSION } from "../vendor/pako/lib/zlib/deflate.js";
|
||||||
|
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
|
||||||
|
|
||||||
|
export default class Deflator {
|
||||||
|
constructor() {
|
||||||
|
this.strm = new ZStream();
|
||||||
|
this.chunkSize = 1024 * 10 * 10;
|
||||||
|
this.outputBuffer = new Uint8Array(this.chunkSize);
|
||||||
|
|
||||||
|
deflateInit(this.strm, Z_DEFAULT_COMPRESSION);
|
||||||
|
}
|
||||||
|
|
||||||
|
deflate(inData) {
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
this.strm.input = inData;
|
||||||
|
this.strm.avail_in = this.strm.input.length;
|
||||||
|
this.strm.next_in = 0;
|
||||||
|
this.strm.output = this.outputBuffer;
|
||||||
|
this.strm.avail_out = this.chunkSize;
|
||||||
|
this.strm.next_out = 0;
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
let lastRet = deflate(this.strm, Z_FULL_FLUSH);
|
||||||
|
let outData = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
|
||||||
|
|
||||||
|
if (lastRet < 0) {
|
||||||
|
throw new Error("zlib deflate failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.strm.avail_in > 0) {
|
||||||
|
// Read chunks until done
|
||||||
|
|
||||||
|
let chunks = [outData];
|
||||||
|
let totalLen = outData.length;
|
||||||
|
do {
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
this.strm.output = new Uint8Array(this.chunkSize);
|
||||||
|
this.strm.next_out = 0;
|
||||||
|
this.strm.avail_out = this.chunkSize;
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
lastRet = deflate(this.strm, Z_FULL_FLUSH);
|
||||||
|
|
||||||
|
if (lastRet < 0) {
|
||||||
|
throw new Error("zlib deflate failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
let chunk = new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
|
||||||
|
totalLen += chunk.length;
|
||||||
|
chunks.push(chunk);
|
||||||
|
} while (this.strm.avail_in > 0);
|
||||||
|
|
||||||
|
// Combine chunks into a single data
|
||||||
|
|
||||||
|
let newData = new Uint8Array(totalLen);
|
||||||
|
let offset = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < chunks.length; i++) {
|
||||||
|
newData.set(chunks[i], offset);
|
||||||
|
offset += chunks[i].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
outData = newData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
this.strm.input = null;
|
||||||
|
this.strm.avail_in = 0;
|
||||||
|
this.strm.next_in = 0;
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
return outData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+575
@@ -0,0 +1,575 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Log from './util/logging.js';
|
||||||
|
import Base64 from "./base64.js";
|
||||||
|
import { toSigned32bit } from './util/int.js';
|
||||||
|
|
||||||
|
export default class Display {
|
||||||
|
constructor(target) {
|
||||||
|
this._drawCtx = null;
|
||||||
|
|
||||||
|
this._renderQ = []; // queue drawing actions for in-oder rendering
|
||||||
|
this._flushPromise = null;
|
||||||
|
|
||||||
|
// the full frame buffer (logical canvas) size
|
||||||
|
this._fbWidth = 0;
|
||||||
|
this._fbHeight = 0;
|
||||||
|
|
||||||
|
this._prevDrawStyle = "";
|
||||||
|
|
||||||
|
Log.Debug(">> Display.constructor");
|
||||||
|
|
||||||
|
// The visible canvas
|
||||||
|
this._target = target;
|
||||||
|
|
||||||
|
if (!this._target) {
|
||||||
|
throw new Error("Target must be set");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof this._target === 'string') {
|
||||||
|
throw new Error('target must be a DOM element');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._target.getContext) {
|
||||||
|
throw new Error("no getContext method");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._targetCtx = this._target.getContext('2d');
|
||||||
|
|
||||||
|
// the visible canvas viewport (i.e. what actually gets seen)
|
||||||
|
this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height };
|
||||||
|
|
||||||
|
// The hidden canvas, where we do the actual rendering
|
||||||
|
this._backbuffer = document.createElement('canvas');
|
||||||
|
this._drawCtx = this._backbuffer.getContext('2d');
|
||||||
|
|
||||||
|
this._damageBounds = { left: 0, top: 0,
|
||||||
|
right: this._backbuffer.width,
|
||||||
|
bottom: this._backbuffer.height };
|
||||||
|
|
||||||
|
Log.Debug("User Agent: " + navigator.userAgent);
|
||||||
|
|
||||||
|
Log.Debug("<< Display.constructor");
|
||||||
|
|
||||||
|
// ===== PROPERTIES =====
|
||||||
|
|
||||||
|
this._scale = 1.0;
|
||||||
|
this._clipViewport = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== PROPERTIES =====
|
||||||
|
|
||||||
|
get scale() { return this._scale; }
|
||||||
|
set scale(scale) {
|
||||||
|
this._rescale(scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
get clipViewport() { return this._clipViewport; }
|
||||||
|
set clipViewport(viewport) {
|
||||||
|
this._clipViewport = viewport;
|
||||||
|
// May need to readjust the viewport dimensions
|
||||||
|
const vp = this._viewportLoc;
|
||||||
|
this.viewportChangeSize(vp.w, vp.h);
|
||||||
|
this.viewportChangePos(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
get width() {
|
||||||
|
return this._fbWidth;
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
return this._fbHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== PUBLIC METHODS =====
|
||||||
|
|
||||||
|
viewportChangePos(deltaX, deltaY) {
|
||||||
|
const vp = this._viewportLoc;
|
||||||
|
deltaX = Math.floor(deltaX);
|
||||||
|
deltaY = Math.floor(deltaY);
|
||||||
|
|
||||||
|
if (!this._clipViewport) {
|
||||||
|
deltaX = -vp.w; // clamped later of out of bounds
|
||||||
|
deltaY = -vp.h;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vx2 = vp.x + vp.w - 1;
|
||||||
|
const vy2 = vp.y + vp.h - 1;
|
||||||
|
|
||||||
|
// Position change
|
||||||
|
|
||||||
|
if (deltaX < 0 && vp.x + deltaX < 0) {
|
||||||
|
deltaX = -vp.x;
|
||||||
|
}
|
||||||
|
if (vx2 + deltaX >= this._fbWidth) {
|
||||||
|
deltaX -= vx2 + deltaX - this._fbWidth + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (vp.y + deltaY < 0) {
|
||||||
|
deltaY = -vp.y;
|
||||||
|
}
|
||||||
|
if (vy2 + deltaY >= this._fbHeight) {
|
||||||
|
deltaY -= (vy2 + deltaY - this._fbHeight + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deltaX === 0 && deltaY === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY);
|
||||||
|
|
||||||
|
vp.x += deltaX;
|
||||||
|
vp.y += deltaY;
|
||||||
|
|
||||||
|
this._damage(vp.x, vp.y, vp.w, vp.h);
|
||||||
|
|
||||||
|
this.flip();
|
||||||
|
}
|
||||||
|
|
||||||
|
viewportChangeSize(width, height) {
|
||||||
|
|
||||||
|
if (!this._clipViewport ||
|
||||||
|
typeof(width) === "undefined" ||
|
||||||
|
typeof(height) === "undefined") {
|
||||||
|
|
||||||
|
Log.Debug("Setting viewport to full display region");
|
||||||
|
width = this._fbWidth;
|
||||||
|
height = this._fbHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
width = Math.floor(width);
|
||||||
|
height = Math.floor(height);
|
||||||
|
|
||||||
|
if (width > this._fbWidth) {
|
||||||
|
width = this._fbWidth;
|
||||||
|
}
|
||||||
|
if (height > this._fbHeight) {
|
||||||
|
height = this._fbHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
const vp = this._viewportLoc;
|
||||||
|
if (vp.w !== width || vp.h !== height) {
|
||||||
|
vp.w = width;
|
||||||
|
vp.h = height;
|
||||||
|
|
||||||
|
const canvas = this._target;
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
|
||||||
|
// The position might need to be updated if we've grown
|
||||||
|
this.viewportChangePos(0, 0);
|
||||||
|
|
||||||
|
this._damage(vp.x, vp.y, vp.w, vp.h);
|
||||||
|
this.flip();
|
||||||
|
|
||||||
|
// Update the visible size of the target canvas
|
||||||
|
this._rescale(this._scale);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
absX(x) {
|
||||||
|
if (this._scale === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return toSigned32bit(x / this._scale + this._viewportLoc.x);
|
||||||
|
}
|
||||||
|
|
||||||
|
absY(y) {
|
||||||
|
if (this._scale === 0) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return toSigned32bit(y / this._scale + this._viewportLoc.y);
|
||||||
|
}
|
||||||
|
|
||||||
|
resize(width, height) {
|
||||||
|
this._prevDrawStyle = "";
|
||||||
|
|
||||||
|
this._fbWidth = width;
|
||||||
|
this._fbHeight = height;
|
||||||
|
|
||||||
|
const canvas = this._backbuffer;
|
||||||
|
if (canvas.width !== width || canvas.height !== height) {
|
||||||
|
|
||||||
|
// We have to save the canvas data since changing the size will clear it
|
||||||
|
let saveImg = null;
|
||||||
|
if (canvas.width > 0 && canvas.height > 0) {
|
||||||
|
saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvas.width !== width) {
|
||||||
|
canvas.width = width;
|
||||||
|
}
|
||||||
|
if (canvas.height !== height) {
|
||||||
|
canvas.height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (saveImg) {
|
||||||
|
this._drawCtx.putImageData(saveImg, 0, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Readjust the viewport as it may be incorrectly sized
|
||||||
|
// and positioned
|
||||||
|
const vp = this._viewportLoc;
|
||||||
|
this.viewportChangeSize(vp.w, vp.h);
|
||||||
|
this.viewportChangePos(0, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
getImageData() {
|
||||||
|
return this._drawCtx.getImageData(0, 0, this.width, this.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
toDataURL(type, encoderOptions) {
|
||||||
|
return this._backbuffer.toDataURL(type, encoderOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
toBlob(callback, type, quality) {
|
||||||
|
return this._backbuffer.toBlob(callback, type, quality);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Track what parts of the visible canvas that need updating
|
||||||
|
_damage(x, y, w, h) {
|
||||||
|
if (x < this._damageBounds.left) {
|
||||||
|
this._damageBounds.left = x;
|
||||||
|
}
|
||||||
|
if (y < this._damageBounds.top) {
|
||||||
|
this._damageBounds.top = y;
|
||||||
|
}
|
||||||
|
if ((x + w) > this._damageBounds.right) {
|
||||||
|
this._damageBounds.right = x + w;
|
||||||
|
}
|
||||||
|
if ((y + h) > this._damageBounds.bottom) {
|
||||||
|
this._damageBounds.bottom = y + h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the visible canvas with the contents of the
|
||||||
|
// rendering canvas
|
||||||
|
flip(fromQueue) {
|
||||||
|
if (this._renderQ.length !== 0 && !fromQueue) {
|
||||||
|
this._renderQPush({
|
||||||
|
'type': 'flip'
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let x = this._damageBounds.left;
|
||||||
|
let y = this._damageBounds.top;
|
||||||
|
let w = this._damageBounds.right - x;
|
||||||
|
let h = this._damageBounds.bottom - y;
|
||||||
|
|
||||||
|
let vx = x - this._viewportLoc.x;
|
||||||
|
let vy = y - this._viewportLoc.y;
|
||||||
|
|
||||||
|
if (vx < 0) {
|
||||||
|
w += vx;
|
||||||
|
x -= vx;
|
||||||
|
vx = 0;
|
||||||
|
}
|
||||||
|
if (vy < 0) {
|
||||||
|
h += vy;
|
||||||
|
y -= vy;
|
||||||
|
vy = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((vx + w) > this._viewportLoc.w) {
|
||||||
|
w = this._viewportLoc.w - vx;
|
||||||
|
}
|
||||||
|
if ((vy + h) > this._viewportLoc.h) {
|
||||||
|
h = this._viewportLoc.h - vy;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((w > 0) && (h > 0)) {
|
||||||
|
// FIXME: We may need to disable image smoothing here
|
||||||
|
// as well (see copyImage()), but we haven't
|
||||||
|
// noticed any problem yet.
|
||||||
|
this._targetCtx.drawImage(this._backbuffer,
|
||||||
|
x, y, w, h,
|
||||||
|
vx, vy, w, h);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._damageBounds.left = this._damageBounds.top = 65535;
|
||||||
|
this._damageBounds.right = this._damageBounds.bottom = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pending() {
|
||||||
|
return this._renderQ.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
if (this._renderQ.length === 0) {
|
||||||
|
return Promise.resolve();
|
||||||
|
} else {
|
||||||
|
if (this._flushPromise === null) {
|
||||||
|
this._flushPromise = new Promise((resolve) => {
|
||||||
|
this._flushResolve = resolve;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return this._flushPromise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fillRect(x, y, width, height, color, fromQueue) {
|
||||||
|
if (this._renderQ.length !== 0 && !fromQueue) {
|
||||||
|
this._renderQPush({
|
||||||
|
'type': 'fill',
|
||||||
|
'x': x,
|
||||||
|
'y': y,
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
'color': color
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._setFillColor(color);
|
||||||
|
this._drawCtx.fillRect(x, y, width, height);
|
||||||
|
this._damage(x, y, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
copyImage(oldX, oldY, newX, newY, w, h, fromQueue) {
|
||||||
|
if (this._renderQ.length !== 0 && !fromQueue) {
|
||||||
|
this._renderQPush({
|
||||||
|
'type': 'copy',
|
||||||
|
'oldX': oldX,
|
||||||
|
'oldY': oldY,
|
||||||
|
'x': newX,
|
||||||
|
'y': newY,
|
||||||
|
'width': w,
|
||||||
|
'height': h,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Due to this bug among others [1] we need to disable the image-smoothing to
|
||||||
|
// avoid getting a blur effect when copying data.
|
||||||
|
//
|
||||||
|
// 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719
|
||||||
|
//
|
||||||
|
// We need to set these every time since all properties are reset
|
||||||
|
// when the the size is changed
|
||||||
|
this._drawCtx.mozImageSmoothingEnabled = false;
|
||||||
|
this._drawCtx.webkitImageSmoothingEnabled = false;
|
||||||
|
this._drawCtx.msImageSmoothingEnabled = false;
|
||||||
|
this._drawCtx.imageSmoothingEnabled = false;
|
||||||
|
|
||||||
|
this._drawCtx.drawImage(this._backbuffer,
|
||||||
|
oldX, oldY, w, h,
|
||||||
|
newX, newY, w, h);
|
||||||
|
this._damage(newX, newY, w, h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
imageRect(x, y, width, height, mime, arr) {
|
||||||
|
/* The internal logic cannot handle empty images, so bail early */
|
||||||
|
if ((width === 0) || (height === 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
img.src = "data: " + mime + ";base64," + Base64.encode(arr);
|
||||||
|
|
||||||
|
this._renderQPush({
|
||||||
|
'type': 'img',
|
||||||
|
'img': img,
|
||||||
|
'x': x,
|
||||||
|
'y': y,
|
||||||
|
'width': width,
|
||||||
|
'height': height
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
videoFrame(x, y, width, height, frame) {
|
||||||
|
this._renderQPush({
|
||||||
|
'type': 'frame',
|
||||||
|
'frame': frame,
|
||||||
|
'x': x,
|
||||||
|
'y': y,
|
||||||
|
'width': width,
|
||||||
|
'height': height
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
blitImage(x, y, width, height, arr, offset, fromQueue) {
|
||||||
|
if (this._renderQ.length !== 0 && !fromQueue) {
|
||||||
|
// NB(directxman12): it's technically more performant here to use preallocated arrays,
|
||||||
|
// but it's a lot of extra work for not a lot of payoff -- if we're using the render queue,
|
||||||
|
// this probably isn't getting called *nearly* as much
|
||||||
|
const newArr = new Uint8Array(width * height * 4);
|
||||||
|
newArr.set(new Uint8Array(arr.buffer, 0, newArr.length));
|
||||||
|
this._renderQPush({
|
||||||
|
'type': 'blit',
|
||||||
|
'data': newArr,
|
||||||
|
'x': x,
|
||||||
|
'y': y,
|
||||||
|
'width': width,
|
||||||
|
'height': height,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// NB(directxman12): arr must be an Type Array view
|
||||||
|
let data = new Uint8ClampedArray(arr.buffer,
|
||||||
|
arr.byteOffset + offset,
|
||||||
|
width * height * 4);
|
||||||
|
let img = new ImageData(data, width, height);
|
||||||
|
this._drawCtx.putImageData(img, x, y);
|
||||||
|
this._damage(x, y, width, height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawImage(img, ...args) {
|
||||||
|
this._drawCtx.drawImage(img, ...args);
|
||||||
|
|
||||||
|
if (args.length <= 4) {
|
||||||
|
const [x, y] = args;
|
||||||
|
this._damage(x, y, img.width, img.height);
|
||||||
|
} else {
|
||||||
|
const [,, sw, sh, dx, dy] = args;
|
||||||
|
this._damage(dx, dy, sw, sh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
autoscale(containerWidth, containerHeight) {
|
||||||
|
let scaleRatio;
|
||||||
|
|
||||||
|
if (containerWidth === 0 || containerHeight === 0) {
|
||||||
|
scaleRatio = 0;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
const vp = this._viewportLoc;
|
||||||
|
const targetAspectRatio = containerWidth / containerHeight;
|
||||||
|
const fbAspectRatio = vp.w / vp.h;
|
||||||
|
|
||||||
|
if (fbAspectRatio >= targetAspectRatio) {
|
||||||
|
scaleRatio = containerWidth / vp.w;
|
||||||
|
} else {
|
||||||
|
scaleRatio = containerHeight / vp.h;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._rescale(scaleRatio);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== PRIVATE METHODS =====
|
||||||
|
|
||||||
|
_rescale(factor) {
|
||||||
|
this._scale = factor;
|
||||||
|
const vp = this._viewportLoc;
|
||||||
|
|
||||||
|
// NB(directxman12): If you set the width directly, or set the
|
||||||
|
// style width to a number, the canvas is cleared.
|
||||||
|
// However, if you set the style width to a string
|
||||||
|
// ('NNNpx'), the canvas is scaled without clearing.
|
||||||
|
const width = factor * vp.w + 'px';
|
||||||
|
const height = factor * vp.h + 'px';
|
||||||
|
|
||||||
|
if ((this._target.style.width !== width) ||
|
||||||
|
(this._target.style.height !== height)) {
|
||||||
|
this._target.style.width = width;
|
||||||
|
this._target.style.height = height;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_setFillColor(color) {
|
||||||
|
const newStyle = 'rgb(' + color[0] + ',' + color[1] + ',' + color[2] + ')';
|
||||||
|
if (newStyle !== this._prevDrawStyle) {
|
||||||
|
this._drawCtx.fillStyle = newStyle;
|
||||||
|
this._prevDrawStyle = newStyle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_renderQPush(action) {
|
||||||
|
this._renderQ.push(action);
|
||||||
|
if (this._renderQ.length === 1) {
|
||||||
|
// If this can be rendered immediately it will be, otherwise
|
||||||
|
// the scanner will wait for the relevant event
|
||||||
|
this._scanRenderQ();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_resumeRenderQ() {
|
||||||
|
// "this" is the object that is ready, not the
|
||||||
|
// display object
|
||||||
|
this.removeEventListener('load', this._noVNCDisplay._resumeRenderQ);
|
||||||
|
this._noVNCDisplay._scanRenderQ();
|
||||||
|
}
|
||||||
|
|
||||||
|
_scanRenderQ() {
|
||||||
|
let ready = true;
|
||||||
|
while (ready && this._renderQ.length > 0) {
|
||||||
|
const a = this._renderQ[0];
|
||||||
|
switch (a.type) {
|
||||||
|
case 'flip':
|
||||||
|
this.flip(true);
|
||||||
|
break;
|
||||||
|
case 'copy':
|
||||||
|
this.copyImage(a.oldX, a.oldY, a.x, a.y, a.width, a.height, true);
|
||||||
|
break;
|
||||||
|
case 'fill':
|
||||||
|
this.fillRect(a.x, a.y, a.width, a.height, a.color, true);
|
||||||
|
break;
|
||||||
|
case 'blit':
|
||||||
|
this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true);
|
||||||
|
break;
|
||||||
|
case 'img':
|
||||||
|
if (a.img.complete) {
|
||||||
|
if (a.img.width !== a.width || a.img.height !== a.height) {
|
||||||
|
Log.Error("Decoded image has incorrect dimensions. Got " +
|
||||||
|
a.img.width + "x" + a.img.height + ". Expected " +
|
||||||
|
a.width + "x" + a.height + ".");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.drawImage(a.img, a.x, a.y);
|
||||||
|
} else {
|
||||||
|
a.img._noVNCDisplay = this;
|
||||||
|
a.img.addEventListener('load', this._resumeRenderQ);
|
||||||
|
// We need to wait for this image to 'load'
|
||||||
|
// to keep things in-order
|
||||||
|
ready = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'frame':
|
||||||
|
if (a.frame.ready) {
|
||||||
|
// The encoded frame may be larger than the rect due to
|
||||||
|
// limitations of the encoder, so we need to crop the
|
||||||
|
// frame.
|
||||||
|
let frame = a.frame.frame;
|
||||||
|
if (frame.codedWidth < a.width || frame.codedHeight < a.height) {
|
||||||
|
Log.Warn("Decoded video frame does not cover its full rectangle area. Expecting at least " +
|
||||||
|
a.width + "x" + a.height + " but got " +
|
||||||
|
frame.codedWidth + "x" + frame.codedHeight);
|
||||||
|
}
|
||||||
|
const sx = 0;
|
||||||
|
const sy = 0;
|
||||||
|
const sw = a.width;
|
||||||
|
const sh = a.height;
|
||||||
|
const dx = a.x;
|
||||||
|
const dy = a.y;
|
||||||
|
const dw = sw;
|
||||||
|
const dh = sh;
|
||||||
|
this.drawImage(frame, sx, sy, sw, sh, dx, dy, dw, dh);
|
||||||
|
frame.close();
|
||||||
|
} else {
|
||||||
|
let display = this;
|
||||||
|
a.frame.promise.then(() => {
|
||||||
|
display._scanRenderQ();
|
||||||
|
});
|
||||||
|
ready = false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ready) {
|
||||||
|
this._renderQ.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._renderQ.length === 0 &&
|
||||||
|
this._flushPromise !== null) {
|
||||||
|
this._flushResolve();
|
||||||
|
this._flushPromise = null;
|
||||||
|
this._flushResolve = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const encodings = {
|
||||||
|
encodingRaw: 0,
|
||||||
|
encodingCopyRect: 1,
|
||||||
|
encodingRRE: 2,
|
||||||
|
encodingHextile: 5,
|
||||||
|
encodingZlib: 6,
|
||||||
|
encodingTight: 7,
|
||||||
|
encodingZRLE: 16,
|
||||||
|
encodingTightPNG: -260,
|
||||||
|
encodingJPEG: 21,
|
||||||
|
encodingH264: 50,
|
||||||
|
|
||||||
|
pseudoEncodingQualityLevel9: -23,
|
||||||
|
pseudoEncodingQualityLevel0: -32,
|
||||||
|
pseudoEncodingDesktopSize: -223,
|
||||||
|
pseudoEncodingLastRect: -224,
|
||||||
|
pseudoEncodingCursor: -239,
|
||||||
|
pseudoEncodingQEMUExtendedKeyEvent: -258,
|
||||||
|
pseudoEncodingQEMULedEvent: -261,
|
||||||
|
pseudoEncodingDesktopName: -307,
|
||||||
|
pseudoEncodingExtendedDesktopSize: -308,
|
||||||
|
pseudoEncodingXvp: -309,
|
||||||
|
pseudoEncodingFence: -312,
|
||||||
|
pseudoEncodingContinuousUpdates: -313,
|
||||||
|
pseudoEncodingExtendedMouseButtons: -316,
|
||||||
|
pseudoEncodingCompressLevel9: -247,
|
||||||
|
pseudoEncodingCompressLevel0: -256,
|
||||||
|
pseudoEncodingVMwareCursor: 0x574d5664,
|
||||||
|
pseudoEncodingExtendedClipboard: 0xc0a1e5ce
|
||||||
|
};
|
||||||
|
|
||||||
|
export function encodingName(num) {
|
||||||
|
switch (num) {
|
||||||
|
case encodings.encodingRaw: return "Raw";
|
||||||
|
case encodings.encodingCopyRect: return "CopyRect";
|
||||||
|
case encodings.encodingRRE: return "RRE";
|
||||||
|
case encodings.encodingHextile: return "Hextile";
|
||||||
|
case encodings.encodingZlib: return "Zlib";
|
||||||
|
case encodings.encodingTight: return "Tight";
|
||||||
|
case encodings.encodingZRLE: return "ZRLE";
|
||||||
|
case encodings.encodingTightPNG: return "TightPNG";
|
||||||
|
case encodings.encodingJPEG: return "JPEG";
|
||||||
|
case encodings.encodingH264: return "H.264";
|
||||||
|
default: return "[unknown encoding " + num + "]";
|
||||||
|
}
|
||||||
|
}
|
||||||
+65
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2020 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { inflateInit, inflate, inflateReset } from "../vendor/pako/lib/zlib/inflate.js";
|
||||||
|
import ZStream from "../vendor/pako/lib/zlib/zstream.js";
|
||||||
|
|
||||||
|
export default class Inflate {
|
||||||
|
constructor() {
|
||||||
|
this.strm = new ZStream();
|
||||||
|
this.chunkSize = 1024 * 10 * 10;
|
||||||
|
this.strm.output = new Uint8Array(this.chunkSize);
|
||||||
|
|
||||||
|
inflateInit(this.strm);
|
||||||
|
}
|
||||||
|
|
||||||
|
setInput(data) {
|
||||||
|
if (!data) {
|
||||||
|
//FIXME: flush remaining data.
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
this.strm.input = null;
|
||||||
|
this.strm.avail_in = 0;
|
||||||
|
this.strm.next_in = 0;
|
||||||
|
} else {
|
||||||
|
this.strm.input = data;
|
||||||
|
this.strm.avail_in = this.strm.input.length;
|
||||||
|
this.strm.next_in = 0;
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inflate(expected) {
|
||||||
|
// resize our output buffer if it's too small
|
||||||
|
// (we could just use multiple chunks, but that would cause an extra
|
||||||
|
// allocation each time to flatten the chunks)
|
||||||
|
if (expected > this.chunkSize) {
|
||||||
|
this.chunkSize = expected;
|
||||||
|
this.strm.output = new Uint8Array(this.chunkSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* eslint-disable camelcase */
|
||||||
|
this.strm.next_out = 0;
|
||||||
|
this.strm.avail_out = expected;
|
||||||
|
/* eslint-enable camelcase */
|
||||||
|
|
||||||
|
let ret = inflate(this.strm, 0); // Flush argument not used.
|
||||||
|
if (ret < 0) {
|
||||||
|
throw new Error("zlib inflate failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.strm.next_out != expected) {
|
||||||
|
throw new Error("Incomplete zlib block");
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out);
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
inflateReset(this.strm);
|
||||||
|
}
|
||||||
|
}
|
||||||
+311
@@ -0,0 +1,311 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2018 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import KeyTable from "./keysym.js";
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mapping between HTML key values and VNC/X11 keysyms for "special"
|
||||||
|
* keys that cannot be handled via their Unicode codepoint.
|
||||||
|
*
|
||||||
|
* See https://www.w3.org/TR/uievents-key/ for possible values.
|
||||||
|
*/
|
||||||
|
|
||||||
|
const DOMKeyTable = {};
|
||||||
|
|
||||||
|
function addStandard(key, standard) {
|
||||||
|
if (standard === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
|
||||||
|
if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\"");
|
||||||
|
DOMKeyTable[key] = [standard, standard, standard, standard];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addLeftRight(key, left, right) {
|
||||||
|
if (left === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
|
||||||
|
if (right === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
|
||||||
|
if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\"");
|
||||||
|
DOMKeyTable[key] = [left, left, right, left];
|
||||||
|
}
|
||||||
|
|
||||||
|
function addNumpad(key, standard, numpad) {
|
||||||
|
if (standard === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
|
||||||
|
if (numpad === undefined) throw new Error("Undefined keysym for key \"" + key + "\"");
|
||||||
|
if (key in DOMKeyTable) throw new Error("Duplicate entry for key \"" + key + "\"");
|
||||||
|
DOMKeyTable[key] = [standard, standard, standard, numpad];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2. Modifier Keys
|
||||||
|
|
||||||
|
addLeftRight("Alt", KeyTable.XK_Alt_L, KeyTable.XK_Alt_R);
|
||||||
|
addStandard("AltGraph", KeyTable.XK_ISO_Level3_Shift);
|
||||||
|
addStandard("CapsLock", KeyTable.XK_Caps_Lock);
|
||||||
|
addLeftRight("Control", KeyTable.XK_Control_L, KeyTable.XK_Control_R);
|
||||||
|
// - Fn
|
||||||
|
// - FnLock
|
||||||
|
addLeftRight("Meta", KeyTable.XK_Super_L, KeyTable.XK_Super_R);
|
||||||
|
addStandard("NumLock", KeyTable.XK_Num_Lock);
|
||||||
|
addStandard("ScrollLock", KeyTable.XK_Scroll_Lock);
|
||||||
|
addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R);
|
||||||
|
// - Symbol
|
||||||
|
// - SymbolLock
|
||||||
|
// - Hyper
|
||||||
|
// - Super
|
||||||
|
|
||||||
|
// 3.3. Whitespace Keys
|
||||||
|
|
||||||
|
addNumpad("Enter", KeyTable.XK_Return, KeyTable.XK_KP_Enter);
|
||||||
|
addStandard("Tab", KeyTable.XK_Tab);
|
||||||
|
addNumpad(" ", KeyTable.XK_space, KeyTable.XK_KP_Space);
|
||||||
|
|
||||||
|
// 3.4. Navigation Keys
|
||||||
|
|
||||||
|
addNumpad("ArrowDown", KeyTable.XK_Down, KeyTable.XK_KP_Down);
|
||||||
|
addNumpad("ArrowLeft", KeyTable.XK_Left, KeyTable.XK_KP_Left);
|
||||||
|
addNumpad("ArrowRight", KeyTable.XK_Right, KeyTable.XK_KP_Right);
|
||||||
|
addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up);
|
||||||
|
addNumpad("End", KeyTable.XK_End, KeyTable.XK_KP_End);
|
||||||
|
addNumpad("Home", KeyTable.XK_Home, KeyTable.XK_KP_Home);
|
||||||
|
addNumpad("PageDown", KeyTable.XK_Next, KeyTable.XK_KP_Next);
|
||||||
|
addNumpad("PageUp", KeyTable.XK_Prior, KeyTable.XK_KP_Prior);
|
||||||
|
|
||||||
|
// 3.5. Editing Keys
|
||||||
|
|
||||||
|
addStandard("Backspace", KeyTable.XK_BackSpace);
|
||||||
|
// Browsers send "Clear" for the numpad 5 without NumLock because
|
||||||
|
// Windows uses VK_Clear for that key. But Unix expects KP_Begin for
|
||||||
|
// that scenario.
|
||||||
|
addNumpad("Clear", KeyTable.XK_Clear, KeyTable.XK_KP_Begin);
|
||||||
|
addStandard("Copy", KeyTable.XF86XK_Copy);
|
||||||
|
// - CrSel
|
||||||
|
addStandard("Cut", KeyTable.XF86XK_Cut);
|
||||||
|
addNumpad("Delete", KeyTable.XK_Delete, KeyTable.XK_KP_Delete);
|
||||||
|
// - EraseEof
|
||||||
|
// - ExSel
|
||||||
|
addNumpad("Insert", KeyTable.XK_Insert, KeyTable.XK_KP_Insert);
|
||||||
|
addStandard("Paste", KeyTable.XF86XK_Paste);
|
||||||
|
addStandard("Redo", KeyTable.XK_Redo);
|
||||||
|
addStandard("Undo", KeyTable.XK_Undo);
|
||||||
|
|
||||||
|
// 3.6. UI Keys
|
||||||
|
|
||||||
|
// - Accept
|
||||||
|
// - Again (could just be XK_Redo)
|
||||||
|
// - Attn
|
||||||
|
addStandard("Cancel", KeyTable.XK_Cancel);
|
||||||
|
addStandard("ContextMenu", KeyTable.XK_Menu);
|
||||||
|
addStandard("Escape", KeyTable.XK_Escape);
|
||||||
|
addStandard("Execute", KeyTable.XK_Execute);
|
||||||
|
addStandard("Find", KeyTable.XK_Find);
|
||||||
|
addStandard("Help", KeyTable.XK_Help);
|
||||||
|
addStandard("Pause", KeyTable.XK_Pause);
|
||||||
|
// - Play
|
||||||
|
// - Props
|
||||||
|
addStandard("Select", KeyTable.XK_Select);
|
||||||
|
addStandard("ZoomIn", KeyTable.XF86XK_ZoomIn);
|
||||||
|
addStandard("ZoomOut", KeyTable.XF86XK_ZoomOut);
|
||||||
|
|
||||||
|
// 3.7. Device Keys
|
||||||
|
|
||||||
|
addStandard("BrightnessDown", KeyTable.XF86XK_MonBrightnessDown);
|
||||||
|
addStandard("BrightnessUp", KeyTable.XF86XK_MonBrightnessUp);
|
||||||
|
addStandard("Eject", KeyTable.XF86XK_Eject);
|
||||||
|
addStandard("LogOff", KeyTable.XF86XK_LogOff);
|
||||||
|
addStandard("Power", KeyTable.XF86XK_PowerOff);
|
||||||
|
addStandard("PowerOff", KeyTable.XF86XK_PowerDown);
|
||||||
|
addStandard("PrintScreen", KeyTable.XK_Print);
|
||||||
|
addStandard("Hibernate", KeyTable.XF86XK_Hibernate);
|
||||||
|
addStandard("Standby", KeyTable.XF86XK_Standby);
|
||||||
|
addStandard("WakeUp", KeyTable.XF86XK_WakeUp);
|
||||||
|
|
||||||
|
// 3.8. IME and Composition Keys
|
||||||
|
|
||||||
|
addStandard("AllCandidates", KeyTable.XK_MultipleCandidate);
|
||||||
|
addStandard("Alphanumeric", KeyTable.XK_Eisu_toggle);
|
||||||
|
addStandard("CodeInput", KeyTable.XK_Codeinput);
|
||||||
|
addStandard("Compose", KeyTable.XK_Multi_key);
|
||||||
|
addStandard("Convert", KeyTable.XK_Henkan);
|
||||||
|
// - Dead
|
||||||
|
// - FinalMode
|
||||||
|
addStandard("GroupFirst", KeyTable.XK_ISO_First_Group);
|
||||||
|
addStandard("GroupLast", KeyTable.XK_ISO_Last_Group);
|
||||||
|
addStandard("GroupNext", KeyTable.XK_ISO_Next_Group);
|
||||||
|
addStandard("GroupPrevious", KeyTable.XK_ISO_Prev_Group);
|
||||||
|
// - ModeChange (XK_Mode_switch is often used for AltGr)
|
||||||
|
// - NextCandidate
|
||||||
|
addStandard("NonConvert", KeyTable.XK_Muhenkan);
|
||||||
|
addStandard("PreviousCandidate", KeyTable.XK_PreviousCandidate);
|
||||||
|
// - Process
|
||||||
|
addStandard("SingleCandidate", KeyTable.XK_SingleCandidate);
|
||||||
|
addStandard("HangulMode", KeyTable.XK_Hangul);
|
||||||
|
addStandard("HanjaMode", KeyTable.XK_Hangul_Hanja);
|
||||||
|
addStandard("JunjaMode", KeyTable.XK_Hangul_Jeonja);
|
||||||
|
addStandard("Eisu", KeyTable.XK_Eisu_toggle);
|
||||||
|
addStandard("Hankaku", KeyTable.XK_Hankaku);
|
||||||
|
addStandard("Hiragana", KeyTable.XK_Hiragana);
|
||||||
|
addStandard("HiraganaKatakana", KeyTable.XK_Hiragana_Katakana);
|
||||||
|
addStandard("KanaMode", KeyTable.XK_Kana_Shift); // could also be _Kana_Lock
|
||||||
|
addStandard("KanjiMode", KeyTable.XK_Kanji);
|
||||||
|
addStandard("Katakana", KeyTable.XK_Katakana);
|
||||||
|
addStandard("Romaji", KeyTable.XK_Romaji);
|
||||||
|
addStandard("Zenkaku", KeyTable.XK_Zenkaku);
|
||||||
|
addStandard("ZenkakuHankaku", KeyTable.XK_Zenkaku_Hankaku);
|
||||||
|
|
||||||
|
// 3.9. General-Purpose Function Keys
|
||||||
|
|
||||||
|
addStandard("F1", KeyTable.XK_F1);
|
||||||
|
addStandard("F2", KeyTable.XK_F2);
|
||||||
|
addStandard("F3", KeyTable.XK_F3);
|
||||||
|
addStandard("F4", KeyTable.XK_F4);
|
||||||
|
addStandard("F5", KeyTable.XK_F5);
|
||||||
|
addStandard("F6", KeyTable.XK_F6);
|
||||||
|
addStandard("F7", KeyTable.XK_F7);
|
||||||
|
addStandard("F8", KeyTable.XK_F8);
|
||||||
|
addStandard("F9", KeyTable.XK_F9);
|
||||||
|
addStandard("F10", KeyTable.XK_F10);
|
||||||
|
addStandard("F11", KeyTable.XK_F11);
|
||||||
|
addStandard("F12", KeyTable.XK_F12);
|
||||||
|
addStandard("F13", KeyTable.XK_F13);
|
||||||
|
addStandard("F14", KeyTable.XK_F14);
|
||||||
|
addStandard("F15", KeyTable.XK_F15);
|
||||||
|
addStandard("F16", KeyTable.XK_F16);
|
||||||
|
addStandard("F17", KeyTable.XK_F17);
|
||||||
|
addStandard("F18", KeyTable.XK_F18);
|
||||||
|
addStandard("F19", KeyTable.XK_F19);
|
||||||
|
addStandard("F20", KeyTable.XK_F20);
|
||||||
|
addStandard("F21", KeyTable.XK_F21);
|
||||||
|
addStandard("F22", KeyTable.XK_F22);
|
||||||
|
addStandard("F23", KeyTable.XK_F23);
|
||||||
|
addStandard("F24", KeyTable.XK_F24);
|
||||||
|
addStandard("F25", KeyTable.XK_F25);
|
||||||
|
addStandard("F26", KeyTable.XK_F26);
|
||||||
|
addStandard("F27", KeyTable.XK_F27);
|
||||||
|
addStandard("F28", KeyTable.XK_F28);
|
||||||
|
addStandard("F29", KeyTable.XK_F29);
|
||||||
|
addStandard("F30", KeyTable.XK_F30);
|
||||||
|
addStandard("F31", KeyTable.XK_F31);
|
||||||
|
addStandard("F32", KeyTable.XK_F32);
|
||||||
|
addStandard("F33", KeyTable.XK_F33);
|
||||||
|
addStandard("F34", KeyTable.XK_F34);
|
||||||
|
addStandard("F35", KeyTable.XK_F35);
|
||||||
|
// - Soft1...
|
||||||
|
|
||||||
|
// 3.10. Multimedia Keys
|
||||||
|
|
||||||
|
// - ChannelDown
|
||||||
|
// - ChannelUp
|
||||||
|
addStandard("Close", KeyTable.XF86XK_Close);
|
||||||
|
addStandard("MailForward", KeyTable.XF86XK_MailForward);
|
||||||
|
addStandard("MailReply", KeyTable.XF86XK_Reply);
|
||||||
|
addStandard("MailSend", KeyTable.XF86XK_Send);
|
||||||
|
// - MediaClose
|
||||||
|
addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward);
|
||||||
|
addStandard("MediaPause", KeyTable.XF86XK_AudioPause);
|
||||||
|
addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay);
|
||||||
|
// - MediaPlayPause
|
||||||
|
addStandard("MediaRecord", KeyTable.XF86XK_AudioRecord);
|
||||||
|
addStandard("MediaRewind", KeyTable.XF86XK_AudioRewind);
|
||||||
|
addStandard("MediaStop", KeyTable.XF86XK_AudioStop);
|
||||||
|
addStandard("MediaTrackNext", KeyTable.XF86XK_AudioNext);
|
||||||
|
addStandard("MediaTrackPrevious", KeyTable.XF86XK_AudioPrev);
|
||||||
|
addStandard("New", KeyTable.XF86XK_New);
|
||||||
|
addStandard("Open", KeyTable.XF86XK_Open);
|
||||||
|
addStandard("Print", KeyTable.XK_Print);
|
||||||
|
addStandard("Save", KeyTable.XF86XK_Save);
|
||||||
|
addStandard("SpellCheck", KeyTable.XF86XK_Spell);
|
||||||
|
|
||||||
|
// 3.11. Multimedia Numpad Keys
|
||||||
|
|
||||||
|
// - Key11
|
||||||
|
// - Key12
|
||||||
|
|
||||||
|
// 3.12. Audio Keys
|
||||||
|
|
||||||
|
// - AudioBalanceLeft
|
||||||
|
// - AudioBalanceRight
|
||||||
|
// - AudioBassBoostDown
|
||||||
|
// - AudioBassBoostToggle
|
||||||
|
// - AudioBassBoostUp
|
||||||
|
// - AudioFaderFront
|
||||||
|
// - AudioFaderRear
|
||||||
|
// - AudioSurroundModeNext
|
||||||
|
// - AudioTrebleDown
|
||||||
|
// - AudioTrebleUp
|
||||||
|
addStandard("AudioVolumeDown", KeyTable.XF86XK_AudioLowerVolume);
|
||||||
|
addStandard("AudioVolumeUp", KeyTable.XF86XK_AudioRaiseVolume);
|
||||||
|
addStandard("AudioVolumeMute", KeyTable.XF86XK_AudioMute);
|
||||||
|
// - MicrophoneToggle
|
||||||
|
// - MicrophoneVolumeDown
|
||||||
|
// - MicrophoneVolumeUp
|
||||||
|
addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute);
|
||||||
|
|
||||||
|
// 3.13. Speech Keys
|
||||||
|
|
||||||
|
// - SpeechCorrectionList
|
||||||
|
// - SpeechInputToggle
|
||||||
|
|
||||||
|
// 3.14. Application Keys
|
||||||
|
|
||||||
|
addStandard("LaunchApplication1", KeyTable.XF86XK_MyComputer);
|
||||||
|
addStandard("LaunchApplication2", KeyTable.XF86XK_Calculator);
|
||||||
|
addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar);
|
||||||
|
// - LaunchContacts
|
||||||
|
addStandard("LaunchMail", KeyTable.XF86XK_Mail);
|
||||||
|
addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia);
|
||||||
|
addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music);
|
||||||
|
addStandard("LaunchPhone", KeyTable.XF86XK_Phone);
|
||||||
|
addStandard("LaunchScreenSaver", KeyTable.XF86XK_ScreenSaver);
|
||||||
|
addStandard("LaunchSpreadsheet", KeyTable.XF86XK_Excel);
|
||||||
|
addStandard("LaunchWebBrowser", KeyTable.XF86XK_WWW);
|
||||||
|
addStandard("LaunchWebCam", KeyTable.XF86XK_WebCam);
|
||||||
|
addStandard("LaunchWordProcessor", KeyTable.XF86XK_Word);
|
||||||
|
|
||||||
|
// 3.15. Browser Keys
|
||||||
|
|
||||||
|
addStandard("BrowserBack", KeyTable.XF86XK_Back);
|
||||||
|
addStandard("BrowserFavorites", KeyTable.XF86XK_Favorites);
|
||||||
|
addStandard("BrowserForward", KeyTable.XF86XK_Forward);
|
||||||
|
addStandard("BrowserHome", KeyTable.XF86XK_HomePage);
|
||||||
|
addStandard("BrowserRefresh", KeyTable.XF86XK_Refresh);
|
||||||
|
addStandard("BrowserSearch", KeyTable.XF86XK_Search);
|
||||||
|
addStandard("BrowserStop", KeyTable.XF86XK_Stop);
|
||||||
|
|
||||||
|
// 3.16. Mobile Phone Keys
|
||||||
|
|
||||||
|
// - A whole bunch...
|
||||||
|
|
||||||
|
// 3.17. TV Keys
|
||||||
|
|
||||||
|
// - A whole bunch...
|
||||||
|
|
||||||
|
// 3.18. Media Controller Keys
|
||||||
|
|
||||||
|
// - A whole bunch...
|
||||||
|
addStandard("Dimmer", KeyTable.XF86XK_BrightnessAdjust);
|
||||||
|
addStandard("MediaAudioTrack", KeyTable.XF86XK_AudioCycleTrack);
|
||||||
|
addStandard("RandomToggle", KeyTable.XF86XK_AudioRandomPlay);
|
||||||
|
addStandard("SplitScreenToggle", KeyTable.XF86XK_SplitScreen);
|
||||||
|
addStandard("Subtitle", KeyTable.XF86XK_Subtitle);
|
||||||
|
addStandard("VideoModeNext", KeyTable.XF86XK_Next_VMode);
|
||||||
|
|
||||||
|
// Extra: Numpad
|
||||||
|
|
||||||
|
addNumpad("=", KeyTable.XK_equal, KeyTable.XK_KP_Equal);
|
||||||
|
addNumpad("+", KeyTable.XK_plus, KeyTable.XK_KP_Add);
|
||||||
|
addNumpad("-", KeyTable.XK_minus, KeyTable.XK_KP_Subtract);
|
||||||
|
addNumpad("*", KeyTable.XK_asterisk, KeyTable.XK_KP_Multiply);
|
||||||
|
addNumpad("/", KeyTable.XK_slash, KeyTable.XK_KP_Divide);
|
||||||
|
addNumpad(".", KeyTable.XK_period, KeyTable.XK_KP_Decimal);
|
||||||
|
addNumpad(",", KeyTable.XK_comma, KeyTable.XK_KP_Separator);
|
||||||
|
addNumpad("0", KeyTable.XK_0, KeyTable.XK_KP_0);
|
||||||
|
addNumpad("1", KeyTable.XK_1, KeyTable.XK_KP_1);
|
||||||
|
addNumpad("2", KeyTable.XK_2, KeyTable.XK_KP_2);
|
||||||
|
addNumpad("3", KeyTable.XK_3, KeyTable.XK_KP_3);
|
||||||
|
addNumpad("4", KeyTable.XK_4, KeyTable.XK_KP_4);
|
||||||
|
addNumpad("5", KeyTable.XK_5, KeyTable.XK_KP_5);
|
||||||
|
addNumpad("6", KeyTable.XK_6, KeyTable.XK_KP_6);
|
||||||
|
addNumpad("7", KeyTable.XK_7, KeyTable.XK_KP_7);
|
||||||
|
addNumpad("8", KeyTable.XK_8, KeyTable.XK_KP_8);
|
||||||
|
addNumpad("9", KeyTable.XK_9, KeyTable.XK_KP_9);
|
||||||
|
|
||||||
|
export default DOMKeyTable;
|
||||||
+129
@@ -0,0 +1,129 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2018 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Fallback mapping between HTML key codes (physical keys) and
|
||||||
|
* HTML key values. This only works for keys that don't vary
|
||||||
|
* between layouts. We also omit those who manage fine by mapping the
|
||||||
|
* Unicode representation.
|
||||||
|
*
|
||||||
|
* See https://www.w3.org/TR/uievents-code/ for possible codes.
|
||||||
|
* See https://www.w3.org/TR/uievents-key/ for possible values.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* eslint-disable key-spacing */
|
||||||
|
|
||||||
|
export default {
|
||||||
|
|
||||||
|
// 3.1.1.1. Writing System Keys
|
||||||
|
|
||||||
|
'Backspace': 'Backspace',
|
||||||
|
|
||||||
|
// 3.1.1.2. Functional Keys
|
||||||
|
|
||||||
|
'AltLeft': 'Alt',
|
||||||
|
'AltRight': 'Alt', // This could also be 'AltGraph'
|
||||||
|
'CapsLock': 'CapsLock',
|
||||||
|
'ContextMenu': 'ContextMenu',
|
||||||
|
'ControlLeft': 'Control',
|
||||||
|
'ControlRight': 'Control',
|
||||||
|
'Enter': 'Enter',
|
||||||
|
'MetaLeft': 'Meta',
|
||||||
|
'MetaRight': 'Meta',
|
||||||
|
'ShiftLeft': 'Shift',
|
||||||
|
'ShiftRight': 'Shift',
|
||||||
|
'Tab': 'Tab',
|
||||||
|
// FIXME: Japanese/Korean keys
|
||||||
|
|
||||||
|
// 3.1.2. Control Pad Section
|
||||||
|
|
||||||
|
'Delete': 'Delete',
|
||||||
|
'End': 'End',
|
||||||
|
'Help': 'Help',
|
||||||
|
'Home': 'Home',
|
||||||
|
'Insert': 'Insert',
|
||||||
|
'PageDown': 'PageDown',
|
||||||
|
'PageUp': 'PageUp',
|
||||||
|
|
||||||
|
// 3.1.3. Arrow Pad Section
|
||||||
|
|
||||||
|
'ArrowDown': 'ArrowDown',
|
||||||
|
'ArrowLeft': 'ArrowLeft',
|
||||||
|
'ArrowRight': 'ArrowRight',
|
||||||
|
'ArrowUp': 'ArrowUp',
|
||||||
|
|
||||||
|
// 3.1.4. Numpad Section
|
||||||
|
|
||||||
|
'NumLock': 'NumLock',
|
||||||
|
'NumpadBackspace': 'Backspace',
|
||||||
|
'NumpadClear': 'Clear',
|
||||||
|
|
||||||
|
// 3.1.5. Function Section
|
||||||
|
|
||||||
|
'Escape': 'Escape',
|
||||||
|
'F1': 'F1',
|
||||||
|
'F2': 'F2',
|
||||||
|
'F3': 'F3',
|
||||||
|
'F4': 'F4',
|
||||||
|
'F5': 'F5',
|
||||||
|
'F6': 'F6',
|
||||||
|
'F7': 'F7',
|
||||||
|
'F8': 'F8',
|
||||||
|
'F9': 'F9',
|
||||||
|
'F10': 'F10',
|
||||||
|
'F11': 'F11',
|
||||||
|
'F12': 'F12',
|
||||||
|
'F13': 'F13',
|
||||||
|
'F14': 'F14',
|
||||||
|
'F15': 'F15',
|
||||||
|
'F16': 'F16',
|
||||||
|
'F17': 'F17',
|
||||||
|
'F18': 'F18',
|
||||||
|
'F19': 'F19',
|
||||||
|
'F20': 'F20',
|
||||||
|
'F21': 'F21',
|
||||||
|
'F22': 'F22',
|
||||||
|
'F23': 'F23',
|
||||||
|
'F24': 'F24',
|
||||||
|
'F25': 'F25',
|
||||||
|
'F26': 'F26',
|
||||||
|
'F27': 'F27',
|
||||||
|
'F28': 'F28',
|
||||||
|
'F29': 'F29',
|
||||||
|
'F30': 'F30',
|
||||||
|
'F31': 'F31',
|
||||||
|
'F32': 'F32',
|
||||||
|
'F33': 'F33',
|
||||||
|
'F34': 'F34',
|
||||||
|
'F35': 'F35',
|
||||||
|
'PrintScreen': 'PrintScreen',
|
||||||
|
'ScrollLock': 'ScrollLock',
|
||||||
|
'Pause': 'Pause',
|
||||||
|
|
||||||
|
// 3.1.6. Media Keys
|
||||||
|
|
||||||
|
'BrowserBack': 'BrowserBack',
|
||||||
|
'BrowserFavorites': 'BrowserFavorites',
|
||||||
|
'BrowserForward': 'BrowserForward',
|
||||||
|
'BrowserHome': 'BrowserHome',
|
||||||
|
'BrowserRefresh': 'BrowserRefresh',
|
||||||
|
'BrowserSearch': 'BrowserSearch',
|
||||||
|
'BrowserStop': 'BrowserStop',
|
||||||
|
'Eject': 'Eject',
|
||||||
|
'LaunchApp1': 'LaunchMyComputer',
|
||||||
|
'LaunchApp2': 'LaunchCalendar',
|
||||||
|
'LaunchMail': 'LaunchMail',
|
||||||
|
'MediaPlayPause': 'MediaPlay',
|
||||||
|
'MediaStop': 'MediaStop',
|
||||||
|
'MediaTrackNext': 'MediaTrackNext',
|
||||||
|
'MediaTrackPrevious': 'MediaTrackPrevious',
|
||||||
|
'Power': 'Power',
|
||||||
|
'Sleep': 'Sleep',
|
||||||
|
'AudioVolumeDown': 'AudioVolumeDown',
|
||||||
|
'AudioVolumeMute': 'AudioVolumeMute',
|
||||||
|
'AudioVolumeUp': 'AudioVolumeUp',
|
||||||
|
'WakeUp': 'WakeUp',
|
||||||
|
};
|
||||||
Vendored
+567
@@ -0,0 +1,567 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2020 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
const GH_NOGESTURE = 0;
|
||||||
|
const GH_ONETAP = 1;
|
||||||
|
const GH_TWOTAP = 2;
|
||||||
|
const GH_THREETAP = 4;
|
||||||
|
const GH_DRAG = 8;
|
||||||
|
const GH_LONGPRESS = 16;
|
||||||
|
const GH_TWODRAG = 32;
|
||||||
|
const GH_PINCH = 64;
|
||||||
|
|
||||||
|
const GH_INITSTATE = 127;
|
||||||
|
|
||||||
|
const GH_MOVE_THRESHOLD = 50;
|
||||||
|
const GH_ANGLE_THRESHOLD = 90; // Degrees
|
||||||
|
|
||||||
|
// Timeout when waiting for gestures (ms)
|
||||||
|
const GH_MULTITOUCH_TIMEOUT = 250;
|
||||||
|
|
||||||
|
// Maximum time between press and release for a tap (ms)
|
||||||
|
const GH_TAP_TIMEOUT = 1000;
|
||||||
|
|
||||||
|
// Timeout when waiting for longpress (ms)
|
||||||
|
const GH_LONGPRESS_TIMEOUT = 1000;
|
||||||
|
|
||||||
|
// Timeout when waiting to decide between PINCH and TWODRAG (ms)
|
||||||
|
const GH_TWOTOUCH_TIMEOUT = 50;
|
||||||
|
|
||||||
|
export default class GestureHandler {
|
||||||
|
constructor() {
|
||||||
|
this._target = null;
|
||||||
|
|
||||||
|
this._state = GH_INITSTATE;
|
||||||
|
|
||||||
|
this._tracked = [];
|
||||||
|
this._ignored = [];
|
||||||
|
|
||||||
|
this._waitingRelease = false;
|
||||||
|
this._releaseStart = 0.0;
|
||||||
|
|
||||||
|
this._longpressTimeoutId = null;
|
||||||
|
this._twoTouchTimeoutId = null;
|
||||||
|
|
||||||
|
this._boundEventHandler = this._eventHandler.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
attach(target) {
|
||||||
|
this.detach();
|
||||||
|
|
||||||
|
this._target = target;
|
||||||
|
this._target.addEventListener('touchstart',
|
||||||
|
this._boundEventHandler);
|
||||||
|
this._target.addEventListener('touchmove',
|
||||||
|
this._boundEventHandler);
|
||||||
|
this._target.addEventListener('touchend',
|
||||||
|
this._boundEventHandler);
|
||||||
|
this._target.addEventListener('touchcancel',
|
||||||
|
this._boundEventHandler);
|
||||||
|
}
|
||||||
|
|
||||||
|
detach() {
|
||||||
|
if (!this._target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._stopLongpressTimeout();
|
||||||
|
this._stopTwoTouchTimeout();
|
||||||
|
|
||||||
|
this._target.removeEventListener('touchstart',
|
||||||
|
this._boundEventHandler);
|
||||||
|
this._target.removeEventListener('touchmove',
|
||||||
|
this._boundEventHandler);
|
||||||
|
this._target.removeEventListener('touchend',
|
||||||
|
this._boundEventHandler);
|
||||||
|
this._target.removeEventListener('touchcancel',
|
||||||
|
this._boundEventHandler);
|
||||||
|
this._target = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_eventHandler(e) {
|
||||||
|
let fn;
|
||||||
|
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
switch (e.type) {
|
||||||
|
case 'touchstart':
|
||||||
|
fn = this._touchStart;
|
||||||
|
break;
|
||||||
|
case 'touchmove':
|
||||||
|
fn = this._touchMove;
|
||||||
|
break;
|
||||||
|
case 'touchend':
|
||||||
|
case 'touchcancel':
|
||||||
|
fn = this._touchEnd;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < e.changedTouches.length; i++) {
|
||||||
|
let touch = e.changedTouches[i];
|
||||||
|
fn.call(this, touch.identifier, touch.clientX, touch.clientY);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_touchStart(id, x, y) {
|
||||||
|
// Ignore any new touches if there is already an active gesture,
|
||||||
|
// or we're in a cleanup state
|
||||||
|
if (this._hasDetectedGesture() || (this._state === GH_NOGESTURE)) {
|
||||||
|
this._ignored.push(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did it take too long between touches that we should no longer
|
||||||
|
// consider this a single gesture?
|
||||||
|
if ((this._tracked.length > 0) &&
|
||||||
|
((Date.now() - this._tracked[0].started) > GH_MULTITOUCH_TIMEOUT)) {
|
||||||
|
this._state = GH_NOGESTURE;
|
||||||
|
this._ignored.push(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're waiting for fingers to release then we should no longer
|
||||||
|
// recognize new touches
|
||||||
|
if (this._waitingRelease) {
|
||||||
|
this._state = GH_NOGESTURE;
|
||||||
|
this._ignored.push(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._tracked.push({
|
||||||
|
id: id,
|
||||||
|
started: Date.now(),
|
||||||
|
active: true,
|
||||||
|
firstX: x,
|
||||||
|
firstY: y,
|
||||||
|
lastX: x,
|
||||||
|
lastY: y,
|
||||||
|
angle: 0
|
||||||
|
});
|
||||||
|
|
||||||
|
switch (this._tracked.length) {
|
||||||
|
case 1:
|
||||||
|
this._startLongpressTimeout();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
this._state &= ~(GH_ONETAP | GH_DRAG | GH_LONGPRESS);
|
||||||
|
this._stopLongpressTimeout();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
this._state &= ~(GH_TWOTAP | GH_TWODRAG | GH_PINCH);
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
this._state = GH_NOGESTURE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_touchMove(id, x, y) {
|
||||||
|
let touch = this._tracked.find(t => t.id === id);
|
||||||
|
|
||||||
|
// If this is an update for a touch we're not tracking, ignore it
|
||||||
|
if (touch === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the touches last position with the event coordinates
|
||||||
|
touch.lastX = x;
|
||||||
|
touch.lastY = y;
|
||||||
|
|
||||||
|
let deltaX = x - touch.firstX;
|
||||||
|
let deltaY = y - touch.firstY;
|
||||||
|
|
||||||
|
// Update angle when the touch has moved
|
||||||
|
if ((touch.firstX !== touch.lastX) ||
|
||||||
|
(touch.firstY !== touch.lastY)) {
|
||||||
|
touch.angle = Math.atan2(deltaY, deltaX) * 180 / Math.PI;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._hasDetectedGesture()) {
|
||||||
|
// Ignore moves smaller than the minimum threshold
|
||||||
|
if (Math.hypot(deltaX, deltaY) < GH_MOVE_THRESHOLD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't be a tap or long press as we've seen movement
|
||||||
|
this._state &= ~(GH_ONETAP | GH_TWOTAP | GH_THREETAP | GH_LONGPRESS);
|
||||||
|
this._stopLongpressTimeout();
|
||||||
|
|
||||||
|
if (this._tracked.length !== 1) {
|
||||||
|
this._state &= ~(GH_DRAG);
|
||||||
|
}
|
||||||
|
if (this._tracked.length !== 2) {
|
||||||
|
this._state &= ~(GH_TWODRAG | GH_PINCH);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to figure out which of our different two touch gestures
|
||||||
|
// this might be
|
||||||
|
if (this._tracked.length === 2) {
|
||||||
|
|
||||||
|
// The other touch is the one where the id doesn't match
|
||||||
|
let prevTouch = this._tracked.find(t => t.id !== id);
|
||||||
|
|
||||||
|
// How far the previous touch point has moved since start
|
||||||
|
let prevDeltaMove = Math.hypot(prevTouch.firstX - prevTouch.lastX,
|
||||||
|
prevTouch.firstY - prevTouch.lastY);
|
||||||
|
|
||||||
|
// We know that the current touch moved far enough,
|
||||||
|
// but unless both touches moved further than their
|
||||||
|
// threshold we don't want to disqualify any gestures
|
||||||
|
if (prevDeltaMove > GH_MOVE_THRESHOLD) {
|
||||||
|
|
||||||
|
// The angle difference between the direction of the touch points
|
||||||
|
let deltaAngle = Math.abs(touch.angle - prevTouch.angle);
|
||||||
|
deltaAngle = Math.abs(((deltaAngle + 180) % 360) - 180);
|
||||||
|
|
||||||
|
// PINCH or TWODRAG can be eliminated depending on the angle
|
||||||
|
if (deltaAngle > GH_ANGLE_THRESHOLD) {
|
||||||
|
this._state &= ~GH_TWODRAG;
|
||||||
|
} else {
|
||||||
|
this._state &= ~GH_PINCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._isTwoTouchTimeoutRunning()) {
|
||||||
|
this._stopTwoTouchTimeout();
|
||||||
|
}
|
||||||
|
} else if (!this._isTwoTouchTimeoutRunning()) {
|
||||||
|
// We can't determine the gesture right now, let's
|
||||||
|
// wait and see if more events are on their way
|
||||||
|
this._startTwoTouchTimeout();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._hasDetectedGesture()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pushEvent('gesturestart');
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pushEvent('gesturemove');
|
||||||
|
}
|
||||||
|
|
||||||
|
_touchEnd(id, x, y) {
|
||||||
|
// Check if this is an ignored touch
|
||||||
|
if (this._ignored.indexOf(id) !== -1) {
|
||||||
|
// Remove this touch from ignored
|
||||||
|
this._ignored.splice(this._ignored.indexOf(id), 1);
|
||||||
|
|
||||||
|
// And reset the state if there are no more touches
|
||||||
|
if ((this._ignored.length === 0) &&
|
||||||
|
(this._tracked.length === 0)) {
|
||||||
|
this._state = GH_INITSTATE;
|
||||||
|
this._waitingRelease = false;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We got a touchend before the timer triggered,
|
||||||
|
// this cannot result in a gesture anymore.
|
||||||
|
if (!this._hasDetectedGesture() &&
|
||||||
|
this._isTwoTouchTimeoutRunning()) {
|
||||||
|
this._stopTwoTouchTimeout();
|
||||||
|
this._state = GH_NOGESTURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some gestures don't trigger until a touch is released
|
||||||
|
if (!this._hasDetectedGesture()) {
|
||||||
|
// Can't be a gesture that relies on movement
|
||||||
|
this._state &= ~(GH_DRAG | GH_TWODRAG | GH_PINCH);
|
||||||
|
// Or something that relies on more time
|
||||||
|
this._state &= ~GH_LONGPRESS;
|
||||||
|
this._stopLongpressTimeout();
|
||||||
|
|
||||||
|
if (!this._waitingRelease) {
|
||||||
|
this._releaseStart = Date.now();
|
||||||
|
this._waitingRelease = true;
|
||||||
|
|
||||||
|
// Can't be a tap that requires more touches than we current have
|
||||||
|
switch (this._tracked.length) {
|
||||||
|
case 1:
|
||||||
|
this._state &= ~(GH_TWOTAP | GH_THREETAP);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
this._state &= ~(GH_ONETAP | GH_THREETAP);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waiting for all touches to release? (i.e. some tap)
|
||||||
|
if (this._waitingRelease) {
|
||||||
|
// Were all touches released at roughly the same time?
|
||||||
|
if ((Date.now() - this._releaseStart) > GH_MULTITOUCH_TIMEOUT) {
|
||||||
|
this._state = GH_NOGESTURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Did too long time pass between press and release?
|
||||||
|
if (this._tracked.some(t => (Date.now() - t.started) > GH_TAP_TIMEOUT)) {
|
||||||
|
this._state = GH_NOGESTURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
let touch = this._tracked.find(t => t.id === id);
|
||||||
|
touch.active = false;
|
||||||
|
|
||||||
|
// Are we still waiting for more releases?
|
||||||
|
if (this._hasDetectedGesture()) {
|
||||||
|
this._pushEvent('gesturestart');
|
||||||
|
} else {
|
||||||
|
// Have we reached a dead end?
|
||||||
|
if (this._state !== GH_NOGESTURE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._hasDetectedGesture()) {
|
||||||
|
this._pushEvent('gestureend');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore any remaining touches until they are ended
|
||||||
|
for (let i = 0; i < this._tracked.length; i++) {
|
||||||
|
if (this._tracked[i].active) {
|
||||||
|
this._ignored.push(this._tracked[i].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this._tracked = [];
|
||||||
|
|
||||||
|
this._state = GH_NOGESTURE;
|
||||||
|
|
||||||
|
// Remove this touch from ignored if it's in there
|
||||||
|
if (this._ignored.indexOf(id) !== -1) {
|
||||||
|
this._ignored.splice(this._ignored.indexOf(id), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We reset the state if ignored is empty
|
||||||
|
if ((this._ignored.length === 0)) {
|
||||||
|
this._state = GH_INITSTATE;
|
||||||
|
this._waitingRelease = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hasDetectedGesture() {
|
||||||
|
if (this._state === GH_NOGESTURE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check to see if the bitmask value is a power of 2
|
||||||
|
// (i.e. only one bit set). If it is, we have a state.
|
||||||
|
if (this._state & (this._state - 1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For taps we also need to have all touches released
|
||||||
|
// before we've fully detected the gesture
|
||||||
|
if (this._state & (GH_ONETAP | GH_TWOTAP | GH_THREETAP)) {
|
||||||
|
if (this._tracked.some(t => t.active)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_startLongpressTimeout() {
|
||||||
|
this._stopLongpressTimeout();
|
||||||
|
this._longpressTimeoutId = setTimeout(() => this._longpressTimeout(),
|
||||||
|
GH_LONGPRESS_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopLongpressTimeout() {
|
||||||
|
clearTimeout(this._longpressTimeoutId);
|
||||||
|
this._longpressTimeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_longpressTimeout() {
|
||||||
|
if (this._hasDetectedGesture()) {
|
||||||
|
throw new Error("A longpress gesture failed, conflict with a different gesture");
|
||||||
|
}
|
||||||
|
|
||||||
|
this._state = GH_LONGPRESS;
|
||||||
|
this._pushEvent('gesturestart');
|
||||||
|
}
|
||||||
|
|
||||||
|
_startTwoTouchTimeout() {
|
||||||
|
this._stopTwoTouchTimeout();
|
||||||
|
this._twoTouchTimeoutId = setTimeout(() => this._twoTouchTimeout(),
|
||||||
|
GH_TWOTOUCH_TIMEOUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
_stopTwoTouchTimeout() {
|
||||||
|
clearTimeout(this._twoTouchTimeoutId);
|
||||||
|
this._twoTouchTimeoutId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_isTwoTouchTimeoutRunning() {
|
||||||
|
return this._twoTouchTimeoutId !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
_twoTouchTimeout() {
|
||||||
|
if (this._tracked.length === 0) {
|
||||||
|
throw new Error("A pinch or two drag gesture failed, no tracked touches");
|
||||||
|
}
|
||||||
|
|
||||||
|
// How far each touch point has moved since start
|
||||||
|
let avgM = this._getAverageMovement();
|
||||||
|
let avgMoveH = Math.abs(avgM.x);
|
||||||
|
let avgMoveV = Math.abs(avgM.y);
|
||||||
|
|
||||||
|
// The difference in the distance between where
|
||||||
|
// the touch points started and where they are now
|
||||||
|
let avgD = this._getAverageDistance();
|
||||||
|
let deltaTouchDistance = Math.abs(Math.hypot(avgD.first.x, avgD.first.y) -
|
||||||
|
Math.hypot(avgD.last.x, avgD.last.y));
|
||||||
|
|
||||||
|
if ((avgMoveV < deltaTouchDistance) &&
|
||||||
|
(avgMoveH < deltaTouchDistance)) {
|
||||||
|
this._state = GH_PINCH;
|
||||||
|
} else {
|
||||||
|
this._state = GH_TWODRAG;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._pushEvent('gesturestart');
|
||||||
|
this._pushEvent('gesturemove');
|
||||||
|
}
|
||||||
|
|
||||||
|
_pushEvent(type) {
|
||||||
|
let detail = { type: this._stateToGesture(this._state) };
|
||||||
|
|
||||||
|
// For most gesture events the current (average) position is the
|
||||||
|
// most useful
|
||||||
|
let avg = this._getPosition();
|
||||||
|
let pos = avg.last;
|
||||||
|
|
||||||
|
// However we have a slight distance to detect gestures, so for the
|
||||||
|
// first gesture event we want to use the first positions we saw
|
||||||
|
if (type === 'gesturestart') {
|
||||||
|
pos = avg.first;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For these gestures, we always want the event coordinates
|
||||||
|
// to be where the gesture began, not the current touch location.
|
||||||
|
switch (this._state) {
|
||||||
|
case GH_TWODRAG:
|
||||||
|
case GH_PINCH:
|
||||||
|
pos = avg.first;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
detail['clientX'] = pos.x;
|
||||||
|
detail['clientY'] = pos.y;
|
||||||
|
|
||||||
|
// FIXME: other coordinates?
|
||||||
|
|
||||||
|
// Some gestures also have a magnitude
|
||||||
|
if (this._state === GH_PINCH) {
|
||||||
|
let distance = this._getAverageDistance();
|
||||||
|
if (type === 'gesturestart') {
|
||||||
|
detail['magnitudeX'] = distance.first.x;
|
||||||
|
detail['magnitudeY'] = distance.first.y;
|
||||||
|
} else {
|
||||||
|
detail['magnitudeX'] = distance.last.x;
|
||||||
|
detail['magnitudeY'] = distance.last.y;
|
||||||
|
}
|
||||||
|
} else if (this._state === GH_TWODRAG) {
|
||||||
|
if (type === 'gesturestart') {
|
||||||
|
detail['magnitudeX'] = 0.0;
|
||||||
|
detail['magnitudeY'] = 0.0;
|
||||||
|
} else {
|
||||||
|
let movement = this._getAverageMovement();
|
||||||
|
detail['magnitudeX'] = movement.x;
|
||||||
|
detail['magnitudeY'] = movement.y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let gev = new CustomEvent(type, { detail: detail });
|
||||||
|
this._target.dispatchEvent(gev);
|
||||||
|
}
|
||||||
|
|
||||||
|
_stateToGesture(state) {
|
||||||
|
switch (state) {
|
||||||
|
case GH_ONETAP:
|
||||||
|
return 'onetap';
|
||||||
|
case GH_TWOTAP:
|
||||||
|
return 'twotap';
|
||||||
|
case GH_THREETAP:
|
||||||
|
return 'threetap';
|
||||||
|
case GH_DRAG:
|
||||||
|
return 'drag';
|
||||||
|
case GH_LONGPRESS:
|
||||||
|
return 'longpress';
|
||||||
|
case GH_TWODRAG:
|
||||||
|
return 'twodrag';
|
||||||
|
case GH_PINCH:
|
||||||
|
return 'pinch';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error("Unknown gesture state: " + state);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getPosition() {
|
||||||
|
if (this._tracked.length === 0) {
|
||||||
|
throw new Error("Failed to get gesture position, no tracked touches");
|
||||||
|
}
|
||||||
|
|
||||||
|
let size = this._tracked.length;
|
||||||
|
let fx = 0, fy = 0, lx = 0, ly = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < this._tracked.length; i++) {
|
||||||
|
fx += this._tracked[i].firstX;
|
||||||
|
fy += this._tracked[i].firstY;
|
||||||
|
lx += this._tracked[i].lastX;
|
||||||
|
ly += this._tracked[i].lastY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { first: { x: fx / size,
|
||||||
|
y: fy / size },
|
||||||
|
last: { x: lx / size,
|
||||||
|
y: ly / size } };
|
||||||
|
}
|
||||||
|
|
||||||
|
_getAverageMovement() {
|
||||||
|
if (this._tracked.length === 0) {
|
||||||
|
throw new Error("Failed to get gesture movement, no tracked touches");
|
||||||
|
}
|
||||||
|
|
||||||
|
let totalH, totalV;
|
||||||
|
totalH = totalV = 0;
|
||||||
|
let size = this._tracked.length;
|
||||||
|
|
||||||
|
for (let i = 0; i < this._tracked.length; i++) {
|
||||||
|
totalH += this._tracked[i].lastX - this._tracked[i].firstX;
|
||||||
|
totalV += this._tracked[i].lastY - this._tracked[i].firstY;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { x: totalH / size,
|
||||||
|
y: totalV / size };
|
||||||
|
}
|
||||||
|
|
||||||
|
_getAverageDistance() {
|
||||||
|
if (this._tracked.length === 0) {
|
||||||
|
throw new Error("Failed to get gesture distance, no tracked touches");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Distance between the first and last tracked touches
|
||||||
|
|
||||||
|
let first = this._tracked[0];
|
||||||
|
let last = this._tracked[this._tracked.length - 1];
|
||||||
|
|
||||||
|
let fdx = Math.abs(last.firstX - first.firstX);
|
||||||
|
let fdy = Math.abs(last.firstY - first.firstY);
|
||||||
|
|
||||||
|
let ldx = Math.abs(last.lastX - first.lastX);
|
||||||
|
let ldy = Math.abs(last.lastY - first.lastY);
|
||||||
|
|
||||||
|
return { first: { x: fdx, y: fdy },
|
||||||
|
last: { x: ldx, y: ldy } };
|
||||||
|
}
|
||||||
|
}
|
||||||
+294
@@ -0,0 +1,294 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Log from '../util/logging.js';
|
||||||
|
import { stopEvent } from '../util/events.js';
|
||||||
|
import * as KeyboardUtil from "./util.js";
|
||||||
|
import KeyTable from "./keysym.js";
|
||||||
|
import * as browser from "../util/browser.js";
|
||||||
|
|
||||||
|
//
|
||||||
|
// Keyboard event handler
|
||||||
|
//
|
||||||
|
|
||||||
|
export default class Keyboard {
|
||||||
|
constructor(target) {
|
||||||
|
this._target = target || null;
|
||||||
|
|
||||||
|
this._keyDownList = {}; // List of depressed keys
|
||||||
|
// (even if they are happy)
|
||||||
|
this._altGrArmed = false; // Windows AltGr detection
|
||||||
|
|
||||||
|
// keep these here so we can refer to them later
|
||||||
|
this._eventHandlers = {
|
||||||
|
'keyup': this._handleKeyUp.bind(this),
|
||||||
|
'keydown': this._handleKeyDown.bind(this),
|
||||||
|
'blur': this._allKeysUp.bind(this),
|
||||||
|
};
|
||||||
|
|
||||||
|
// ===== EVENT HANDLERS =====
|
||||||
|
|
||||||
|
this.onkeyevent = () => {}; // Handler for key press/release
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== PRIVATE METHODS =====
|
||||||
|
|
||||||
|
_sendKeyEvent(keysym, code, down, numlock = null, capslock = null) {
|
||||||
|
if (down) {
|
||||||
|
this._keyDownList[code] = keysym;
|
||||||
|
} else {
|
||||||
|
// Do we really think this key is down?
|
||||||
|
if (!(code in this._keyDownList)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
delete this._keyDownList[code];
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.Debug("onkeyevent " + (down ? "down" : "up") +
|
||||||
|
", keysym: " + keysym, ", code: " + code +
|
||||||
|
", numlock: " + numlock + ", capslock: " + capslock);
|
||||||
|
this.onkeyevent(keysym, code, down, numlock, capslock);
|
||||||
|
}
|
||||||
|
|
||||||
|
_getKeyCode(e) {
|
||||||
|
const code = KeyboardUtil.getKeycode(e);
|
||||||
|
if (code !== 'Unidentified') {
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unstable, but we don't have anything else to go on
|
||||||
|
if (e.keyCode) {
|
||||||
|
// 229 is used for composition events
|
||||||
|
if (e.keyCode !== 229) {
|
||||||
|
return 'Platform' + e.keyCode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A precursor to the final DOM3 standard. Unfortunately it
|
||||||
|
// is not layout independent, so it is as bad as using keyCode
|
||||||
|
if (e.keyIdentifier) {
|
||||||
|
// Non-character key?
|
||||||
|
if (e.keyIdentifier.substr(0, 2) !== 'U+') {
|
||||||
|
return e.keyIdentifier;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codepoint = parseInt(e.keyIdentifier.substr(2), 16);
|
||||||
|
const char = String.fromCharCode(codepoint).toUpperCase();
|
||||||
|
|
||||||
|
return 'Platform' + char.charCodeAt();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unidentified';
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleKeyDown(e) {
|
||||||
|
const code = this._getKeyCode(e);
|
||||||
|
let keysym = KeyboardUtil.getKeysym(e);
|
||||||
|
let numlock = e.getModifierState('NumLock');
|
||||||
|
let capslock = e.getModifierState('CapsLock');
|
||||||
|
|
||||||
|
// getModifierState for NumLock is not supported on mac and ios and always returns false.
|
||||||
|
// Set to null to indicate unknown/unsupported instead.
|
||||||
|
if (browser.isMac() || browser.isIOS()) {
|
||||||
|
numlock = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows doesn't have a proper AltGr, but handles it using
|
||||||
|
// fake Ctrl+Alt. However the remote end might not be Windows,
|
||||||
|
// so we need to merge those in to a single AltGr event. We
|
||||||
|
// detect this case by seeing the two key events directly after
|
||||||
|
// each other with a very short time between them (<50ms).
|
||||||
|
if (this._altGrArmed) {
|
||||||
|
this._altGrArmed = false;
|
||||||
|
clearTimeout(this._altGrTimeout);
|
||||||
|
|
||||||
|
if ((code === "AltRight") &&
|
||||||
|
((e.timeStamp - this._altGrCtrlTime) < 50)) {
|
||||||
|
// FIXME: We fail to detect this if either Ctrl key is
|
||||||
|
// first manually pressed as Windows then no
|
||||||
|
// longer sends the fake Ctrl down event. It
|
||||||
|
// does however happily send real Ctrl events
|
||||||
|
// even when AltGr is already down. Some
|
||||||
|
// browsers detect this for us though and set the
|
||||||
|
// key to "AltGraph".
|
||||||
|
keysym = KeyTable.XK_ISO_Level3_Shift;
|
||||||
|
} else {
|
||||||
|
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true, numlock, capslock);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We cannot handle keys we cannot track, but we also need
|
||||||
|
// to deal with virtual keyboards which omit key info
|
||||||
|
if (code === 'Unidentified') {
|
||||||
|
if (keysym) {
|
||||||
|
// If it's a virtual keyboard then it should be
|
||||||
|
// sufficient to just send press and release right
|
||||||
|
// after each other
|
||||||
|
this._sendKeyEvent(keysym, code, true, numlock, capslock);
|
||||||
|
this._sendKeyEvent(keysym, code, false, numlock, capslock);
|
||||||
|
}
|
||||||
|
|
||||||
|
stopEvent(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alt behaves more like AltGraph on macOS, so shuffle the
|
||||||
|
// keys around a bit to make things more sane for the remote
|
||||||
|
// server. This method is used by RealVNC and TigerVNC (and
|
||||||
|
// possibly others).
|
||||||
|
if (browser.isMac() || browser.isIOS()) {
|
||||||
|
switch (keysym) {
|
||||||
|
case KeyTable.XK_Super_L:
|
||||||
|
keysym = KeyTable.XK_Alt_L;
|
||||||
|
break;
|
||||||
|
case KeyTable.XK_Super_R:
|
||||||
|
keysym = KeyTable.XK_Super_L;
|
||||||
|
break;
|
||||||
|
case KeyTable.XK_Alt_L:
|
||||||
|
keysym = KeyTable.XK_Mode_switch;
|
||||||
|
break;
|
||||||
|
case KeyTable.XK_Alt_R:
|
||||||
|
keysym = KeyTable.XK_ISO_Level3_Shift;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is this key already pressed? If so, then we must use the
|
||||||
|
// same keysym or we'll confuse the server
|
||||||
|
if (code in this._keyDownList) {
|
||||||
|
keysym = this._keyDownList[code];
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS doesn't send proper key releases if a key is pressed
|
||||||
|
// while meta is held down
|
||||||
|
if ((browser.isMac() || browser.isIOS()) &&
|
||||||
|
(e.metaKey && code !== 'MetaLeft' && code !== 'MetaRight')) {
|
||||||
|
this._sendKeyEvent(keysym, code, true, numlock, capslock);
|
||||||
|
this._sendKeyEvent(keysym, code, false, numlock, capslock);
|
||||||
|
stopEvent(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS doesn't send proper key events for modifiers, only
|
||||||
|
// state change events. That gets extra confusing for CapsLock
|
||||||
|
// which toggles on each press, but not on release. So pretend
|
||||||
|
// it was a quick press and release of the button.
|
||||||
|
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
|
||||||
|
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true, numlock, capslock);
|
||||||
|
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false, numlock, capslock);
|
||||||
|
stopEvent(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows doesn't send proper key releases for a bunch of
|
||||||
|
// Japanese IM keys so we have to fake the release right away
|
||||||
|
const jpBadKeys = [ KeyTable.XK_Zenkaku_Hankaku,
|
||||||
|
KeyTable.XK_Eisu_toggle,
|
||||||
|
KeyTable.XK_Katakana,
|
||||||
|
KeyTable.XK_Hiragana,
|
||||||
|
KeyTable.XK_Romaji ];
|
||||||
|
if (browser.isWindows() && jpBadKeys.includes(keysym)) {
|
||||||
|
this._sendKeyEvent(keysym, code, true, numlock, capslock);
|
||||||
|
this._sendKeyEvent(keysym, code, false, numlock, capslock);
|
||||||
|
stopEvent(e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stopEvent(e);
|
||||||
|
|
||||||
|
// Possible start of AltGr sequence? (see above)
|
||||||
|
if ((code === "ControlLeft") && browser.isWindows() &&
|
||||||
|
!("ControlLeft" in this._keyDownList)) {
|
||||||
|
this._altGrArmed = true;
|
||||||
|
this._altGrTimeout = setTimeout(this._interruptAltGrSequence.bind(this), 100);
|
||||||
|
this._altGrCtrlTime = e.timeStamp;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sendKeyEvent(keysym, code, true, numlock, capslock);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleKeyUp(e) {
|
||||||
|
stopEvent(e);
|
||||||
|
|
||||||
|
const code = this._getKeyCode(e);
|
||||||
|
|
||||||
|
// We can't get a release in the middle of an AltGr sequence, so
|
||||||
|
// abort that detection
|
||||||
|
this._interruptAltGrSequence();
|
||||||
|
|
||||||
|
// See comment in _handleKeyDown()
|
||||||
|
if ((browser.isMac() || browser.isIOS()) && (code === 'CapsLock')) {
|
||||||
|
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true);
|
||||||
|
this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sendKeyEvent(this._keyDownList[code], code, false);
|
||||||
|
|
||||||
|
// Windows has a rather nasty bug where it won't send key
|
||||||
|
// release events for a Shift button if the other Shift is still
|
||||||
|
// pressed
|
||||||
|
if (browser.isWindows() && ((code === 'ShiftLeft') ||
|
||||||
|
(code === 'ShiftRight'))) {
|
||||||
|
if ('ShiftRight' in this._keyDownList) {
|
||||||
|
this._sendKeyEvent(this._keyDownList['ShiftRight'],
|
||||||
|
'ShiftRight', false);
|
||||||
|
}
|
||||||
|
if ('ShiftLeft' in this._keyDownList) {
|
||||||
|
this._sendKeyEvent(this._keyDownList['ShiftLeft'],
|
||||||
|
'ShiftLeft', false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_interruptAltGrSequence() {
|
||||||
|
if (this._altGrArmed) {
|
||||||
|
this._altGrArmed = false;
|
||||||
|
clearTimeout(this._altGrTimeout);
|
||||||
|
this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_allKeysUp() {
|
||||||
|
Log.Debug(">> Keyboard.allKeysUp");
|
||||||
|
|
||||||
|
// Prevent control key being processed after losing focus.
|
||||||
|
this._interruptAltGrSequence();
|
||||||
|
|
||||||
|
for (let code in this._keyDownList) {
|
||||||
|
this._sendKeyEvent(this._keyDownList[code], code, false);
|
||||||
|
}
|
||||||
|
Log.Debug("<< Keyboard.allKeysUp");
|
||||||
|
}
|
||||||
|
|
||||||
|
// ===== PUBLIC METHODS =====
|
||||||
|
|
||||||
|
grab() {
|
||||||
|
//Log.Debug(">> Keyboard.grab");
|
||||||
|
|
||||||
|
this._target.addEventListener('keydown', this._eventHandlers.keydown);
|
||||||
|
this._target.addEventListener('keyup', this._eventHandlers.keyup);
|
||||||
|
|
||||||
|
// Release (key up) if window loses focus
|
||||||
|
window.addEventListener('blur', this._eventHandlers.blur);
|
||||||
|
|
||||||
|
//Log.Debug("<< Keyboard.grab");
|
||||||
|
}
|
||||||
|
|
||||||
|
ungrab() {
|
||||||
|
//Log.Debug(">> Keyboard.ungrab");
|
||||||
|
|
||||||
|
this._target.removeEventListener('keydown', this._eventHandlers.keydown);
|
||||||
|
this._target.removeEventListener('keyup', this._eventHandlers.keyup);
|
||||||
|
window.removeEventListener('blur', this._eventHandlers.blur);
|
||||||
|
|
||||||
|
// Release (key up) all keys that are in a down state
|
||||||
|
this._allKeysUp();
|
||||||
|
|
||||||
|
//Log.Debug(">> Keyboard.ungrab");
|
||||||
|
}
|
||||||
|
}
|
||||||
+616
@@ -0,0 +1,616 @@
|
|||||||
|
/* eslint-disable key-spacing */
|
||||||
|
|
||||||
|
export default {
|
||||||
|
XK_VoidSymbol: 0xffffff, /* Void symbol */
|
||||||
|
|
||||||
|
XK_BackSpace: 0xff08, /* Back space, back char */
|
||||||
|
XK_Tab: 0xff09,
|
||||||
|
XK_Linefeed: 0xff0a, /* Linefeed, LF */
|
||||||
|
XK_Clear: 0xff0b,
|
||||||
|
XK_Return: 0xff0d, /* Return, enter */
|
||||||
|
XK_Pause: 0xff13, /* Pause, hold */
|
||||||
|
XK_Scroll_Lock: 0xff14,
|
||||||
|
XK_Sys_Req: 0xff15,
|
||||||
|
XK_Escape: 0xff1b,
|
||||||
|
XK_Delete: 0xffff, /* Delete, rubout */
|
||||||
|
|
||||||
|
/* International & multi-key character composition */
|
||||||
|
|
||||||
|
XK_Multi_key: 0xff20, /* Multi-key character compose */
|
||||||
|
XK_Codeinput: 0xff37,
|
||||||
|
XK_SingleCandidate: 0xff3c,
|
||||||
|
XK_MultipleCandidate: 0xff3d,
|
||||||
|
XK_PreviousCandidate: 0xff3e,
|
||||||
|
|
||||||
|
/* Japanese keyboard support */
|
||||||
|
|
||||||
|
XK_Kanji: 0xff21, /* Kanji, Kanji convert */
|
||||||
|
XK_Muhenkan: 0xff22, /* Cancel Conversion */
|
||||||
|
XK_Henkan_Mode: 0xff23, /* Start/Stop Conversion */
|
||||||
|
XK_Henkan: 0xff23, /* Alias for Henkan_Mode */
|
||||||
|
XK_Romaji: 0xff24, /* to Romaji */
|
||||||
|
XK_Hiragana: 0xff25, /* to Hiragana */
|
||||||
|
XK_Katakana: 0xff26, /* to Katakana */
|
||||||
|
XK_Hiragana_Katakana: 0xff27, /* Hiragana/Katakana toggle */
|
||||||
|
XK_Zenkaku: 0xff28, /* to Zenkaku */
|
||||||
|
XK_Hankaku: 0xff29, /* to Hankaku */
|
||||||
|
XK_Zenkaku_Hankaku: 0xff2a, /* Zenkaku/Hankaku toggle */
|
||||||
|
XK_Touroku: 0xff2b, /* Add to Dictionary */
|
||||||
|
XK_Massyo: 0xff2c, /* Delete from Dictionary */
|
||||||
|
XK_Kana_Lock: 0xff2d, /* Kana Lock */
|
||||||
|
XK_Kana_Shift: 0xff2e, /* Kana Shift */
|
||||||
|
XK_Eisu_Shift: 0xff2f, /* Alphanumeric Shift */
|
||||||
|
XK_Eisu_toggle: 0xff30, /* Alphanumeric toggle */
|
||||||
|
XK_Kanji_Bangou: 0xff37, /* Codeinput */
|
||||||
|
XK_Zen_Koho: 0xff3d, /* Multiple/All Candidate(s) */
|
||||||
|
XK_Mae_Koho: 0xff3e, /* Previous Candidate */
|
||||||
|
|
||||||
|
/* Cursor control & motion */
|
||||||
|
|
||||||
|
XK_Home: 0xff50,
|
||||||
|
XK_Left: 0xff51, /* Move left, left arrow */
|
||||||
|
XK_Up: 0xff52, /* Move up, up arrow */
|
||||||
|
XK_Right: 0xff53, /* Move right, right arrow */
|
||||||
|
XK_Down: 0xff54, /* Move down, down arrow */
|
||||||
|
XK_Prior: 0xff55, /* Prior, previous */
|
||||||
|
XK_Page_Up: 0xff55,
|
||||||
|
XK_Next: 0xff56, /* Next */
|
||||||
|
XK_Page_Down: 0xff56,
|
||||||
|
XK_End: 0xff57, /* EOL */
|
||||||
|
XK_Begin: 0xff58, /* BOL */
|
||||||
|
|
||||||
|
|
||||||
|
/* Misc functions */
|
||||||
|
|
||||||
|
XK_Select: 0xff60, /* Select, mark */
|
||||||
|
XK_Print: 0xff61,
|
||||||
|
XK_Execute: 0xff62, /* Execute, run, do */
|
||||||
|
XK_Insert: 0xff63, /* Insert, insert here */
|
||||||
|
XK_Undo: 0xff65,
|
||||||
|
XK_Redo: 0xff66, /* Redo, again */
|
||||||
|
XK_Menu: 0xff67,
|
||||||
|
XK_Find: 0xff68, /* Find, search */
|
||||||
|
XK_Cancel: 0xff69, /* Cancel, stop, abort, exit */
|
||||||
|
XK_Help: 0xff6a, /* Help */
|
||||||
|
XK_Break: 0xff6b,
|
||||||
|
XK_Mode_switch: 0xff7e, /* Character set switch */
|
||||||
|
XK_script_switch: 0xff7e, /* Alias for mode_switch */
|
||||||
|
XK_Num_Lock: 0xff7f,
|
||||||
|
|
||||||
|
/* Keypad functions, keypad numbers cleverly chosen to map to ASCII */
|
||||||
|
|
||||||
|
XK_KP_Space: 0xff80, /* Space */
|
||||||
|
XK_KP_Tab: 0xff89,
|
||||||
|
XK_KP_Enter: 0xff8d, /* Enter */
|
||||||
|
XK_KP_F1: 0xff91, /* PF1, KP_A, ... */
|
||||||
|
XK_KP_F2: 0xff92,
|
||||||
|
XK_KP_F3: 0xff93,
|
||||||
|
XK_KP_F4: 0xff94,
|
||||||
|
XK_KP_Home: 0xff95,
|
||||||
|
XK_KP_Left: 0xff96,
|
||||||
|
XK_KP_Up: 0xff97,
|
||||||
|
XK_KP_Right: 0xff98,
|
||||||
|
XK_KP_Down: 0xff99,
|
||||||
|
XK_KP_Prior: 0xff9a,
|
||||||
|
XK_KP_Page_Up: 0xff9a,
|
||||||
|
XK_KP_Next: 0xff9b,
|
||||||
|
XK_KP_Page_Down: 0xff9b,
|
||||||
|
XK_KP_End: 0xff9c,
|
||||||
|
XK_KP_Begin: 0xff9d,
|
||||||
|
XK_KP_Insert: 0xff9e,
|
||||||
|
XK_KP_Delete: 0xff9f,
|
||||||
|
XK_KP_Equal: 0xffbd, /* Equals */
|
||||||
|
XK_KP_Multiply: 0xffaa,
|
||||||
|
XK_KP_Add: 0xffab,
|
||||||
|
XK_KP_Separator: 0xffac, /* Separator, often comma */
|
||||||
|
XK_KP_Subtract: 0xffad,
|
||||||
|
XK_KP_Decimal: 0xffae,
|
||||||
|
XK_KP_Divide: 0xffaf,
|
||||||
|
|
||||||
|
XK_KP_0: 0xffb0,
|
||||||
|
XK_KP_1: 0xffb1,
|
||||||
|
XK_KP_2: 0xffb2,
|
||||||
|
XK_KP_3: 0xffb3,
|
||||||
|
XK_KP_4: 0xffb4,
|
||||||
|
XK_KP_5: 0xffb5,
|
||||||
|
XK_KP_6: 0xffb6,
|
||||||
|
XK_KP_7: 0xffb7,
|
||||||
|
XK_KP_8: 0xffb8,
|
||||||
|
XK_KP_9: 0xffb9,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Auxiliary functions; note the duplicate definitions for left and right
|
||||||
|
* function keys; Sun keyboards and a few other manufacturers have such
|
||||||
|
* function key groups on the left and/or right sides of the keyboard.
|
||||||
|
* We've not found a keyboard with more than 35 function keys total.
|
||||||
|
*/
|
||||||
|
|
||||||
|
XK_F1: 0xffbe,
|
||||||
|
XK_F2: 0xffbf,
|
||||||
|
XK_F3: 0xffc0,
|
||||||
|
XK_F4: 0xffc1,
|
||||||
|
XK_F5: 0xffc2,
|
||||||
|
XK_F6: 0xffc3,
|
||||||
|
XK_F7: 0xffc4,
|
||||||
|
XK_F8: 0xffc5,
|
||||||
|
XK_F9: 0xffc6,
|
||||||
|
XK_F10: 0xffc7,
|
||||||
|
XK_F11: 0xffc8,
|
||||||
|
XK_L1: 0xffc8,
|
||||||
|
XK_F12: 0xffc9,
|
||||||
|
XK_L2: 0xffc9,
|
||||||
|
XK_F13: 0xffca,
|
||||||
|
XK_L3: 0xffca,
|
||||||
|
XK_F14: 0xffcb,
|
||||||
|
XK_L4: 0xffcb,
|
||||||
|
XK_F15: 0xffcc,
|
||||||
|
XK_L5: 0xffcc,
|
||||||
|
XK_F16: 0xffcd,
|
||||||
|
XK_L6: 0xffcd,
|
||||||
|
XK_F17: 0xffce,
|
||||||
|
XK_L7: 0xffce,
|
||||||
|
XK_F18: 0xffcf,
|
||||||
|
XK_L8: 0xffcf,
|
||||||
|
XK_F19: 0xffd0,
|
||||||
|
XK_L9: 0xffd0,
|
||||||
|
XK_F20: 0xffd1,
|
||||||
|
XK_L10: 0xffd1,
|
||||||
|
XK_F21: 0xffd2,
|
||||||
|
XK_R1: 0xffd2,
|
||||||
|
XK_F22: 0xffd3,
|
||||||
|
XK_R2: 0xffd3,
|
||||||
|
XK_F23: 0xffd4,
|
||||||
|
XK_R3: 0xffd4,
|
||||||
|
XK_F24: 0xffd5,
|
||||||
|
XK_R4: 0xffd5,
|
||||||
|
XK_F25: 0xffd6,
|
||||||
|
XK_R5: 0xffd6,
|
||||||
|
XK_F26: 0xffd7,
|
||||||
|
XK_R6: 0xffd7,
|
||||||
|
XK_F27: 0xffd8,
|
||||||
|
XK_R7: 0xffd8,
|
||||||
|
XK_F28: 0xffd9,
|
||||||
|
XK_R8: 0xffd9,
|
||||||
|
XK_F29: 0xffda,
|
||||||
|
XK_R9: 0xffda,
|
||||||
|
XK_F30: 0xffdb,
|
||||||
|
XK_R10: 0xffdb,
|
||||||
|
XK_F31: 0xffdc,
|
||||||
|
XK_R11: 0xffdc,
|
||||||
|
XK_F32: 0xffdd,
|
||||||
|
XK_R12: 0xffdd,
|
||||||
|
XK_F33: 0xffde,
|
||||||
|
XK_R13: 0xffde,
|
||||||
|
XK_F34: 0xffdf,
|
||||||
|
XK_R14: 0xffdf,
|
||||||
|
XK_F35: 0xffe0,
|
||||||
|
XK_R15: 0xffe0,
|
||||||
|
|
||||||
|
/* Modifiers */
|
||||||
|
|
||||||
|
XK_Shift_L: 0xffe1, /* Left shift */
|
||||||
|
XK_Shift_R: 0xffe2, /* Right shift */
|
||||||
|
XK_Control_L: 0xffe3, /* Left control */
|
||||||
|
XK_Control_R: 0xffe4, /* Right control */
|
||||||
|
XK_Caps_Lock: 0xffe5, /* Caps lock */
|
||||||
|
XK_Shift_Lock: 0xffe6, /* Shift lock */
|
||||||
|
|
||||||
|
XK_Meta_L: 0xffe7, /* Left meta */
|
||||||
|
XK_Meta_R: 0xffe8, /* Right meta */
|
||||||
|
XK_Alt_L: 0xffe9, /* Left alt */
|
||||||
|
XK_Alt_R: 0xffea, /* Right alt */
|
||||||
|
XK_Super_L: 0xffeb, /* Left super */
|
||||||
|
XK_Super_R: 0xffec, /* Right super */
|
||||||
|
XK_Hyper_L: 0xffed, /* Left hyper */
|
||||||
|
XK_Hyper_R: 0xffee, /* Right hyper */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Keyboard (XKB) Extension function and modifier keys
|
||||||
|
* (from Appendix C of "The X Keyboard Extension: Protocol Specification")
|
||||||
|
* Byte 3 = 0xfe
|
||||||
|
*/
|
||||||
|
|
||||||
|
XK_ISO_Level3_Shift: 0xfe03, /* AltGr */
|
||||||
|
XK_ISO_Next_Group: 0xfe08,
|
||||||
|
XK_ISO_Prev_Group: 0xfe0a,
|
||||||
|
XK_ISO_First_Group: 0xfe0c,
|
||||||
|
XK_ISO_Last_Group: 0xfe0e,
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Latin 1
|
||||||
|
* (ISO/IEC 8859-1: Unicode U+0020..U+00FF)
|
||||||
|
* Byte 3: 0
|
||||||
|
*/
|
||||||
|
|
||||||
|
XK_space: 0x0020, /* U+0020 SPACE */
|
||||||
|
XK_exclam: 0x0021, /* U+0021 EXCLAMATION MARK */
|
||||||
|
XK_quotedbl: 0x0022, /* U+0022 QUOTATION MARK */
|
||||||
|
XK_numbersign: 0x0023, /* U+0023 NUMBER SIGN */
|
||||||
|
XK_dollar: 0x0024, /* U+0024 DOLLAR SIGN */
|
||||||
|
XK_percent: 0x0025, /* U+0025 PERCENT SIGN */
|
||||||
|
XK_ampersand: 0x0026, /* U+0026 AMPERSAND */
|
||||||
|
XK_apostrophe: 0x0027, /* U+0027 APOSTROPHE */
|
||||||
|
XK_quoteright: 0x0027, /* deprecated */
|
||||||
|
XK_parenleft: 0x0028, /* U+0028 LEFT PARENTHESIS */
|
||||||
|
XK_parenright: 0x0029, /* U+0029 RIGHT PARENTHESIS */
|
||||||
|
XK_asterisk: 0x002a, /* U+002A ASTERISK */
|
||||||
|
XK_plus: 0x002b, /* U+002B PLUS SIGN */
|
||||||
|
XK_comma: 0x002c, /* U+002C COMMA */
|
||||||
|
XK_minus: 0x002d, /* U+002D HYPHEN-MINUS */
|
||||||
|
XK_period: 0x002e, /* U+002E FULL STOP */
|
||||||
|
XK_slash: 0x002f, /* U+002F SOLIDUS */
|
||||||
|
XK_0: 0x0030, /* U+0030 DIGIT ZERO */
|
||||||
|
XK_1: 0x0031, /* U+0031 DIGIT ONE */
|
||||||
|
XK_2: 0x0032, /* U+0032 DIGIT TWO */
|
||||||
|
XK_3: 0x0033, /* U+0033 DIGIT THREE */
|
||||||
|
XK_4: 0x0034, /* U+0034 DIGIT FOUR */
|
||||||
|
XK_5: 0x0035, /* U+0035 DIGIT FIVE */
|
||||||
|
XK_6: 0x0036, /* U+0036 DIGIT SIX */
|
||||||
|
XK_7: 0x0037, /* U+0037 DIGIT SEVEN */
|
||||||
|
XK_8: 0x0038, /* U+0038 DIGIT EIGHT */
|
||||||
|
XK_9: 0x0039, /* U+0039 DIGIT NINE */
|
||||||
|
XK_colon: 0x003a, /* U+003A COLON */
|
||||||
|
XK_semicolon: 0x003b, /* U+003B SEMICOLON */
|
||||||
|
XK_less: 0x003c, /* U+003C LESS-THAN SIGN */
|
||||||
|
XK_equal: 0x003d, /* U+003D EQUALS SIGN */
|
||||||
|
XK_greater: 0x003e, /* U+003E GREATER-THAN SIGN */
|
||||||
|
XK_question: 0x003f, /* U+003F QUESTION MARK */
|
||||||
|
XK_at: 0x0040, /* U+0040 COMMERCIAL AT */
|
||||||
|
XK_A: 0x0041, /* U+0041 LATIN CAPITAL LETTER A */
|
||||||
|
XK_B: 0x0042, /* U+0042 LATIN CAPITAL LETTER B */
|
||||||
|
XK_C: 0x0043, /* U+0043 LATIN CAPITAL LETTER C */
|
||||||
|
XK_D: 0x0044, /* U+0044 LATIN CAPITAL LETTER D */
|
||||||
|
XK_E: 0x0045, /* U+0045 LATIN CAPITAL LETTER E */
|
||||||
|
XK_F: 0x0046, /* U+0046 LATIN CAPITAL LETTER F */
|
||||||
|
XK_G: 0x0047, /* U+0047 LATIN CAPITAL LETTER G */
|
||||||
|
XK_H: 0x0048, /* U+0048 LATIN CAPITAL LETTER H */
|
||||||
|
XK_I: 0x0049, /* U+0049 LATIN CAPITAL LETTER I */
|
||||||
|
XK_J: 0x004a, /* U+004A LATIN CAPITAL LETTER J */
|
||||||
|
XK_K: 0x004b, /* U+004B LATIN CAPITAL LETTER K */
|
||||||
|
XK_L: 0x004c, /* U+004C LATIN CAPITAL LETTER L */
|
||||||
|
XK_M: 0x004d, /* U+004D LATIN CAPITAL LETTER M */
|
||||||
|
XK_N: 0x004e, /* U+004E LATIN CAPITAL LETTER N */
|
||||||
|
XK_O: 0x004f, /* U+004F LATIN CAPITAL LETTER O */
|
||||||
|
XK_P: 0x0050, /* U+0050 LATIN CAPITAL LETTER P */
|
||||||
|
XK_Q: 0x0051, /* U+0051 LATIN CAPITAL LETTER Q */
|
||||||
|
XK_R: 0x0052, /* U+0052 LATIN CAPITAL LETTER R */
|
||||||
|
XK_S: 0x0053, /* U+0053 LATIN CAPITAL LETTER S */
|
||||||
|
XK_T: 0x0054, /* U+0054 LATIN CAPITAL LETTER T */
|
||||||
|
XK_U: 0x0055, /* U+0055 LATIN CAPITAL LETTER U */
|
||||||
|
XK_V: 0x0056, /* U+0056 LATIN CAPITAL LETTER V */
|
||||||
|
XK_W: 0x0057, /* U+0057 LATIN CAPITAL LETTER W */
|
||||||
|
XK_X: 0x0058, /* U+0058 LATIN CAPITAL LETTER X */
|
||||||
|
XK_Y: 0x0059, /* U+0059 LATIN CAPITAL LETTER Y */
|
||||||
|
XK_Z: 0x005a, /* U+005A LATIN CAPITAL LETTER Z */
|
||||||
|
XK_bracketleft: 0x005b, /* U+005B LEFT SQUARE BRACKET */
|
||||||
|
XK_backslash: 0x005c, /* U+005C REVERSE SOLIDUS */
|
||||||
|
XK_bracketright: 0x005d, /* U+005D RIGHT SQUARE BRACKET */
|
||||||
|
XK_asciicircum: 0x005e, /* U+005E CIRCUMFLEX ACCENT */
|
||||||
|
XK_underscore: 0x005f, /* U+005F LOW LINE */
|
||||||
|
XK_grave: 0x0060, /* U+0060 GRAVE ACCENT */
|
||||||
|
XK_quoteleft: 0x0060, /* deprecated */
|
||||||
|
XK_a: 0x0061, /* U+0061 LATIN SMALL LETTER A */
|
||||||
|
XK_b: 0x0062, /* U+0062 LATIN SMALL LETTER B */
|
||||||
|
XK_c: 0x0063, /* U+0063 LATIN SMALL LETTER C */
|
||||||
|
XK_d: 0x0064, /* U+0064 LATIN SMALL LETTER D */
|
||||||
|
XK_e: 0x0065, /* U+0065 LATIN SMALL LETTER E */
|
||||||
|
XK_f: 0x0066, /* U+0066 LATIN SMALL LETTER F */
|
||||||
|
XK_g: 0x0067, /* U+0067 LATIN SMALL LETTER G */
|
||||||
|
XK_h: 0x0068, /* U+0068 LATIN SMALL LETTER H */
|
||||||
|
XK_i: 0x0069, /* U+0069 LATIN SMALL LETTER I */
|
||||||
|
XK_j: 0x006a, /* U+006A LATIN SMALL LETTER J */
|
||||||
|
XK_k: 0x006b, /* U+006B LATIN SMALL LETTER K */
|
||||||
|
XK_l: 0x006c, /* U+006C LATIN SMALL LETTER L */
|
||||||
|
XK_m: 0x006d, /* U+006D LATIN SMALL LETTER M */
|
||||||
|
XK_n: 0x006e, /* U+006E LATIN SMALL LETTER N */
|
||||||
|
XK_o: 0x006f, /* U+006F LATIN SMALL LETTER O */
|
||||||
|
XK_p: 0x0070, /* U+0070 LATIN SMALL LETTER P */
|
||||||
|
XK_q: 0x0071, /* U+0071 LATIN SMALL LETTER Q */
|
||||||
|
XK_r: 0x0072, /* U+0072 LATIN SMALL LETTER R */
|
||||||
|
XK_s: 0x0073, /* U+0073 LATIN SMALL LETTER S */
|
||||||
|
XK_t: 0x0074, /* U+0074 LATIN SMALL LETTER T */
|
||||||
|
XK_u: 0x0075, /* U+0075 LATIN SMALL LETTER U */
|
||||||
|
XK_v: 0x0076, /* U+0076 LATIN SMALL LETTER V */
|
||||||
|
XK_w: 0x0077, /* U+0077 LATIN SMALL LETTER W */
|
||||||
|
XK_x: 0x0078, /* U+0078 LATIN SMALL LETTER X */
|
||||||
|
XK_y: 0x0079, /* U+0079 LATIN SMALL LETTER Y */
|
||||||
|
XK_z: 0x007a, /* U+007A LATIN SMALL LETTER Z */
|
||||||
|
XK_braceleft: 0x007b, /* U+007B LEFT CURLY BRACKET */
|
||||||
|
XK_bar: 0x007c, /* U+007C VERTICAL LINE */
|
||||||
|
XK_braceright: 0x007d, /* U+007D RIGHT CURLY BRACKET */
|
||||||
|
XK_asciitilde: 0x007e, /* U+007E TILDE */
|
||||||
|
|
||||||
|
XK_nobreakspace: 0x00a0, /* U+00A0 NO-BREAK SPACE */
|
||||||
|
XK_exclamdown: 0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */
|
||||||
|
XK_cent: 0x00a2, /* U+00A2 CENT SIGN */
|
||||||
|
XK_sterling: 0x00a3, /* U+00A3 POUND SIGN */
|
||||||
|
XK_currency: 0x00a4, /* U+00A4 CURRENCY SIGN */
|
||||||
|
XK_yen: 0x00a5, /* U+00A5 YEN SIGN */
|
||||||
|
XK_brokenbar: 0x00a6, /* U+00A6 BROKEN BAR */
|
||||||
|
XK_section: 0x00a7, /* U+00A7 SECTION SIGN */
|
||||||
|
XK_diaeresis: 0x00a8, /* U+00A8 DIAERESIS */
|
||||||
|
XK_copyright: 0x00a9, /* U+00A9 COPYRIGHT SIGN */
|
||||||
|
XK_ordfeminine: 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */
|
||||||
|
XK_guillemotleft: 0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */
|
||||||
|
XK_notsign: 0x00ac, /* U+00AC NOT SIGN */
|
||||||
|
XK_hyphen: 0x00ad, /* U+00AD SOFT HYPHEN */
|
||||||
|
XK_registered: 0x00ae, /* U+00AE REGISTERED SIGN */
|
||||||
|
XK_macron: 0x00af, /* U+00AF MACRON */
|
||||||
|
XK_degree: 0x00b0, /* U+00B0 DEGREE SIGN */
|
||||||
|
XK_plusminus: 0x00b1, /* U+00B1 PLUS-MINUS SIGN */
|
||||||
|
XK_twosuperior: 0x00b2, /* U+00B2 SUPERSCRIPT TWO */
|
||||||
|
XK_threesuperior: 0x00b3, /* U+00B3 SUPERSCRIPT THREE */
|
||||||
|
XK_acute: 0x00b4, /* U+00B4 ACUTE ACCENT */
|
||||||
|
XK_mu: 0x00b5, /* U+00B5 MICRO SIGN */
|
||||||
|
XK_paragraph: 0x00b6, /* U+00B6 PILCROW SIGN */
|
||||||
|
XK_periodcentered: 0x00b7, /* U+00B7 MIDDLE DOT */
|
||||||
|
XK_cedilla: 0x00b8, /* U+00B8 CEDILLA */
|
||||||
|
XK_onesuperior: 0x00b9, /* U+00B9 SUPERSCRIPT ONE */
|
||||||
|
XK_masculine: 0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */
|
||||||
|
XK_guillemotright: 0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */
|
||||||
|
XK_onequarter: 0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */
|
||||||
|
XK_onehalf: 0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */
|
||||||
|
XK_threequarters: 0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */
|
||||||
|
XK_questiondown: 0x00bf, /* U+00BF INVERTED QUESTION MARK */
|
||||||
|
XK_Agrave: 0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */
|
||||||
|
XK_Aacute: 0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */
|
||||||
|
XK_Acircumflex: 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */
|
||||||
|
XK_Atilde: 0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */
|
||||||
|
XK_Adiaeresis: 0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */
|
||||||
|
XK_Aring: 0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */
|
||||||
|
XK_AE: 0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */
|
||||||
|
XK_Ccedilla: 0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */
|
||||||
|
XK_Egrave: 0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */
|
||||||
|
XK_Eacute: 0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */
|
||||||
|
XK_Ecircumflex: 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */
|
||||||
|
XK_Ediaeresis: 0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */
|
||||||
|
XK_Igrave: 0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */
|
||||||
|
XK_Iacute: 0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */
|
||||||
|
XK_Icircumflex: 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */
|
||||||
|
XK_Idiaeresis: 0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */
|
||||||
|
XK_ETH: 0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */
|
||||||
|
XK_Eth: 0x00d0, /* deprecated */
|
||||||
|
XK_Ntilde: 0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */
|
||||||
|
XK_Ograve: 0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */
|
||||||
|
XK_Oacute: 0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */
|
||||||
|
XK_Ocircumflex: 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */
|
||||||
|
XK_Otilde: 0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */
|
||||||
|
XK_Odiaeresis: 0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */
|
||||||
|
XK_multiply: 0x00d7, /* U+00D7 MULTIPLICATION SIGN */
|
||||||
|
XK_Oslash: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */
|
||||||
|
XK_Ooblique: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */
|
||||||
|
XK_Ugrave: 0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */
|
||||||
|
XK_Uacute: 0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */
|
||||||
|
XK_Ucircumflex: 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */
|
||||||
|
XK_Udiaeresis: 0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */
|
||||||
|
XK_Yacute: 0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */
|
||||||
|
XK_THORN: 0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */
|
||||||
|
XK_Thorn: 0x00de, /* deprecated */
|
||||||
|
XK_ssharp: 0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */
|
||||||
|
XK_agrave: 0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */
|
||||||
|
XK_aacute: 0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */
|
||||||
|
XK_acircumflex: 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */
|
||||||
|
XK_atilde: 0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */
|
||||||
|
XK_adiaeresis: 0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */
|
||||||
|
XK_aring: 0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */
|
||||||
|
XK_ae: 0x00e6, /* U+00E6 LATIN SMALL LETTER AE */
|
||||||
|
XK_ccedilla: 0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */
|
||||||
|
XK_egrave: 0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */
|
||||||
|
XK_eacute: 0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */
|
||||||
|
XK_ecircumflex: 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */
|
||||||
|
XK_ediaeresis: 0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */
|
||||||
|
XK_igrave: 0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */
|
||||||
|
XK_iacute: 0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */
|
||||||
|
XK_icircumflex: 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */
|
||||||
|
XK_idiaeresis: 0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */
|
||||||
|
XK_eth: 0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */
|
||||||
|
XK_ntilde: 0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */
|
||||||
|
XK_ograve: 0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */
|
||||||
|
XK_oacute: 0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */
|
||||||
|
XK_ocircumflex: 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */
|
||||||
|
XK_otilde: 0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */
|
||||||
|
XK_odiaeresis: 0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */
|
||||||
|
XK_division: 0x00f7, /* U+00F7 DIVISION SIGN */
|
||||||
|
XK_oslash: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */
|
||||||
|
XK_ooblique: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */
|
||||||
|
XK_ugrave: 0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */
|
||||||
|
XK_uacute: 0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */
|
||||||
|
XK_ucircumflex: 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */
|
||||||
|
XK_udiaeresis: 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */
|
||||||
|
XK_yacute: 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */
|
||||||
|
XK_thorn: 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */
|
||||||
|
XK_ydiaeresis: 0x00ff, /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Korean
|
||||||
|
* Byte 3 = 0x0e
|
||||||
|
*/
|
||||||
|
|
||||||
|
XK_Hangul: 0xff31, /* Hangul start/stop(toggle) */
|
||||||
|
XK_Hangul_Hanja: 0xff34, /* Start Hangul->Hanja Conversion */
|
||||||
|
XK_Hangul_Jeonja: 0xff38, /* Jeonja mode */
|
||||||
|
|
||||||
|
/*
|
||||||
|
* XFree86 vendor specific keysyms.
|
||||||
|
*
|
||||||
|
* The XFree86 keysym range is 0x10080001 - 0x1008FFFF.
|
||||||
|
*/
|
||||||
|
|
||||||
|
XF86XK_ModeLock: 0x1008FF01,
|
||||||
|
XF86XK_MonBrightnessUp: 0x1008FF02,
|
||||||
|
XF86XK_MonBrightnessDown: 0x1008FF03,
|
||||||
|
XF86XK_KbdLightOnOff: 0x1008FF04,
|
||||||
|
XF86XK_KbdBrightnessUp: 0x1008FF05,
|
||||||
|
XF86XK_KbdBrightnessDown: 0x1008FF06,
|
||||||
|
XF86XK_Standby: 0x1008FF10,
|
||||||
|
XF86XK_AudioLowerVolume: 0x1008FF11,
|
||||||
|
XF86XK_AudioMute: 0x1008FF12,
|
||||||
|
XF86XK_AudioRaiseVolume: 0x1008FF13,
|
||||||
|
XF86XK_AudioPlay: 0x1008FF14,
|
||||||
|
XF86XK_AudioStop: 0x1008FF15,
|
||||||
|
XF86XK_AudioPrev: 0x1008FF16,
|
||||||
|
XF86XK_AudioNext: 0x1008FF17,
|
||||||
|
XF86XK_HomePage: 0x1008FF18,
|
||||||
|
XF86XK_Mail: 0x1008FF19,
|
||||||
|
XF86XK_Start: 0x1008FF1A,
|
||||||
|
XF86XK_Search: 0x1008FF1B,
|
||||||
|
XF86XK_AudioRecord: 0x1008FF1C,
|
||||||
|
XF86XK_Calculator: 0x1008FF1D,
|
||||||
|
XF86XK_Memo: 0x1008FF1E,
|
||||||
|
XF86XK_ToDoList: 0x1008FF1F,
|
||||||
|
XF86XK_Calendar: 0x1008FF20,
|
||||||
|
XF86XK_PowerDown: 0x1008FF21,
|
||||||
|
XF86XK_ContrastAdjust: 0x1008FF22,
|
||||||
|
XF86XK_RockerUp: 0x1008FF23,
|
||||||
|
XF86XK_RockerDown: 0x1008FF24,
|
||||||
|
XF86XK_RockerEnter: 0x1008FF25,
|
||||||
|
XF86XK_Back: 0x1008FF26,
|
||||||
|
XF86XK_Forward: 0x1008FF27,
|
||||||
|
XF86XK_Stop: 0x1008FF28,
|
||||||
|
XF86XK_Refresh: 0x1008FF29,
|
||||||
|
XF86XK_PowerOff: 0x1008FF2A,
|
||||||
|
XF86XK_WakeUp: 0x1008FF2B,
|
||||||
|
XF86XK_Eject: 0x1008FF2C,
|
||||||
|
XF86XK_ScreenSaver: 0x1008FF2D,
|
||||||
|
XF86XK_WWW: 0x1008FF2E,
|
||||||
|
XF86XK_Sleep: 0x1008FF2F,
|
||||||
|
XF86XK_Favorites: 0x1008FF30,
|
||||||
|
XF86XK_AudioPause: 0x1008FF31,
|
||||||
|
XF86XK_AudioMedia: 0x1008FF32,
|
||||||
|
XF86XK_MyComputer: 0x1008FF33,
|
||||||
|
XF86XK_VendorHome: 0x1008FF34,
|
||||||
|
XF86XK_LightBulb: 0x1008FF35,
|
||||||
|
XF86XK_Shop: 0x1008FF36,
|
||||||
|
XF86XK_History: 0x1008FF37,
|
||||||
|
XF86XK_OpenURL: 0x1008FF38,
|
||||||
|
XF86XK_AddFavorite: 0x1008FF39,
|
||||||
|
XF86XK_HotLinks: 0x1008FF3A,
|
||||||
|
XF86XK_BrightnessAdjust: 0x1008FF3B,
|
||||||
|
XF86XK_Finance: 0x1008FF3C,
|
||||||
|
XF86XK_Community: 0x1008FF3D,
|
||||||
|
XF86XK_AudioRewind: 0x1008FF3E,
|
||||||
|
XF86XK_BackForward: 0x1008FF3F,
|
||||||
|
XF86XK_Launch0: 0x1008FF40,
|
||||||
|
XF86XK_Launch1: 0x1008FF41,
|
||||||
|
XF86XK_Launch2: 0x1008FF42,
|
||||||
|
XF86XK_Launch3: 0x1008FF43,
|
||||||
|
XF86XK_Launch4: 0x1008FF44,
|
||||||
|
XF86XK_Launch5: 0x1008FF45,
|
||||||
|
XF86XK_Launch6: 0x1008FF46,
|
||||||
|
XF86XK_Launch7: 0x1008FF47,
|
||||||
|
XF86XK_Launch8: 0x1008FF48,
|
||||||
|
XF86XK_Launch9: 0x1008FF49,
|
||||||
|
XF86XK_LaunchA: 0x1008FF4A,
|
||||||
|
XF86XK_LaunchB: 0x1008FF4B,
|
||||||
|
XF86XK_LaunchC: 0x1008FF4C,
|
||||||
|
XF86XK_LaunchD: 0x1008FF4D,
|
||||||
|
XF86XK_LaunchE: 0x1008FF4E,
|
||||||
|
XF86XK_LaunchF: 0x1008FF4F,
|
||||||
|
XF86XK_ApplicationLeft: 0x1008FF50,
|
||||||
|
XF86XK_ApplicationRight: 0x1008FF51,
|
||||||
|
XF86XK_Book: 0x1008FF52,
|
||||||
|
XF86XK_CD: 0x1008FF53,
|
||||||
|
XF86XK_Calculater: 0x1008FF54,
|
||||||
|
XF86XK_Clear: 0x1008FF55,
|
||||||
|
XF86XK_Close: 0x1008FF56,
|
||||||
|
XF86XK_Copy: 0x1008FF57,
|
||||||
|
XF86XK_Cut: 0x1008FF58,
|
||||||
|
XF86XK_Display: 0x1008FF59,
|
||||||
|
XF86XK_DOS: 0x1008FF5A,
|
||||||
|
XF86XK_Documents: 0x1008FF5B,
|
||||||
|
XF86XK_Excel: 0x1008FF5C,
|
||||||
|
XF86XK_Explorer: 0x1008FF5D,
|
||||||
|
XF86XK_Game: 0x1008FF5E,
|
||||||
|
XF86XK_Go: 0x1008FF5F,
|
||||||
|
XF86XK_iTouch: 0x1008FF60,
|
||||||
|
XF86XK_LogOff: 0x1008FF61,
|
||||||
|
XF86XK_Market: 0x1008FF62,
|
||||||
|
XF86XK_Meeting: 0x1008FF63,
|
||||||
|
XF86XK_MenuKB: 0x1008FF65,
|
||||||
|
XF86XK_MenuPB: 0x1008FF66,
|
||||||
|
XF86XK_MySites: 0x1008FF67,
|
||||||
|
XF86XK_New: 0x1008FF68,
|
||||||
|
XF86XK_News: 0x1008FF69,
|
||||||
|
XF86XK_OfficeHome: 0x1008FF6A,
|
||||||
|
XF86XK_Open: 0x1008FF6B,
|
||||||
|
XF86XK_Option: 0x1008FF6C,
|
||||||
|
XF86XK_Paste: 0x1008FF6D,
|
||||||
|
XF86XK_Phone: 0x1008FF6E,
|
||||||
|
XF86XK_Q: 0x1008FF70,
|
||||||
|
XF86XK_Reply: 0x1008FF72,
|
||||||
|
XF86XK_Reload: 0x1008FF73,
|
||||||
|
XF86XK_RotateWindows: 0x1008FF74,
|
||||||
|
XF86XK_RotationPB: 0x1008FF75,
|
||||||
|
XF86XK_RotationKB: 0x1008FF76,
|
||||||
|
XF86XK_Save: 0x1008FF77,
|
||||||
|
XF86XK_ScrollUp: 0x1008FF78,
|
||||||
|
XF86XK_ScrollDown: 0x1008FF79,
|
||||||
|
XF86XK_ScrollClick: 0x1008FF7A,
|
||||||
|
XF86XK_Send: 0x1008FF7B,
|
||||||
|
XF86XK_Spell: 0x1008FF7C,
|
||||||
|
XF86XK_SplitScreen: 0x1008FF7D,
|
||||||
|
XF86XK_Support: 0x1008FF7E,
|
||||||
|
XF86XK_TaskPane: 0x1008FF7F,
|
||||||
|
XF86XK_Terminal: 0x1008FF80,
|
||||||
|
XF86XK_Tools: 0x1008FF81,
|
||||||
|
XF86XK_Travel: 0x1008FF82,
|
||||||
|
XF86XK_UserPB: 0x1008FF84,
|
||||||
|
XF86XK_User1KB: 0x1008FF85,
|
||||||
|
XF86XK_User2KB: 0x1008FF86,
|
||||||
|
XF86XK_Video: 0x1008FF87,
|
||||||
|
XF86XK_WheelButton: 0x1008FF88,
|
||||||
|
XF86XK_Word: 0x1008FF89,
|
||||||
|
XF86XK_Xfer: 0x1008FF8A,
|
||||||
|
XF86XK_ZoomIn: 0x1008FF8B,
|
||||||
|
XF86XK_ZoomOut: 0x1008FF8C,
|
||||||
|
XF86XK_Away: 0x1008FF8D,
|
||||||
|
XF86XK_Messenger: 0x1008FF8E,
|
||||||
|
XF86XK_WebCam: 0x1008FF8F,
|
||||||
|
XF86XK_MailForward: 0x1008FF90,
|
||||||
|
XF86XK_Pictures: 0x1008FF91,
|
||||||
|
XF86XK_Music: 0x1008FF92,
|
||||||
|
XF86XK_Battery: 0x1008FF93,
|
||||||
|
XF86XK_Bluetooth: 0x1008FF94,
|
||||||
|
XF86XK_WLAN: 0x1008FF95,
|
||||||
|
XF86XK_UWB: 0x1008FF96,
|
||||||
|
XF86XK_AudioForward: 0x1008FF97,
|
||||||
|
XF86XK_AudioRepeat: 0x1008FF98,
|
||||||
|
XF86XK_AudioRandomPlay: 0x1008FF99,
|
||||||
|
XF86XK_Subtitle: 0x1008FF9A,
|
||||||
|
XF86XK_AudioCycleTrack: 0x1008FF9B,
|
||||||
|
XF86XK_CycleAngle: 0x1008FF9C,
|
||||||
|
XF86XK_FrameBack: 0x1008FF9D,
|
||||||
|
XF86XK_FrameForward: 0x1008FF9E,
|
||||||
|
XF86XK_Time: 0x1008FF9F,
|
||||||
|
XF86XK_Select: 0x1008FFA0,
|
||||||
|
XF86XK_View: 0x1008FFA1,
|
||||||
|
XF86XK_TopMenu: 0x1008FFA2,
|
||||||
|
XF86XK_Red: 0x1008FFA3,
|
||||||
|
XF86XK_Green: 0x1008FFA4,
|
||||||
|
XF86XK_Yellow: 0x1008FFA5,
|
||||||
|
XF86XK_Blue: 0x1008FFA6,
|
||||||
|
XF86XK_Suspend: 0x1008FFA7,
|
||||||
|
XF86XK_Hibernate: 0x1008FFA8,
|
||||||
|
XF86XK_TouchpadToggle: 0x1008FFA9,
|
||||||
|
XF86XK_TouchpadOn: 0x1008FFB0,
|
||||||
|
XF86XK_TouchpadOff: 0x1008FFB1,
|
||||||
|
XF86XK_AudioMicMute: 0x1008FFB2,
|
||||||
|
XF86XK_Switch_VT_1: 0x1008FE01,
|
||||||
|
XF86XK_Switch_VT_2: 0x1008FE02,
|
||||||
|
XF86XK_Switch_VT_3: 0x1008FE03,
|
||||||
|
XF86XK_Switch_VT_4: 0x1008FE04,
|
||||||
|
XF86XK_Switch_VT_5: 0x1008FE05,
|
||||||
|
XF86XK_Switch_VT_6: 0x1008FE06,
|
||||||
|
XF86XK_Switch_VT_7: 0x1008FE07,
|
||||||
|
XF86XK_Switch_VT_8: 0x1008FE08,
|
||||||
|
XF86XK_Switch_VT_9: 0x1008FE09,
|
||||||
|
XF86XK_Switch_VT_10: 0x1008FE0A,
|
||||||
|
XF86XK_Switch_VT_11: 0x1008FE0B,
|
||||||
|
XF86XK_Switch_VT_12: 0x1008FE0C,
|
||||||
|
XF86XK_Ungrab: 0x1008FE20,
|
||||||
|
XF86XK_ClearGrab: 0x1008FE21,
|
||||||
|
XF86XK_Next_VMode: 0x1008FE22,
|
||||||
|
XF86XK_Prev_VMode: 0x1008FE23,
|
||||||
|
XF86XK_LogWindowTree: 0x1008FE24,
|
||||||
|
XF86XK_LogGrabInfo: 0x1008FE25,
|
||||||
|
};
|
||||||
+688
@@ -0,0 +1,688 @@
|
|||||||
|
/*
|
||||||
|
* Mapping from Unicode codepoints to X11/RFB keysyms
|
||||||
|
*
|
||||||
|
* This file was automatically generated from keysymdef.h
|
||||||
|
* DO NOT EDIT!
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* Functions at the bottom */
|
||||||
|
|
||||||
|
const codepoints = {
|
||||||
|
0x0100: 0x03c0, // XK_Amacron
|
||||||
|
0x0101: 0x03e0, // XK_amacron
|
||||||
|
0x0102: 0x01c3, // XK_Abreve
|
||||||
|
0x0103: 0x01e3, // XK_abreve
|
||||||
|
0x0104: 0x01a1, // XK_Aogonek
|
||||||
|
0x0105: 0x01b1, // XK_aogonek
|
||||||
|
0x0106: 0x01c6, // XK_Cacute
|
||||||
|
0x0107: 0x01e6, // XK_cacute
|
||||||
|
0x0108: 0x02c6, // XK_Ccircumflex
|
||||||
|
0x0109: 0x02e6, // XK_ccircumflex
|
||||||
|
0x010a: 0x02c5, // XK_Cabovedot
|
||||||
|
0x010b: 0x02e5, // XK_cabovedot
|
||||||
|
0x010c: 0x01c8, // XK_Ccaron
|
||||||
|
0x010d: 0x01e8, // XK_ccaron
|
||||||
|
0x010e: 0x01cf, // XK_Dcaron
|
||||||
|
0x010f: 0x01ef, // XK_dcaron
|
||||||
|
0x0110: 0x01d0, // XK_Dstroke
|
||||||
|
0x0111: 0x01f0, // XK_dstroke
|
||||||
|
0x0112: 0x03aa, // XK_Emacron
|
||||||
|
0x0113: 0x03ba, // XK_emacron
|
||||||
|
0x0116: 0x03cc, // XK_Eabovedot
|
||||||
|
0x0117: 0x03ec, // XK_eabovedot
|
||||||
|
0x0118: 0x01ca, // XK_Eogonek
|
||||||
|
0x0119: 0x01ea, // XK_eogonek
|
||||||
|
0x011a: 0x01cc, // XK_Ecaron
|
||||||
|
0x011b: 0x01ec, // XK_ecaron
|
||||||
|
0x011c: 0x02d8, // XK_Gcircumflex
|
||||||
|
0x011d: 0x02f8, // XK_gcircumflex
|
||||||
|
0x011e: 0x02ab, // XK_Gbreve
|
||||||
|
0x011f: 0x02bb, // XK_gbreve
|
||||||
|
0x0120: 0x02d5, // XK_Gabovedot
|
||||||
|
0x0121: 0x02f5, // XK_gabovedot
|
||||||
|
0x0122: 0x03ab, // XK_Gcedilla
|
||||||
|
0x0123: 0x03bb, // XK_gcedilla
|
||||||
|
0x0124: 0x02a6, // XK_Hcircumflex
|
||||||
|
0x0125: 0x02b6, // XK_hcircumflex
|
||||||
|
0x0126: 0x02a1, // XK_Hstroke
|
||||||
|
0x0127: 0x02b1, // XK_hstroke
|
||||||
|
0x0128: 0x03a5, // XK_Itilde
|
||||||
|
0x0129: 0x03b5, // XK_itilde
|
||||||
|
0x012a: 0x03cf, // XK_Imacron
|
||||||
|
0x012b: 0x03ef, // XK_imacron
|
||||||
|
0x012e: 0x03c7, // XK_Iogonek
|
||||||
|
0x012f: 0x03e7, // XK_iogonek
|
||||||
|
0x0130: 0x02a9, // XK_Iabovedot
|
||||||
|
0x0131: 0x02b9, // XK_idotless
|
||||||
|
0x0134: 0x02ac, // XK_Jcircumflex
|
||||||
|
0x0135: 0x02bc, // XK_jcircumflex
|
||||||
|
0x0136: 0x03d3, // XK_Kcedilla
|
||||||
|
0x0137: 0x03f3, // XK_kcedilla
|
||||||
|
0x0138: 0x03a2, // XK_kra
|
||||||
|
0x0139: 0x01c5, // XK_Lacute
|
||||||
|
0x013a: 0x01e5, // XK_lacute
|
||||||
|
0x013b: 0x03a6, // XK_Lcedilla
|
||||||
|
0x013c: 0x03b6, // XK_lcedilla
|
||||||
|
0x013d: 0x01a5, // XK_Lcaron
|
||||||
|
0x013e: 0x01b5, // XK_lcaron
|
||||||
|
0x0141: 0x01a3, // XK_Lstroke
|
||||||
|
0x0142: 0x01b3, // XK_lstroke
|
||||||
|
0x0143: 0x01d1, // XK_Nacute
|
||||||
|
0x0144: 0x01f1, // XK_nacute
|
||||||
|
0x0145: 0x03d1, // XK_Ncedilla
|
||||||
|
0x0146: 0x03f1, // XK_ncedilla
|
||||||
|
0x0147: 0x01d2, // XK_Ncaron
|
||||||
|
0x0148: 0x01f2, // XK_ncaron
|
||||||
|
0x014a: 0x03bd, // XK_ENG
|
||||||
|
0x014b: 0x03bf, // XK_eng
|
||||||
|
0x014c: 0x03d2, // XK_Omacron
|
||||||
|
0x014d: 0x03f2, // XK_omacron
|
||||||
|
0x0150: 0x01d5, // XK_Odoubleacute
|
||||||
|
0x0151: 0x01f5, // XK_odoubleacute
|
||||||
|
0x0152: 0x13bc, // XK_OE
|
||||||
|
0x0153: 0x13bd, // XK_oe
|
||||||
|
0x0154: 0x01c0, // XK_Racute
|
||||||
|
0x0155: 0x01e0, // XK_racute
|
||||||
|
0x0156: 0x03a3, // XK_Rcedilla
|
||||||
|
0x0157: 0x03b3, // XK_rcedilla
|
||||||
|
0x0158: 0x01d8, // XK_Rcaron
|
||||||
|
0x0159: 0x01f8, // XK_rcaron
|
||||||
|
0x015a: 0x01a6, // XK_Sacute
|
||||||
|
0x015b: 0x01b6, // XK_sacute
|
||||||
|
0x015c: 0x02de, // XK_Scircumflex
|
||||||
|
0x015d: 0x02fe, // XK_scircumflex
|
||||||
|
0x015e: 0x01aa, // XK_Scedilla
|
||||||
|
0x015f: 0x01ba, // XK_scedilla
|
||||||
|
0x0160: 0x01a9, // XK_Scaron
|
||||||
|
0x0161: 0x01b9, // XK_scaron
|
||||||
|
0x0162: 0x01de, // XK_Tcedilla
|
||||||
|
0x0163: 0x01fe, // XK_tcedilla
|
||||||
|
0x0164: 0x01ab, // XK_Tcaron
|
||||||
|
0x0165: 0x01bb, // XK_tcaron
|
||||||
|
0x0166: 0x03ac, // XK_Tslash
|
||||||
|
0x0167: 0x03bc, // XK_tslash
|
||||||
|
0x0168: 0x03dd, // XK_Utilde
|
||||||
|
0x0169: 0x03fd, // XK_utilde
|
||||||
|
0x016a: 0x03de, // XK_Umacron
|
||||||
|
0x016b: 0x03fe, // XK_umacron
|
||||||
|
0x016c: 0x02dd, // XK_Ubreve
|
||||||
|
0x016d: 0x02fd, // XK_ubreve
|
||||||
|
0x016e: 0x01d9, // XK_Uring
|
||||||
|
0x016f: 0x01f9, // XK_uring
|
||||||
|
0x0170: 0x01db, // XK_Udoubleacute
|
||||||
|
0x0171: 0x01fb, // XK_udoubleacute
|
||||||
|
0x0172: 0x03d9, // XK_Uogonek
|
||||||
|
0x0173: 0x03f9, // XK_uogonek
|
||||||
|
0x0178: 0x13be, // XK_Ydiaeresis
|
||||||
|
0x0179: 0x01ac, // XK_Zacute
|
||||||
|
0x017a: 0x01bc, // XK_zacute
|
||||||
|
0x017b: 0x01af, // XK_Zabovedot
|
||||||
|
0x017c: 0x01bf, // XK_zabovedot
|
||||||
|
0x017d: 0x01ae, // XK_Zcaron
|
||||||
|
0x017e: 0x01be, // XK_zcaron
|
||||||
|
0x0192: 0x08f6, // XK_function
|
||||||
|
0x01d2: 0x10001d1, // XK_Ocaron
|
||||||
|
0x02c7: 0x01b7, // XK_caron
|
||||||
|
0x02d8: 0x01a2, // XK_breve
|
||||||
|
0x02d9: 0x01ff, // XK_abovedot
|
||||||
|
0x02db: 0x01b2, // XK_ogonek
|
||||||
|
0x02dd: 0x01bd, // XK_doubleacute
|
||||||
|
0x0385: 0x07ae, // XK_Greek_accentdieresis
|
||||||
|
0x0386: 0x07a1, // XK_Greek_ALPHAaccent
|
||||||
|
0x0388: 0x07a2, // XK_Greek_EPSILONaccent
|
||||||
|
0x0389: 0x07a3, // XK_Greek_ETAaccent
|
||||||
|
0x038a: 0x07a4, // XK_Greek_IOTAaccent
|
||||||
|
0x038c: 0x07a7, // XK_Greek_OMICRONaccent
|
||||||
|
0x038e: 0x07a8, // XK_Greek_UPSILONaccent
|
||||||
|
0x038f: 0x07ab, // XK_Greek_OMEGAaccent
|
||||||
|
0x0390: 0x07b6, // XK_Greek_iotaaccentdieresis
|
||||||
|
0x0391: 0x07c1, // XK_Greek_ALPHA
|
||||||
|
0x0392: 0x07c2, // XK_Greek_BETA
|
||||||
|
0x0393: 0x07c3, // XK_Greek_GAMMA
|
||||||
|
0x0394: 0x07c4, // XK_Greek_DELTA
|
||||||
|
0x0395: 0x07c5, // XK_Greek_EPSILON
|
||||||
|
0x0396: 0x07c6, // XK_Greek_ZETA
|
||||||
|
0x0397: 0x07c7, // XK_Greek_ETA
|
||||||
|
0x0398: 0x07c8, // XK_Greek_THETA
|
||||||
|
0x0399: 0x07c9, // XK_Greek_IOTA
|
||||||
|
0x039a: 0x07ca, // XK_Greek_KAPPA
|
||||||
|
0x039b: 0x07cb, // XK_Greek_LAMDA
|
||||||
|
0x039c: 0x07cc, // XK_Greek_MU
|
||||||
|
0x039d: 0x07cd, // XK_Greek_NU
|
||||||
|
0x039e: 0x07ce, // XK_Greek_XI
|
||||||
|
0x039f: 0x07cf, // XK_Greek_OMICRON
|
||||||
|
0x03a0: 0x07d0, // XK_Greek_PI
|
||||||
|
0x03a1: 0x07d1, // XK_Greek_RHO
|
||||||
|
0x03a3: 0x07d2, // XK_Greek_SIGMA
|
||||||
|
0x03a4: 0x07d4, // XK_Greek_TAU
|
||||||
|
0x03a5: 0x07d5, // XK_Greek_UPSILON
|
||||||
|
0x03a6: 0x07d6, // XK_Greek_PHI
|
||||||
|
0x03a7: 0x07d7, // XK_Greek_CHI
|
||||||
|
0x03a8: 0x07d8, // XK_Greek_PSI
|
||||||
|
0x03a9: 0x07d9, // XK_Greek_OMEGA
|
||||||
|
0x03aa: 0x07a5, // XK_Greek_IOTAdieresis
|
||||||
|
0x03ab: 0x07a9, // XK_Greek_UPSILONdieresis
|
||||||
|
0x03ac: 0x07b1, // XK_Greek_alphaaccent
|
||||||
|
0x03ad: 0x07b2, // XK_Greek_epsilonaccent
|
||||||
|
0x03ae: 0x07b3, // XK_Greek_etaaccent
|
||||||
|
0x03af: 0x07b4, // XK_Greek_iotaaccent
|
||||||
|
0x03b0: 0x07ba, // XK_Greek_upsilonaccentdieresis
|
||||||
|
0x03b1: 0x07e1, // XK_Greek_alpha
|
||||||
|
0x03b2: 0x07e2, // XK_Greek_beta
|
||||||
|
0x03b3: 0x07e3, // XK_Greek_gamma
|
||||||
|
0x03b4: 0x07e4, // XK_Greek_delta
|
||||||
|
0x03b5: 0x07e5, // XK_Greek_epsilon
|
||||||
|
0x03b6: 0x07e6, // XK_Greek_zeta
|
||||||
|
0x03b7: 0x07e7, // XK_Greek_eta
|
||||||
|
0x03b8: 0x07e8, // XK_Greek_theta
|
||||||
|
0x03b9: 0x07e9, // XK_Greek_iota
|
||||||
|
0x03ba: 0x07ea, // XK_Greek_kappa
|
||||||
|
0x03bb: 0x07eb, // XK_Greek_lamda
|
||||||
|
0x03bc: 0x07ec, // XK_Greek_mu
|
||||||
|
0x03bd: 0x07ed, // XK_Greek_nu
|
||||||
|
0x03be: 0x07ee, // XK_Greek_xi
|
||||||
|
0x03bf: 0x07ef, // XK_Greek_omicron
|
||||||
|
0x03c0: 0x07f0, // XK_Greek_pi
|
||||||
|
0x03c1: 0x07f1, // XK_Greek_rho
|
||||||
|
0x03c2: 0x07f3, // XK_Greek_finalsmallsigma
|
||||||
|
0x03c3: 0x07f2, // XK_Greek_sigma
|
||||||
|
0x03c4: 0x07f4, // XK_Greek_tau
|
||||||
|
0x03c5: 0x07f5, // XK_Greek_upsilon
|
||||||
|
0x03c6: 0x07f6, // XK_Greek_phi
|
||||||
|
0x03c7: 0x07f7, // XK_Greek_chi
|
||||||
|
0x03c8: 0x07f8, // XK_Greek_psi
|
||||||
|
0x03c9: 0x07f9, // XK_Greek_omega
|
||||||
|
0x03ca: 0x07b5, // XK_Greek_iotadieresis
|
||||||
|
0x03cb: 0x07b9, // XK_Greek_upsilondieresis
|
||||||
|
0x03cc: 0x07b7, // XK_Greek_omicronaccent
|
||||||
|
0x03cd: 0x07b8, // XK_Greek_upsilonaccent
|
||||||
|
0x03ce: 0x07bb, // XK_Greek_omegaaccent
|
||||||
|
0x0401: 0x06b3, // XK_Cyrillic_IO
|
||||||
|
0x0402: 0x06b1, // XK_Serbian_DJE
|
||||||
|
0x0403: 0x06b2, // XK_Macedonia_GJE
|
||||||
|
0x0404: 0x06b4, // XK_Ukrainian_IE
|
||||||
|
0x0405: 0x06b5, // XK_Macedonia_DSE
|
||||||
|
0x0406: 0x06b6, // XK_Ukrainian_I
|
||||||
|
0x0407: 0x06b7, // XK_Ukrainian_YI
|
||||||
|
0x0408: 0x06b8, // XK_Cyrillic_JE
|
||||||
|
0x0409: 0x06b9, // XK_Cyrillic_LJE
|
||||||
|
0x040a: 0x06ba, // XK_Cyrillic_NJE
|
||||||
|
0x040b: 0x06bb, // XK_Serbian_TSHE
|
||||||
|
0x040c: 0x06bc, // XK_Macedonia_KJE
|
||||||
|
0x040e: 0x06be, // XK_Byelorussian_SHORTU
|
||||||
|
0x040f: 0x06bf, // XK_Cyrillic_DZHE
|
||||||
|
0x0410: 0x06e1, // XK_Cyrillic_A
|
||||||
|
0x0411: 0x06e2, // XK_Cyrillic_BE
|
||||||
|
0x0412: 0x06f7, // XK_Cyrillic_VE
|
||||||
|
0x0413: 0x06e7, // XK_Cyrillic_GHE
|
||||||
|
0x0414: 0x06e4, // XK_Cyrillic_DE
|
||||||
|
0x0415: 0x06e5, // XK_Cyrillic_IE
|
||||||
|
0x0416: 0x06f6, // XK_Cyrillic_ZHE
|
||||||
|
0x0417: 0x06fa, // XK_Cyrillic_ZE
|
||||||
|
0x0418: 0x06e9, // XK_Cyrillic_I
|
||||||
|
0x0419: 0x06ea, // XK_Cyrillic_SHORTI
|
||||||
|
0x041a: 0x06eb, // XK_Cyrillic_KA
|
||||||
|
0x041b: 0x06ec, // XK_Cyrillic_EL
|
||||||
|
0x041c: 0x06ed, // XK_Cyrillic_EM
|
||||||
|
0x041d: 0x06ee, // XK_Cyrillic_EN
|
||||||
|
0x041e: 0x06ef, // XK_Cyrillic_O
|
||||||
|
0x041f: 0x06f0, // XK_Cyrillic_PE
|
||||||
|
0x0420: 0x06f2, // XK_Cyrillic_ER
|
||||||
|
0x0421: 0x06f3, // XK_Cyrillic_ES
|
||||||
|
0x0422: 0x06f4, // XK_Cyrillic_TE
|
||||||
|
0x0423: 0x06f5, // XK_Cyrillic_U
|
||||||
|
0x0424: 0x06e6, // XK_Cyrillic_EF
|
||||||
|
0x0425: 0x06e8, // XK_Cyrillic_HA
|
||||||
|
0x0426: 0x06e3, // XK_Cyrillic_TSE
|
||||||
|
0x0427: 0x06fe, // XK_Cyrillic_CHE
|
||||||
|
0x0428: 0x06fb, // XK_Cyrillic_SHA
|
||||||
|
0x0429: 0x06fd, // XK_Cyrillic_SHCHA
|
||||||
|
0x042a: 0x06ff, // XK_Cyrillic_HARDSIGN
|
||||||
|
0x042b: 0x06f9, // XK_Cyrillic_YERU
|
||||||
|
0x042c: 0x06f8, // XK_Cyrillic_SOFTSIGN
|
||||||
|
0x042d: 0x06fc, // XK_Cyrillic_E
|
||||||
|
0x042e: 0x06e0, // XK_Cyrillic_YU
|
||||||
|
0x042f: 0x06f1, // XK_Cyrillic_YA
|
||||||
|
0x0430: 0x06c1, // XK_Cyrillic_a
|
||||||
|
0x0431: 0x06c2, // XK_Cyrillic_be
|
||||||
|
0x0432: 0x06d7, // XK_Cyrillic_ve
|
||||||
|
0x0433: 0x06c7, // XK_Cyrillic_ghe
|
||||||
|
0x0434: 0x06c4, // XK_Cyrillic_de
|
||||||
|
0x0435: 0x06c5, // XK_Cyrillic_ie
|
||||||
|
0x0436: 0x06d6, // XK_Cyrillic_zhe
|
||||||
|
0x0437: 0x06da, // XK_Cyrillic_ze
|
||||||
|
0x0438: 0x06c9, // XK_Cyrillic_i
|
||||||
|
0x0439: 0x06ca, // XK_Cyrillic_shorti
|
||||||
|
0x043a: 0x06cb, // XK_Cyrillic_ka
|
||||||
|
0x043b: 0x06cc, // XK_Cyrillic_el
|
||||||
|
0x043c: 0x06cd, // XK_Cyrillic_em
|
||||||
|
0x043d: 0x06ce, // XK_Cyrillic_en
|
||||||
|
0x043e: 0x06cf, // XK_Cyrillic_o
|
||||||
|
0x043f: 0x06d0, // XK_Cyrillic_pe
|
||||||
|
0x0440: 0x06d2, // XK_Cyrillic_er
|
||||||
|
0x0441: 0x06d3, // XK_Cyrillic_es
|
||||||
|
0x0442: 0x06d4, // XK_Cyrillic_te
|
||||||
|
0x0443: 0x06d5, // XK_Cyrillic_u
|
||||||
|
0x0444: 0x06c6, // XK_Cyrillic_ef
|
||||||
|
0x0445: 0x06c8, // XK_Cyrillic_ha
|
||||||
|
0x0446: 0x06c3, // XK_Cyrillic_tse
|
||||||
|
0x0447: 0x06de, // XK_Cyrillic_che
|
||||||
|
0x0448: 0x06db, // XK_Cyrillic_sha
|
||||||
|
0x0449: 0x06dd, // XK_Cyrillic_shcha
|
||||||
|
0x044a: 0x06df, // XK_Cyrillic_hardsign
|
||||||
|
0x044b: 0x06d9, // XK_Cyrillic_yeru
|
||||||
|
0x044c: 0x06d8, // XK_Cyrillic_softsign
|
||||||
|
0x044d: 0x06dc, // XK_Cyrillic_e
|
||||||
|
0x044e: 0x06c0, // XK_Cyrillic_yu
|
||||||
|
0x044f: 0x06d1, // XK_Cyrillic_ya
|
||||||
|
0x0451: 0x06a3, // XK_Cyrillic_io
|
||||||
|
0x0452: 0x06a1, // XK_Serbian_dje
|
||||||
|
0x0453: 0x06a2, // XK_Macedonia_gje
|
||||||
|
0x0454: 0x06a4, // XK_Ukrainian_ie
|
||||||
|
0x0455: 0x06a5, // XK_Macedonia_dse
|
||||||
|
0x0456: 0x06a6, // XK_Ukrainian_i
|
||||||
|
0x0457: 0x06a7, // XK_Ukrainian_yi
|
||||||
|
0x0458: 0x06a8, // XK_Cyrillic_je
|
||||||
|
0x0459: 0x06a9, // XK_Cyrillic_lje
|
||||||
|
0x045a: 0x06aa, // XK_Cyrillic_nje
|
||||||
|
0x045b: 0x06ab, // XK_Serbian_tshe
|
||||||
|
0x045c: 0x06ac, // XK_Macedonia_kje
|
||||||
|
0x045e: 0x06ae, // XK_Byelorussian_shortu
|
||||||
|
0x045f: 0x06af, // XK_Cyrillic_dzhe
|
||||||
|
0x0490: 0x06bd, // XK_Ukrainian_GHE_WITH_UPTURN
|
||||||
|
0x0491: 0x06ad, // XK_Ukrainian_ghe_with_upturn
|
||||||
|
0x05d0: 0x0ce0, // XK_hebrew_aleph
|
||||||
|
0x05d1: 0x0ce1, // XK_hebrew_bet
|
||||||
|
0x05d2: 0x0ce2, // XK_hebrew_gimel
|
||||||
|
0x05d3: 0x0ce3, // XK_hebrew_dalet
|
||||||
|
0x05d4: 0x0ce4, // XK_hebrew_he
|
||||||
|
0x05d5: 0x0ce5, // XK_hebrew_waw
|
||||||
|
0x05d6: 0x0ce6, // XK_hebrew_zain
|
||||||
|
0x05d7: 0x0ce7, // XK_hebrew_chet
|
||||||
|
0x05d8: 0x0ce8, // XK_hebrew_tet
|
||||||
|
0x05d9: 0x0ce9, // XK_hebrew_yod
|
||||||
|
0x05da: 0x0cea, // XK_hebrew_finalkaph
|
||||||
|
0x05db: 0x0ceb, // XK_hebrew_kaph
|
||||||
|
0x05dc: 0x0cec, // XK_hebrew_lamed
|
||||||
|
0x05dd: 0x0ced, // XK_hebrew_finalmem
|
||||||
|
0x05de: 0x0cee, // XK_hebrew_mem
|
||||||
|
0x05df: 0x0cef, // XK_hebrew_finalnun
|
||||||
|
0x05e0: 0x0cf0, // XK_hebrew_nun
|
||||||
|
0x05e1: 0x0cf1, // XK_hebrew_samech
|
||||||
|
0x05e2: 0x0cf2, // XK_hebrew_ayin
|
||||||
|
0x05e3: 0x0cf3, // XK_hebrew_finalpe
|
||||||
|
0x05e4: 0x0cf4, // XK_hebrew_pe
|
||||||
|
0x05e5: 0x0cf5, // XK_hebrew_finalzade
|
||||||
|
0x05e6: 0x0cf6, // XK_hebrew_zade
|
||||||
|
0x05e7: 0x0cf7, // XK_hebrew_qoph
|
||||||
|
0x05e8: 0x0cf8, // XK_hebrew_resh
|
||||||
|
0x05e9: 0x0cf9, // XK_hebrew_shin
|
||||||
|
0x05ea: 0x0cfa, // XK_hebrew_taw
|
||||||
|
0x060c: 0x05ac, // XK_Arabic_comma
|
||||||
|
0x061b: 0x05bb, // XK_Arabic_semicolon
|
||||||
|
0x061f: 0x05bf, // XK_Arabic_question_mark
|
||||||
|
0x0621: 0x05c1, // XK_Arabic_hamza
|
||||||
|
0x0622: 0x05c2, // XK_Arabic_maddaonalef
|
||||||
|
0x0623: 0x05c3, // XK_Arabic_hamzaonalef
|
||||||
|
0x0624: 0x05c4, // XK_Arabic_hamzaonwaw
|
||||||
|
0x0625: 0x05c5, // XK_Arabic_hamzaunderalef
|
||||||
|
0x0626: 0x05c6, // XK_Arabic_hamzaonyeh
|
||||||
|
0x0627: 0x05c7, // XK_Arabic_alef
|
||||||
|
0x0628: 0x05c8, // XK_Arabic_beh
|
||||||
|
0x0629: 0x05c9, // XK_Arabic_tehmarbuta
|
||||||
|
0x062a: 0x05ca, // XK_Arabic_teh
|
||||||
|
0x062b: 0x05cb, // XK_Arabic_theh
|
||||||
|
0x062c: 0x05cc, // XK_Arabic_jeem
|
||||||
|
0x062d: 0x05cd, // XK_Arabic_hah
|
||||||
|
0x062e: 0x05ce, // XK_Arabic_khah
|
||||||
|
0x062f: 0x05cf, // XK_Arabic_dal
|
||||||
|
0x0630: 0x05d0, // XK_Arabic_thal
|
||||||
|
0x0631: 0x05d1, // XK_Arabic_ra
|
||||||
|
0x0632: 0x05d2, // XK_Arabic_zain
|
||||||
|
0x0633: 0x05d3, // XK_Arabic_seen
|
||||||
|
0x0634: 0x05d4, // XK_Arabic_sheen
|
||||||
|
0x0635: 0x05d5, // XK_Arabic_sad
|
||||||
|
0x0636: 0x05d6, // XK_Arabic_dad
|
||||||
|
0x0637: 0x05d7, // XK_Arabic_tah
|
||||||
|
0x0638: 0x05d8, // XK_Arabic_zah
|
||||||
|
0x0639: 0x05d9, // XK_Arabic_ain
|
||||||
|
0x063a: 0x05da, // XK_Arabic_ghain
|
||||||
|
0x0640: 0x05e0, // XK_Arabic_tatweel
|
||||||
|
0x0641: 0x05e1, // XK_Arabic_feh
|
||||||
|
0x0642: 0x05e2, // XK_Arabic_qaf
|
||||||
|
0x0643: 0x05e3, // XK_Arabic_kaf
|
||||||
|
0x0644: 0x05e4, // XK_Arabic_lam
|
||||||
|
0x0645: 0x05e5, // XK_Arabic_meem
|
||||||
|
0x0646: 0x05e6, // XK_Arabic_noon
|
||||||
|
0x0647: 0x05e7, // XK_Arabic_ha
|
||||||
|
0x0648: 0x05e8, // XK_Arabic_waw
|
||||||
|
0x0649: 0x05e9, // XK_Arabic_alefmaksura
|
||||||
|
0x064a: 0x05ea, // XK_Arabic_yeh
|
||||||
|
0x064b: 0x05eb, // XK_Arabic_fathatan
|
||||||
|
0x064c: 0x05ec, // XK_Arabic_dammatan
|
||||||
|
0x064d: 0x05ed, // XK_Arabic_kasratan
|
||||||
|
0x064e: 0x05ee, // XK_Arabic_fatha
|
||||||
|
0x064f: 0x05ef, // XK_Arabic_damma
|
||||||
|
0x0650: 0x05f0, // XK_Arabic_kasra
|
||||||
|
0x0651: 0x05f1, // XK_Arabic_shadda
|
||||||
|
0x0652: 0x05f2, // XK_Arabic_sukun
|
||||||
|
0x0e01: 0x0da1, // XK_Thai_kokai
|
||||||
|
0x0e02: 0x0da2, // XK_Thai_khokhai
|
||||||
|
0x0e03: 0x0da3, // XK_Thai_khokhuat
|
||||||
|
0x0e04: 0x0da4, // XK_Thai_khokhwai
|
||||||
|
0x0e05: 0x0da5, // XK_Thai_khokhon
|
||||||
|
0x0e06: 0x0da6, // XK_Thai_khorakhang
|
||||||
|
0x0e07: 0x0da7, // XK_Thai_ngongu
|
||||||
|
0x0e08: 0x0da8, // XK_Thai_chochan
|
||||||
|
0x0e09: 0x0da9, // XK_Thai_choching
|
||||||
|
0x0e0a: 0x0daa, // XK_Thai_chochang
|
||||||
|
0x0e0b: 0x0dab, // XK_Thai_soso
|
||||||
|
0x0e0c: 0x0dac, // XK_Thai_chochoe
|
||||||
|
0x0e0d: 0x0dad, // XK_Thai_yoying
|
||||||
|
0x0e0e: 0x0dae, // XK_Thai_dochada
|
||||||
|
0x0e0f: 0x0daf, // XK_Thai_topatak
|
||||||
|
0x0e10: 0x0db0, // XK_Thai_thothan
|
||||||
|
0x0e11: 0x0db1, // XK_Thai_thonangmontho
|
||||||
|
0x0e12: 0x0db2, // XK_Thai_thophuthao
|
||||||
|
0x0e13: 0x0db3, // XK_Thai_nonen
|
||||||
|
0x0e14: 0x0db4, // XK_Thai_dodek
|
||||||
|
0x0e15: 0x0db5, // XK_Thai_totao
|
||||||
|
0x0e16: 0x0db6, // XK_Thai_thothung
|
||||||
|
0x0e17: 0x0db7, // XK_Thai_thothahan
|
||||||
|
0x0e18: 0x0db8, // XK_Thai_thothong
|
||||||
|
0x0e19: 0x0db9, // XK_Thai_nonu
|
||||||
|
0x0e1a: 0x0dba, // XK_Thai_bobaimai
|
||||||
|
0x0e1b: 0x0dbb, // XK_Thai_popla
|
||||||
|
0x0e1c: 0x0dbc, // XK_Thai_phophung
|
||||||
|
0x0e1d: 0x0dbd, // XK_Thai_fofa
|
||||||
|
0x0e1e: 0x0dbe, // XK_Thai_phophan
|
||||||
|
0x0e1f: 0x0dbf, // XK_Thai_fofan
|
||||||
|
0x0e20: 0x0dc0, // XK_Thai_phosamphao
|
||||||
|
0x0e21: 0x0dc1, // XK_Thai_moma
|
||||||
|
0x0e22: 0x0dc2, // XK_Thai_yoyak
|
||||||
|
0x0e23: 0x0dc3, // XK_Thai_rorua
|
||||||
|
0x0e24: 0x0dc4, // XK_Thai_ru
|
||||||
|
0x0e25: 0x0dc5, // XK_Thai_loling
|
||||||
|
0x0e26: 0x0dc6, // XK_Thai_lu
|
||||||
|
0x0e27: 0x0dc7, // XK_Thai_wowaen
|
||||||
|
0x0e28: 0x0dc8, // XK_Thai_sosala
|
||||||
|
0x0e29: 0x0dc9, // XK_Thai_sorusi
|
||||||
|
0x0e2a: 0x0dca, // XK_Thai_sosua
|
||||||
|
0x0e2b: 0x0dcb, // XK_Thai_hohip
|
||||||
|
0x0e2c: 0x0dcc, // XK_Thai_lochula
|
||||||
|
0x0e2d: 0x0dcd, // XK_Thai_oang
|
||||||
|
0x0e2e: 0x0dce, // XK_Thai_honokhuk
|
||||||
|
0x0e2f: 0x0dcf, // XK_Thai_paiyannoi
|
||||||
|
0x0e30: 0x0dd0, // XK_Thai_saraa
|
||||||
|
0x0e31: 0x0dd1, // XK_Thai_maihanakat
|
||||||
|
0x0e32: 0x0dd2, // XK_Thai_saraaa
|
||||||
|
0x0e33: 0x0dd3, // XK_Thai_saraam
|
||||||
|
0x0e34: 0x0dd4, // XK_Thai_sarai
|
||||||
|
0x0e35: 0x0dd5, // XK_Thai_saraii
|
||||||
|
0x0e36: 0x0dd6, // XK_Thai_saraue
|
||||||
|
0x0e37: 0x0dd7, // XK_Thai_sarauee
|
||||||
|
0x0e38: 0x0dd8, // XK_Thai_sarau
|
||||||
|
0x0e39: 0x0dd9, // XK_Thai_sarauu
|
||||||
|
0x0e3a: 0x0dda, // XK_Thai_phinthu
|
||||||
|
0x0e3f: 0x0ddf, // XK_Thai_baht
|
||||||
|
0x0e40: 0x0de0, // XK_Thai_sarae
|
||||||
|
0x0e41: 0x0de1, // XK_Thai_saraae
|
||||||
|
0x0e42: 0x0de2, // XK_Thai_sarao
|
||||||
|
0x0e43: 0x0de3, // XK_Thai_saraaimaimuan
|
||||||
|
0x0e44: 0x0de4, // XK_Thai_saraaimaimalai
|
||||||
|
0x0e45: 0x0de5, // XK_Thai_lakkhangyao
|
||||||
|
0x0e46: 0x0de6, // XK_Thai_maiyamok
|
||||||
|
0x0e47: 0x0de7, // XK_Thai_maitaikhu
|
||||||
|
0x0e48: 0x0de8, // XK_Thai_maiek
|
||||||
|
0x0e49: 0x0de9, // XK_Thai_maitho
|
||||||
|
0x0e4a: 0x0dea, // XK_Thai_maitri
|
||||||
|
0x0e4b: 0x0deb, // XK_Thai_maichattawa
|
||||||
|
0x0e4c: 0x0dec, // XK_Thai_thanthakhat
|
||||||
|
0x0e4d: 0x0ded, // XK_Thai_nikhahit
|
||||||
|
0x0e50: 0x0df0, // XK_Thai_leksun
|
||||||
|
0x0e51: 0x0df1, // XK_Thai_leknung
|
||||||
|
0x0e52: 0x0df2, // XK_Thai_leksong
|
||||||
|
0x0e53: 0x0df3, // XK_Thai_leksam
|
||||||
|
0x0e54: 0x0df4, // XK_Thai_leksi
|
||||||
|
0x0e55: 0x0df5, // XK_Thai_lekha
|
||||||
|
0x0e56: 0x0df6, // XK_Thai_lekhok
|
||||||
|
0x0e57: 0x0df7, // XK_Thai_lekchet
|
||||||
|
0x0e58: 0x0df8, // XK_Thai_lekpaet
|
||||||
|
0x0e59: 0x0df9, // XK_Thai_lekkao
|
||||||
|
0x2002: 0x0aa2, // XK_enspace
|
||||||
|
0x2003: 0x0aa1, // XK_emspace
|
||||||
|
0x2004: 0x0aa3, // XK_em3space
|
||||||
|
0x2005: 0x0aa4, // XK_em4space
|
||||||
|
0x2007: 0x0aa5, // XK_digitspace
|
||||||
|
0x2008: 0x0aa6, // XK_punctspace
|
||||||
|
0x2009: 0x0aa7, // XK_thinspace
|
||||||
|
0x200a: 0x0aa8, // XK_hairspace
|
||||||
|
0x2012: 0x0abb, // XK_figdash
|
||||||
|
0x2013: 0x0aaa, // XK_endash
|
||||||
|
0x2014: 0x0aa9, // XK_emdash
|
||||||
|
0x2015: 0x07af, // XK_Greek_horizbar
|
||||||
|
0x2017: 0x0cdf, // XK_hebrew_doublelowline
|
||||||
|
0x2018: 0x0ad0, // XK_leftsinglequotemark
|
||||||
|
0x2019: 0x0ad1, // XK_rightsinglequotemark
|
||||||
|
0x201a: 0x0afd, // XK_singlelowquotemark
|
||||||
|
0x201c: 0x0ad2, // XK_leftdoublequotemark
|
||||||
|
0x201d: 0x0ad3, // XK_rightdoublequotemark
|
||||||
|
0x201e: 0x0afe, // XK_doublelowquotemark
|
||||||
|
0x2020: 0x0af1, // XK_dagger
|
||||||
|
0x2021: 0x0af2, // XK_doubledagger
|
||||||
|
0x2022: 0x0ae6, // XK_enfilledcircbullet
|
||||||
|
0x2025: 0x0aaf, // XK_doubbaselinedot
|
||||||
|
0x2026: 0x0aae, // XK_ellipsis
|
||||||
|
0x2030: 0x0ad5, // XK_permille
|
||||||
|
0x2032: 0x0ad6, // XK_minutes
|
||||||
|
0x2033: 0x0ad7, // XK_seconds
|
||||||
|
0x2038: 0x0afc, // XK_caret
|
||||||
|
0x203e: 0x047e, // XK_overline
|
||||||
|
0x20a9: 0x0eff, // XK_Korean_Won
|
||||||
|
0x20ac: 0x20ac, // XK_EuroSign
|
||||||
|
0x2105: 0x0ab8, // XK_careof
|
||||||
|
0x2116: 0x06b0, // XK_numerosign
|
||||||
|
0x2117: 0x0afb, // XK_phonographcopyright
|
||||||
|
0x211e: 0x0ad4, // XK_prescription
|
||||||
|
0x2122: 0x0ac9, // XK_trademark
|
||||||
|
0x2153: 0x0ab0, // XK_onethird
|
||||||
|
0x2154: 0x0ab1, // XK_twothirds
|
||||||
|
0x2155: 0x0ab2, // XK_onefifth
|
||||||
|
0x2156: 0x0ab3, // XK_twofifths
|
||||||
|
0x2157: 0x0ab4, // XK_threefifths
|
||||||
|
0x2158: 0x0ab5, // XK_fourfifths
|
||||||
|
0x2159: 0x0ab6, // XK_onesixth
|
||||||
|
0x215a: 0x0ab7, // XK_fivesixths
|
||||||
|
0x215b: 0x0ac3, // XK_oneeighth
|
||||||
|
0x215c: 0x0ac4, // XK_threeeighths
|
||||||
|
0x215d: 0x0ac5, // XK_fiveeighths
|
||||||
|
0x215e: 0x0ac6, // XK_seveneighths
|
||||||
|
0x2190: 0x08fb, // XK_leftarrow
|
||||||
|
0x2191: 0x08fc, // XK_uparrow
|
||||||
|
0x2192: 0x08fd, // XK_rightarrow
|
||||||
|
0x2193: 0x08fe, // XK_downarrow
|
||||||
|
0x21d2: 0x08ce, // XK_implies
|
||||||
|
0x21d4: 0x08cd, // XK_ifonlyif
|
||||||
|
0x2202: 0x08ef, // XK_partialderivative
|
||||||
|
0x2207: 0x08c5, // XK_nabla
|
||||||
|
0x2218: 0x0bca, // XK_jot
|
||||||
|
0x221a: 0x08d6, // XK_radical
|
||||||
|
0x221d: 0x08c1, // XK_variation
|
||||||
|
0x221e: 0x08c2, // XK_infinity
|
||||||
|
0x2227: 0x08de, // XK_logicaland
|
||||||
|
0x2228: 0x08df, // XK_logicalor
|
||||||
|
0x2229: 0x08dc, // XK_intersection
|
||||||
|
0x222a: 0x08dd, // XK_union
|
||||||
|
0x222b: 0x08bf, // XK_integral
|
||||||
|
0x2234: 0x08c0, // XK_therefore
|
||||||
|
0x223c: 0x08c8, // XK_approximate
|
||||||
|
0x2243: 0x08c9, // XK_similarequal
|
||||||
|
0x2245: 0x1002248, // XK_approxeq
|
||||||
|
0x2260: 0x08bd, // XK_notequal
|
||||||
|
0x2261: 0x08cf, // XK_identical
|
||||||
|
0x2264: 0x08bc, // XK_lessthanequal
|
||||||
|
0x2265: 0x08be, // XK_greaterthanequal
|
||||||
|
0x2282: 0x08da, // XK_includedin
|
||||||
|
0x2283: 0x08db, // XK_includes
|
||||||
|
0x22a2: 0x0bfc, // XK_righttack
|
||||||
|
0x22a3: 0x0bdc, // XK_lefttack
|
||||||
|
0x22a4: 0x0bc2, // XK_downtack
|
||||||
|
0x22a5: 0x0bce, // XK_uptack
|
||||||
|
0x2308: 0x0bd3, // XK_upstile
|
||||||
|
0x230a: 0x0bc4, // XK_downstile
|
||||||
|
0x2315: 0x0afa, // XK_telephonerecorder
|
||||||
|
0x2320: 0x08a4, // XK_topintegral
|
||||||
|
0x2321: 0x08a5, // XK_botintegral
|
||||||
|
0x2395: 0x0bcc, // XK_quad
|
||||||
|
0x239b: 0x08ab, // XK_topleftparens
|
||||||
|
0x239d: 0x08ac, // XK_botleftparens
|
||||||
|
0x239e: 0x08ad, // XK_toprightparens
|
||||||
|
0x23a0: 0x08ae, // XK_botrightparens
|
||||||
|
0x23a1: 0x08a7, // XK_topleftsqbracket
|
||||||
|
0x23a3: 0x08a8, // XK_botleftsqbracket
|
||||||
|
0x23a4: 0x08a9, // XK_toprightsqbracket
|
||||||
|
0x23a6: 0x08aa, // XK_botrightsqbracket
|
||||||
|
0x23a8: 0x08af, // XK_leftmiddlecurlybrace
|
||||||
|
0x23ac: 0x08b0, // XK_rightmiddlecurlybrace
|
||||||
|
0x23b7: 0x08a1, // XK_leftradical
|
||||||
|
0x23ba: 0x09ef, // XK_horizlinescan1
|
||||||
|
0x23bb: 0x09f0, // XK_horizlinescan3
|
||||||
|
0x23bc: 0x09f2, // XK_horizlinescan7
|
||||||
|
0x23bd: 0x09f3, // XK_horizlinescan9
|
||||||
|
0x2409: 0x09e2, // XK_ht
|
||||||
|
0x240a: 0x09e5, // XK_lf
|
||||||
|
0x240b: 0x09e9, // XK_vt
|
||||||
|
0x240c: 0x09e3, // XK_ff
|
||||||
|
0x240d: 0x09e4, // XK_cr
|
||||||
|
0x2423: 0x0aac, // XK_signifblank
|
||||||
|
0x2424: 0x09e8, // XK_nl
|
||||||
|
0x2500: 0x08a3, // XK_horizconnector
|
||||||
|
0x2502: 0x08a6, // XK_vertconnector
|
||||||
|
0x250c: 0x08a2, // XK_topleftradical
|
||||||
|
0x2510: 0x09eb, // XK_uprightcorner
|
||||||
|
0x2514: 0x09ed, // XK_lowleftcorner
|
||||||
|
0x2518: 0x09ea, // XK_lowrightcorner
|
||||||
|
0x251c: 0x09f4, // XK_leftt
|
||||||
|
0x2524: 0x09f5, // XK_rightt
|
||||||
|
0x252c: 0x09f7, // XK_topt
|
||||||
|
0x2534: 0x09f6, // XK_bott
|
||||||
|
0x253c: 0x09ee, // XK_crossinglines
|
||||||
|
0x2592: 0x09e1, // XK_checkerboard
|
||||||
|
0x25aa: 0x0ae7, // XK_enfilledsqbullet
|
||||||
|
0x25ab: 0x0ae1, // XK_enopensquarebullet
|
||||||
|
0x25ac: 0x0adb, // XK_filledrectbullet
|
||||||
|
0x25ad: 0x0ae2, // XK_openrectbullet
|
||||||
|
0x25ae: 0x0adf, // XK_emfilledrect
|
||||||
|
0x25af: 0x0acf, // XK_emopenrectangle
|
||||||
|
0x25b2: 0x0ae8, // XK_filledtribulletup
|
||||||
|
0x25b3: 0x0ae3, // XK_opentribulletup
|
||||||
|
0x25b6: 0x0add, // XK_filledrighttribullet
|
||||||
|
0x25b7: 0x0acd, // XK_rightopentriangle
|
||||||
|
0x25bc: 0x0ae9, // XK_filledtribulletdown
|
||||||
|
0x25bd: 0x0ae4, // XK_opentribulletdown
|
||||||
|
0x25c0: 0x0adc, // XK_filledlefttribullet
|
||||||
|
0x25c1: 0x0acc, // XK_leftopentriangle
|
||||||
|
0x25c6: 0x09e0, // XK_soliddiamond
|
||||||
|
0x25cb: 0x0ace, // XK_emopencircle
|
||||||
|
0x25cf: 0x0ade, // XK_emfilledcircle
|
||||||
|
0x25e6: 0x0ae0, // XK_enopencircbullet
|
||||||
|
0x2606: 0x0ae5, // XK_openstar
|
||||||
|
0x260e: 0x0af9, // XK_telephone
|
||||||
|
0x2613: 0x0aca, // XK_signaturemark
|
||||||
|
0x261c: 0x0aea, // XK_leftpointer
|
||||||
|
0x261e: 0x0aeb, // XK_rightpointer
|
||||||
|
0x2640: 0x0af8, // XK_femalesymbol
|
||||||
|
0x2642: 0x0af7, // XK_malesymbol
|
||||||
|
0x2663: 0x0aec, // XK_club
|
||||||
|
0x2665: 0x0aee, // XK_heart
|
||||||
|
0x2666: 0x0aed, // XK_diamond
|
||||||
|
0x266d: 0x0af6, // XK_musicalflat
|
||||||
|
0x266f: 0x0af5, // XK_musicalsharp
|
||||||
|
0x2713: 0x0af3, // XK_checkmark
|
||||||
|
0x2717: 0x0af4, // XK_ballotcross
|
||||||
|
0x271d: 0x0ad9, // XK_latincross
|
||||||
|
0x2720: 0x0af0, // XK_maltesecross
|
||||||
|
0x27e8: 0x0abc, // XK_leftanglebracket
|
||||||
|
0x27e9: 0x0abe, // XK_rightanglebracket
|
||||||
|
0x3001: 0x04a4, // XK_kana_comma
|
||||||
|
0x3002: 0x04a1, // XK_kana_fullstop
|
||||||
|
0x300c: 0x04a2, // XK_kana_openingbracket
|
||||||
|
0x300d: 0x04a3, // XK_kana_closingbracket
|
||||||
|
0x309b: 0x04de, // XK_voicedsound
|
||||||
|
0x309c: 0x04df, // XK_semivoicedsound
|
||||||
|
0x30a1: 0x04a7, // XK_kana_a
|
||||||
|
0x30a2: 0x04b1, // XK_kana_A
|
||||||
|
0x30a3: 0x04a8, // XK_kana_i
|
||||||
|
0x30a4: 0x04b2, // XK_kana_I
|
||||||
|
0x30a5: 0x04a9, // XK_kana_u
|
||||||
|
0x30a6: 0x04b3, // XK_kana_U
|
||||||
|
0x30a7: 0x04aa, // XK_kana_e
|
||||||
|
0x30a8: 0x04b4, // XK_kana_E
|
||||||
|
0x30a9: 0x04ab, // XK_kana_o
|
||||||
|
0x30aa: 0x04b5, // XK_kana_O
|
||||||
|
0x30ab: 0x04b6, // XK_kana_KA
|
||||||
|
0x30ad: 0x04b7, // XK_kana_KI
|
||||||
|
0x30af: 0x04b8, // XK_kana_KU
|
||||||
|
0x30b1: 0x04b9, // XK_kana_KE
|
||||||
|
0x30b3: 0x04ba, // XK_kana_KO
|
||||||
|
0x30b5: 0x04bb, // XK_kana_SA
|
||||||
|
0x30b7: 0x04bc, // XK_kana_SHI
|
||||||
|
0x30b9: 0x04bd, // XK_kana_SU
|
||||||
|
0x30bb: 0x04be, // XK_kana_SE
|
||||||
|
0x30bd: 0x04bf, // XK_kana_SO
|
||||||
|
0x30bf: 0x04c0, // XK_kana_TA
|
||||||
|
0x30c1: 0x04c1, // XK_kana_CHI
|
||||||
|
0x30c3: 0x04af, // XK_kana_tsu
|
||||||
|
0x30c4: 0x04c2, // XK_kana_TSU
|
||||||
|
0x30c6: 0x04c3, // XK_kana_TE
|
||||||
|
0x30c8: 0x04c4, // XK_kana_TO
|
||||||
|
0x30ca: 0x04c5, // XK_kana_NA
|
||||||
|
0x30cb: 0x04c6, // XK_kana_NI
|
||||||
|
0x30cc: 0x04c7, // XK_kana_NU
|
||||||
|
0x30cd: 0x04c8, // XK_kana_NE
|
||||||
|
0x30ce: 0x04c9, // XK_kana_NO
|
||||||
|
0x30cf: 0x04ca, // XK_kana_HA
|
||||||
|
0x30d2: 0x04cb, // XK_kana_HI
|
||||||
|
0x30d5: 0x04cc, // XK_kana_FU
|
||||||
|
0x30d8: 0x04cd, // XK_kana_HE
|
||||||
|
0x30db: 0x04ce, // XK_kana_HO
|
||||||
|
0x30de: 0x04cf, // XK_kana_MA
|
||||||
|
0x30df: 0x04d0, // XK_kana_MI
|
||||||
|
0x30e0: 0x04d1, // XK_kana_MU
|
||||||
|
0x30e1: 0x04d2, // XK_kana_ME
|
||||||
|
0x30e2: 0x04d3, // XK_kana_MO
|
||||||
|
0x30e3: 0x04ac, // XK_kana_ya
|
||||||
|
0x30e4: 0x04d4, // XK_kana_YA
|
||||||
|
0x30e5: 0x04ad, // XK_kana_yu
|
||||||
|
0x30e6: 0x04d5, // XK_kana_YU
|
||||||
|
0x30e7: 0x04ae, // XK_kana_yo
|
||||||
|
0x30e8: 0x04d6, // XK_kana_YO
|
||||||
|
0x30e9: 0x04d7, // XK_kana_RA
|
||||||
|
0x30ea: 0x04d8, // XK_kana_RI
|
||||||
|
0x30eb: 0x04d9, // XK_kana_RU
|
||||||
|
0x30ec: 0x04da, // XK_kana_RE
|
||||||
|
0x30ed: 0x04db, // XK_kana_RO
|
||||||
|
0x30ef: 0x04dc, // XK_kana_WA
|
||||||
|
0x30f2: 0x04a6, // XK_kana_WO
|
||||||
|
0x30f3: 0x04dd, // XK_kana_N
|
||||||
|
0x30fb: 0x04a5, // XK_kana_conjunctive
|
||||||
|
0x30fc: 0x04b0, // XK_prolongedsound
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
lookup(u) {
|
||||||
|
// Latin-1 is one-to-one mapping
|
||||||
|
if ((u >= 0x20) && (u <= 0xff)) {
|
||||||
|
return u;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lookup table (fairly random)
|
||||||
|
const keysym = codepoints[u];
|
||||||
|
if (keysym !== undefined) {
|
||||||
|
return keysym;
|
||||||
|
}
|
||||||
|
|
||||||
|
// General mapping as final fallback
|
||||||
|
return 0x01000000 | u;
|
||||||
|
},
|
||||||
|
};
|
||||||
+191
@@ -0,0 +1,191 @@
|
|||||||
|
import KeyTable from "./keysym.js";
|
||||||
|
import keysyms from "./keysymdef.js";
|
||||||
|
import vkeys from "./vkeys.js";
|
||||||
|
import fixedkeys from "./fixedkeys.js";
|
||||||
|
import DOMKeyTable from "./domkeytable.js";
|
||||||
|
import * as browser from "../util/browser.js";
|
||||||
|
|
||||||
|
// Get 'KeyboardEvent.code', handling legacy browsers
|
||||||
|
export function getKeycode(evt) {
|
||||||
|
// Are we getting proper key identifiers?
|
||||||
|
// (unfortunately Firefox and Chrome are crappy here and gives
|
||||||
|
// us an empty string on some platforms, rather than leaving it
|
||||||
|
// undefined)
|
||||||
|
if (evt.code) {
|
||||||
|
// Mozilla isn't fully in sync with the spec yet
|
||||||
|
switch (evt.code) {
|
||||||
|
case 'OSLeft': return 'MetaLeft';
|
||||||
|
case 'OSRight': return 'MetaRight';
|
||||||
|
}
|
||||||
|
|
||||||
|
return evt.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The de-facto standard is to use Windows Virtual-Key codes
|
||||||
|
// in the 'keyCode' field for non-printable characters
|
||||||
|
if (evt.keyCode in vkeys) {
|
||||||
|
let code = vkeys[evt.keyCode];
|
||||||
|
|
||||||
|
// macOS has messed up this code for some reason
|
||||||
|
if (browser.isMac() && (code === 'ContextMenu')) {
|
||||||
|
code = 'MetaRight';
|
||||||
|
}
|
||||||
|
|
||||||
|
// The keyCode doesn't distinguish between left and right
|
||||||
|
// for the standard modifiers
|
||||||
|
if (evt.location === 2) {
|
||||||
|
switch (code) {
|
||||||
|
case 'ShiftLeft': return 'ShiftRight';
|
||||||
|
case 'ControlLeft': return 'ControlRight';
|
||||||
|
case 'AltLeft': return 'AltRight';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nor a bunch of the numpad keys
|
||||||
|
if (evt.location === 3) {
|
||||||
|
switch (code) {
|
||||||
|
case 'Delete': return 'NumpadDecimal';
|
||||||
|
case 'Insert': return 'Numpad0';
|
||||||
|
case 'End': return 'Numpad1';
|
||||||
|
case 'ArrowDown': return 'Numpad2';
|
||||||
|
case 'PageDown': return 'Numpad3';
|
||||||
|
case 'ArrowLeft': return 'Numpad4';
|
||||||
|
case 'ArrowRight': return 'Numpad6';
|
||||||
|
case 'Home': return 'Numpad7';
|
||||||
|
case 'ArrowUp': return 'Numpad8';
|
||||||
|
case 'PageUp': return 'Numpad9';
|
||||||
|
case 'Enter': return 'NumpadEnter';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Unidentified';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 'KeyboardEvent.key', handling legacy browsers
|
||||||
|
export function getKey(evt) {
|
||||||
|
// Are we getting a proper key value?
|
||||||
|
if ((evt.key !== undefined) && (evt.key !== 'Unidentified')) {
|
||||||
|
// Mozilla isn't fully in sync with the spec yet
|
||||||
|
switch (evt.key) {
|
||||||
|
case 'OS': return 'Meta';
|
||||||
|
case 'LaunchMyComputer': return 'LaunchApplication1';
|
||||||
|
case 'LaunchCalculator': return 'LaunchApplication2';
|
||||||
|
}
|
||||||
|
|
||||||
|
// iOS leaks some OS names
|
||||||
|
switch (evt.key) {
|
||||||
|
case 'UIKeyInputUpArrow': return 'ArrowUp';
|
||||||
|
case 'UIKeyInputDownArrow': return 'ArrowDown';
|
||||||
|
case 'UIKeyInputLeftArrow': return 'ArrowLeft';
|
||||||
|
case 'UIKeyInputRightArrow': return 'ArrowRight';
|
||||||
|
case 'UIKeyInputEscape': return 'Escape';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Broken behaviour in Chrome
|
||||||
|
if ((evt.key === '\x00') && (evt.code === 'NumpadDecimal')) {
|
||||||
|
return 'Delete';
|
||||||
|
}
|
||||||
|
|
||||||
|
return evt.key;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to deduce it based on the physical key
|
||||||
|
const code = getKeycode(evt);
|
||||||
|
if (code in fixedkeys) {
|
||||||
|
return fixedkeys[code];
|
||||||
|
}
|
||||||
|
|
||||||
|
// If that failed, then see if we have a printable character
|
||||||
|
if (evt.charCode) {
|
||||||
|
return String.fromCharCode(evt.charCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point we have nothing left to go on
|
||||||
|
return 'Unidentified';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the most reliable keysym value we can get from a key event
|
||||||
|
export function getKeysym(evt) {
|
||||||
|
const key = getKey(evt);
|
||||||
|
|
||||||
|
if (key === 'Unidentified') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// First look up special keys
|
||||||
|
if (key in DOMKeyTable) {
|
||||||
|
let location = evt.location;
|
||||||
|
|
||||||
|
// Safari screws up location for the right cmd key
|
||||||
|
if ((key === 'Meta') && (location === 0)) {
|
||||||
|
location = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// And for Clear
|
||||||
|
if ((key === 'Clear') && (location === 3)) {
|
||||||
|
let code = getKeycode(evt);
|
||||||
|
if (code === 'NumLock') {
|
||||||
|
location = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((location === undefined) || (location > 3)) {
|
||||||
|
location = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The original Meta key now gets confused with the Windows key
|
||||||
|
// https://bugs.chromium.org/p/chromium/issues/detail?id=1020141
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1232918
|
||||||
|
if (key === 'Meta') {
|
||||||
|
let code = getKeycode(evt);
|
||||||
|
if (code === 'AltLeft') {
|
||||||
|
return KeyTable.XK_Meta_L;
|
||||||
|
} else if (code === 'AltRight') {
|
||||||
|
return KeyTable.XK_Meta_R;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// macOS has Clear instead of NumLock, but the remote system is
|
||||||
|
// probably not macOS, so lying here is probably best...
|
||||||
|
if (key === 'Clear') {
|
||||||
|
let code = getKeycode(evt);
|
||||||
|
if (code === 'NumLock') {
|
||||||
|
return KeyTable.XK_Num_Lock;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Windows sends alternating symbols for some keys when using a
|
||||||
|
// Japanese layout. We have no way of synchronising with the IM
|
||||||
|
// running on the remote system, so we send some combined keysym
|
||||||
|
// instead and hope for the best.
|
||||||
|
if (browser.isWindows()) {
|
||||||
|
switch (key) {
|
||||||
|
case 'Zenkaku':
|
||||||
|
case 'Hankaku':
|
||||||
|
return KeyTable.XK_Zenkaku_Hankaku;
|
||||||
|
case 'Romaji':
|
||||||
|
case 'KanaMode':
|
||||||
|
return KeyTable.XK_Romaji;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DOMKeyTable[key][location];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we need to look at the Unicode symbol instead
|
||||||
|
|
||||||
|
// Special key? (FIXME: Should have been caught earlier)
|
||||||
|
if (key.length !== 1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const codepoint = key.charCodeAt();
|
||||||
|
if (codepoint) {
|
||||||
|
return keysyms.lookup(codepoint);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2018 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Mapping between Microsoft® Windows® Virtual-Key codes and
|
||||||
|
* HTML key codes.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default {
|
||||||
|
0x08: 'Backspace',
|
||||||
|
0x09: 'Tab',
|
||||||
|
0x0a: 'NumpadClear',
|
||||||
|
0x0d: 'Enter',
|
||||||
|
0x10: 'ShiftLeft',
|
||||||
|
0x11: 'ControlLeft',
|
||||||
|
0x12: 'AltLeft',
|
||||||
|
0x13: 'Pause',
|
||||||
|
0x14: 'CapsLock',
|
||||||
|
0x15: 'Lang1',
|
||||||
|
0x19: 'Lang2',
|
||||||
|
0x1b: 'Escape',
|
||||||
|
0x1c: 'Convert',
|
||||||
|
0x1d: 'NonConvert',
|
||||||
|
0x20: 'Space',
|
||||||
|
0x21: 'PageUp',
|
||||||
|
0x22: 'PageDown',
|
||||||
|
0x23: 'End',
|
||||||
|
0x24: 'Home',
|
||||||
|
0x25: 'ArrowLeft',
|
||||||
|
0x26: 'ArrowUp',
|
||||||
|
0x27: 'ArrowRight',
|
||||||
|
0x28: 'ArrowDown',
|
||||||
|
0x29: 'Select',
|
||||||
|
0x2c: 'PrintScreen',
|
||||||
|
0x2d: 'Insert',
|
||||||
|
0x2e: 'Delete',
|
||||||
|
0x2f: 'Help',
|
||||||
|
0x30: 'Digit0',
|
||||||
|
0x31: 'Digit1',
|
||||||
|
0x32: 'Digit2',
|
||||||
|
0x33: 'Digit3',
|
||||||
|
0x34: 'Digit4',
|
||||||
|
0x35: 'Digit5',
|
||||||
|
0x36: 'Digit6',
|
||||||
|
0x37: 'Digit7',
|
||||||
|
0x38: 'Digit8',
|
||||||
|
0x39: 'Digit9',
|
||||||
|
0x5b: 'MetaLeft',
|
||||||
|
0x5c: 'MetaRight',
|
||||||
|
0x5d: 'ContextMenu',
|
||||||
|
0x5f: 'Sleep',
|
||||||
|
0x60: 'Numpad0',
|
||||||
|
0x61: 'Numpad1',
|
||||||
|
0x62: 'Numpad2',
|
||||||
|
0x63: 'Numpad3',
|
||||||
|
0x64: 'Numpad4',
|
||||||
|
0x65: 'Numpad5',
|
||||||
|
0x66: 'Numpad6',
|
||||||
|
0x67: 'Numpad7',
|
||||||
|
0x68: 'Numpad8',
|
||||||
|
0x69: 'Numpad9',
|
||||||
|
0x6a: 'NumpadMultiply',
|
||||||
|
0x6b: 'NumpadAdd',
|
||||||
|
0x6c: 'NumpadDecimal',
|
||||||
|
0x6d: 'NumpadSubtract',
|
||||||
|
0x6e: 'NumpadDecimal', // Duplicate, because buggy on Windows
|
||||||
|
0x6f: 'NumpadDivide',
|
||||||
|
0x70: 'F1',
|
||||||
|
0x71: 'F2',
|
||||||
|
0x72: 'F3',
|
||||||
|
0x73: 'F4',
|
||||||
|
0x74: 'F5',
|
||||||
|
0x75: 'F6',
|
||||||
|
0x76: 'F7',
|
||||||
|
0x77: 'F8',
|
||||||
|
0x78: 'F9',
|
||||||
|
0x79: 'F10',
|
||||||
|
0x7a: 'F11',
|
||||||
|
0x7b: 'F12',
|
||||||
|
0x7c: 'F13',
|
||||||
|
0x7d: 'F14',
|
||||||
|
0x7e: 'F15',
|
||||||
|
0x7f: 'F16',
|
||||||
|
0x80: 'F17',
|
||||||
|
0x81: 'F18',
|
||||||
|
0x82: 'F19',
|
||||||
|
0x83: 'F20',
|
||||||
|
0x84: 'F21',
|
||||||
|
0x85: 'F22',
|
||||||
|
0x86: 'F23',
|
||||||
|
0x87: 'F24',
|
||||||
|
0x90: 'NumLock',
|
||||||
|
0x91: 'ScrollLock',
|
||||||
|
0xa6: 'BrowserBack',
|
||||||
|
0xa7: 'BrowserForward',
|
||||||
|
0xa8: 'BrowserRefresh',
|
||||||
|
0xa9: 'BrowserStop',
|
||||||
|
0xaa: 'BrowserSearch',
|
||||||
|
0xab: 'BrowserFavorites',
|
||||||
|
0xac: 'BrowserHome',
|
||||||
|
0xad: 'AudioVolumeMute',
|
||||||
|
0xae: 'AudioVolumeDown',
|
||||||
|
0xaf: 'AudioVolumeUp',
|
||||||
|
0xb0: 'MediaTrackNext',
|
||||||
|
0xb1: 'MediaTrackPrevious',
|
||||||
|
0xb2: 'MediaStop',
|
||||||
|
0xb3: 'MediaPlayPause',
|
||||||
|
0xb4: 'LaunchMail',
|
||||||
|
0xb5: 'MediaSelect',
|
||||||
|
0xb6: 'LaunchApp1',
|
||||||
|
0xb7: 'LaunchApp2',
|
||||||
|
0xe1: 'AltRight', // Only when it is AltGraph
|
||||||
|
};
|
||||||
+173
@@ -0,0 +1,173 @@
|
|||||||
|
/*
|
||||||
|
* This file is auto-generated from keymaps.csv
|
||||||
|
* Database checksum sha256(76d68c10e97d37fe2ea459e210125ae41796253fb217e900bf2983ade13a7920)
|
||||||
|
* To re-generate, run:
|
||||||
|
* keymap-gen code-map --lang=js keymaps.csv html atset1
|
||||||
|
*/
|
||||||
|
export default {
|
||||||
|
"Again": 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */
|
||||||
|
"AltLeft": 0x38, /* html:AltLeft (AltLeft) -> linux:56 (KEY_LEFTALT) -> atset1:56 */
|
||||||
|
"AltRight": 0xe038, /* html:AltRight (AltRight) -> linux:100 (KEY_RIGHTALT) -> atset1:57400 */
|
||||||
|
"ArrowDown": 0xe050, /* html:ArrowDown (ArrowDown) -> linux:108 (KEY_DOWN) -> atset1:57424 */
|
||||||
|
"ArrowLeft": 0xe04b, /* html:ArrowLeft (ArrowLeft) -> linux:105 (KEY_LEFT) -> atset1:57419 */
|
||||||
|
"ArrowRight": 0xe04d, /* html:ArrowRight (ArrowRight) -> linux:106 (KEY_RIGHT) -> atset1:57421 */
|
||||||
|
"ArrowUp": 0xe048, /* html:ArrowUp (ArrowUp) -> linux:103 (KEY_UP) -> atset1:57416 */
|
||||||
|
"AudioVolumeDown": 0xe02e, /* html:AudioVolumeDown (AudioVolumeDown) -> linux:114 (KEY_VOLUMEDOWN) -> atset1:57390 */
|
||||||
|
"AudioVolumeMute": 0xe020, /* html:AudioVolumeMute (AudioVolumeMute) -> linux:113 (KEY_MUTE) -> atset1:57376 */
|
||||||
|
"AudioVolumeUp": 0xe030, /* html:AudioVolumeUp (AudioVolumeUp) -> linux:115 (KEY_VOLUMEUP) -> atset1:57392 */
|
||||||
|
"Backquote": 0x29, /* html:Backquote (Backquote) -> linux:41 (KEY_GRAVE) -> atset1:41 */
|
||||||
|
"Backslash": 0x2b, /* html:Backslash (Backslash) -> linux:43 (KEY_BACKSLASH) -> atset1:43 */
|
||||||
|
"Backspace": 0xe, /* html:Backspace (Backspace) -> linux:14 (KEY_BACKSPACE) -> atset1:14 */
|
||||||
|
"BracketLeft": 0x1a, /* html:BracketLeft (BracketLeft) -> linux:26 (KEY_LEFTBRACE) -> atset1:26 */
|
||||||
|
"BracketRight": 0x1b, /* html:BracketRight (BracketRight) -> linux:27 (KEY_RIGHTBRACE) -> atset1:27 */
|
||||||
|
"BrowserBack": 0xe06a, /* html:BrowserBack (BrowserBack) -> linux:158 (KEY_BACK) -> atset1:57450 */
|
||||||
|
"BrowserFavorites": 0xe066, /* html:BrowserFavorites (BrowserFavorites) -> linux:156 (KEY_BOOKMARKS) -> atset1:57446 */
|
||||||
|
"BrowserForward": 0xe069, /* html:BrowserForward (BrowserForward) -> linux:159 (KEY_FORWARD) -> atset1:57449 */
|
||||||
|
"BrowserHome": 0xe032, /* html:BrowserHome (BrowserHome) -> linux:172 (KEY_HOMEPAGE) -> atset1:57394 */
|
||||||
|
"BrowserRefresh": 0xe067, /* html:BrowserRefresh (BrowserRefresh) -> linux:173 (KEY_REFRESH) -> atset1:57447 */
|
||||||
|
"BrowserSearch": 0xe065, /* html:BrowserSearch (BrowserSearch) -> linux:217 (KEY_SEARCH) -> atset1:57445 */
|
||||||
|
"BrowserStop": 0xe068, /* html:BrowserStop (BrowserStop) -> linux:128 (KEY_STOP) -> atset1:57448 */
|
||||||
|
"CapsLock": 0x3a, /* html:CapsLock (CapsLock) -> linux:58 (KEY_CAPSLOCK) -> atset1:58 */
|
||||||
|
"Comma": 0x33, /* html:Comma (Comma) -> linux:51 (KEY_COMMA) -> atset1:51 */
|
||||||
|
"ContextMenu": 0xe05d, /* html:ContextMenu (ContextMenu) -> linux:127 (KEY_COMPOSE) -> atset1:57437 */
|
||||||
|
"ControlLeft": 0x1d, /* html:ControlLeft (ControlLeft) -> linux:29 (KEY_LEFTCTRL) -> atset1:29 */
|
||||||
|
"ControlRight": 0xe01d, /* html:ControlRight (ControlRight) -> linux:97 (KEY_RIGHTCTRL) -> atset1:57373 */
|
||||||
|
"Convert": 0x79, /* html:Convert (Convert) -> linux:92 (KEY_HENKAN) -> atset1:121 */
|
||||||
|
"Copy": 0xe078, /* html:Copy (Copy) -> linux:133 (KEY_COPY) -> atset1:57464 */
|
||||||
|
"Cut": 0xe03c, /* html:Cut (Cut) -> linux:137 (KEY_CUT) -> atset1:57404 */
|
||||||
|
"Delete": 0xe053, /* html:Delete (Delete) -> linux:111 (KEY_DELETE) -> atset1:57427 */
|
||||||
|
"Digit0": 0xb, /* html:Digit0 (Digit0) -> linux:11 (KEY_0) -> atset1:11 */
|
||||||
|
"Digit1": 0x2, /* html:Digit1 (Digit1) -> linux:2 (KEY_1) -> atset1:2 */
|
||||||
|
"Digit2": 0x3, /* html:Digit2 (Digit2) -> linux:3 (KEY_2) -> atset1:3 */
|
||||||
|
"Digit3": 0x4, /* html:Digit3 (Digit3) -> linux:4 (KEY_3) -> atset1:4 */
|
||||||
|
"Digit4": 0x5, /* html:Digit4 (Digit4) -> linux:5 (KEY_4) -> atset1:5 */
|
||||||
|
"Digit5": 0x6, /* html:Digit5 (Digit5) -> linux:6 (KEY_5) -> atset1:6 */
|
||||||
|
"Digit6": 0x7, /* html:Digit6 (Digit6) -> linux:7 (KEY_6) -> atset1:7 */
|
||||||
|
"Digit7": 0x8, /* html:Digit7 (Digit7) -> linux:8 (KEY_7) -> atset1:8 */
|
||||||
|
"Digit8": 0x9, /* html:Digit8 (Digit8) -> linux:9 (KEY_8) -> atset1:9 */
|
||||||
|
"Digit9": 0xa, /* html:Digit9 (Digit9) -> linux:10 (KEY_9) -> atset1:10 */
|
||||||
|
"Eject": 0xe07d, /* html:Eject (Eject) -> linux:162 (KEY_EJECTCLOSECD) -> atset1:57469 */
|
||||||
|
"End": 0xe04f, /* html:End (End) -> linux:107 (KEY_END) -> atset1:57423 */
|
||||||
|
"Enter": 0x1c, /* html:Enter (Enter) -> linux:28 (KEY_ENTER) -> atset1:28 */
|
||||||
|
"Equal": 0xd, /* html:Equal (Equal) -> linux:13 (KEY_EQUAL) -> atset1:13 */
|
||||||
|
"Escape": 0x1, /* html:Escape (Escape) -> linux:1 (KEY_ESC) -> atset1:1 */
|
||||||
|
"F1": 0x3b, /* html:F1 (F1) -> linux:59 (KEY_F1) -> atset1:59 */
|
||||||
|
"F10": 0x44, /* html:F10 (F10) -> linux:68 (KEY_F10) -> atset1:68 */
|
||||||
|
"F11": 0x57, /* html:F11 (F11) -> linux:87 (KEY_F11) -> atset1:87 */
|
||||||
|
"F12": 0x58, /* html:F12 (F12) -> linux:88 (KEY_F12) -> atset1:88 */
|
||||||
|
"F13": 0x5d, /* html:F13 (F13) -> linux:183 (KEY_F13) -> atset1:93 */
|
||||||
|
"F14": 0x5e, /* html:F14 (F14) -> linux:184 (KEY_F14) -> atset1:94 */
|
||||||
|
"F15": 0x5f, /* html:F15 (F15) -> linux:185 (KEY_F15) -> atset1:95 */
|
||||||
|
"F16": 0x55, /* html:F16 (F16) -> linux:186 (KEY_F16) -> atset1:85 */
|
||||||
|
"F17": 0xe003, /* html:F17 (F17) -> linux:187 (KEY_F17) -> atset1:57347 */
|
||||||
|
"F18": 0xe077, /* html:F18 (F18) -> linux:188 (KEY_F18) -> atset1:57463 */
|
||||||
|
"F19": 0xe004, /* html:F19 (F19) -> linux:189 (KEY_F19) -> atset1:57348 */
|
||||||
|
"F2": 0x3c, /* html:F2 (F2) -> linux:60 (KEY_F2) -> atset1:60 */
|
||||||
|
"F20": 0x5a, /* html:F20 (F20) -> linux:190 (KEY_F20) -> atset1:90 */
|
||||||
|
"F21": 0x74, /* html:F21 (F21) -> linux:191 (KEY_F21) -> atset1:116 */
|
||||||
|
"F22": 0xe079, /* html:F22 (F22) -> linux:192 (KEY_F22) -> atset1:57465 */
|
||||||
|
"F23": 0x6d, /* html:F23 (F23) -> linux:193 (KEY_F23) -> atset1:109 */
|
||||||
|
"F24": 0x6f, /* html:F24 (F24) -> linux:194 (KEY_F24) -> atset1:111 */
|
||||||
|
"F3": 0x3d, /* html:F3 (F3) -> linux:61 (KEY_F3) -> atset1:61 */
|
||||||
|
"F4": 0x3e, /* html:F4 (F4) -> linux:62 (KEY_F4) -> atset1:62 */
|
||||||
|
"F5": 0x3f, /* html:F5 (F5) -> linux:63 (KEY_F5) -> atset1:63 */
|
||||||
|
"F6": 0x40, /* html:F6 (F6) -> linux:64 (KEY_F6) -> atset1:64 */
|
||||||
|
"F7": 0x41, /* html:F7 (F7) -> linux:65 (KEY_F7) -> atset1:65 */
|
||||||
|
"F8": 0x42, /* html:F8 (F8) -> linux:66 (KEY_F8) -> atset1:66 */
|
||||||
|
"F9": 0x43, /* html:F9 (F9) -> linux:67 (KEY_F9) -> atset1:67 */
|
||||||
|
"Find": 0xe041, /* html:Find (Find) -> linux:136 (KEY_FIND) -> atset1:57409 */
|
||||||
|
"Help": 0xe075, /* html:Help (Help) -> linux:138 (KEY_HELP) -> atset1:57461 */
|
||||||
|
"Hiragana": 0x77, /* html:Hiragana (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */
|
||||||
|
"Home": 0xe047, /* html:Home (Home) -> linux:102 (KEY_HOME) -> atset1:57415 */
|
||||||
|
"Insert": 0xe052, /* html:Insert (Insert) -> linux:110 (KEY_INSERT) -> atset1:57426 */
|
||||||
|
"IntlBackslash": 0x56, /* html:IntlBackslash (IntlBackslash) -> linux:86 (KEY_102ND) -> atset1:86 */
|
||||||
|
"IntlRo": 0x73, /* html:IntlRo (IntlRo) -> linux:89 (KEY_RO) -> atset1:115 */
|
||||||
|
"IntlYen": 0x7d, /* html:IntlYen (IntlYen) -> linux:124 (KEY_YEN) -> atset1:125 */
|
||||||
|
"KanaMode": 0x70, /* html:KanaMode (KanaMode) -> linux:93 (KEY_KATAKANAHIRAGANA) -> atset1:112 */
|
||||||
|
"Katakana": 0x78, /* html:Katakana (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */
|
||||||
|
"KeyA": 0x1e, /* html:KeyA (KeyA) -> linux:30 (KEY_A) -> atset1:30 */
|
||||||
|
"KeyB": 0x30, /* html:KeyB (KeyB) -> linux:48 (KEY_B) -> atset1:48 */
|
||||||
|
"KeyC": 0x2e, /* html:KeyC (KeyC) -> linux:46 (KEY_C) -> atset1:46 */
|
||||||
|
"KeyD": 0x20, /* html:KeyD (KeyD) -> linux:32 (KEY_D) -> atset1:32 */
|
||||||
|
"KeyE": 0x12, /* html:KeyE (KeyE) -> linux:18 (KEY_E) -> atset1:18 */
|
||||||
|
"KeyF": 0x21, /* html:KeyF (KeyF) -> linux:33 (KEY_F) -> atset1:33 */
|
||||||
|
"KeyG": 0x22, /* html:KeyG (KeyG) -> linux:34 (KEY_G) -> atset1:34 */
|
||||||
|
"KeyH": 0x23, /* html:KeyH (KeyH) -> linux:35 (KEY_H) -> atset1:35 */
|
||||||
|
"KeyI": 0x17, /* html:KeyI (KeyI) -> linux:23 (KEY_I) -> atset1:23 */
|
||||||
|
"KeyJ": 0x24, /* html:KeyJ (KeyJ) -> linux:36 (KEY_J) -> atset1:36 */
|
||||||
|
"KeyK": 0x25, /* html:KeyK (KeyK) -> linux:37 (KEY_K) -> atset1:37 */
|
||||||
|
"KeyL": 0x26, /* html:KeyL (KeyL) -> linux:38 (KEY_L) -> atset1:38 */
|
||||||
|
"KeyM": 0x32, /* html:KeyM (KeyM) -> linux:50 (KEY_M) -> atset1:50 */
|
||||||
|
"KeyN": 0x31, /* html:KeyN (KeyN) -> linux:49 (KEY_N) -> atset1:49 */
|
||||||
|
"KeyO": 0x18, /* html:KeyO (KeyO) -> linux:24 (KEY_O) -> atset1:24 */
|
||||||
|
"KeyP": 0x19, /* html:KeyP (KeyP) -> linux:25 (KEY_P) -> atset1:25 */
|
||||||
|
"KeyQ": 0x10, /* html:KeyQ (KeyQ) -> linux:16 (KEY_Q) -> atset1:16 */
|
||||||
|
"KeyR": 0x13, /* html:KeyR (KeyR) -> linux:19 (KEY_R) -> atset1:19 */
|
||||||
|
"KeyS": 0x1f, /* html:KeyS (KeyS) -> linux:31 (KEY_S) -> atset1:31 */
|
||||||
|
"KeyT": 0x14, /* html:KeyT (KeyT) -> linux:20 (KEY_T) -> atset1:20 */
|
||||||
|
"KeyU": 0x16, /* html:KeyU (KeyU) -> linux:22 (KEY_U) -> atset1:22 */
|
||||||
|
"KeyV": 0x2f, /* html:KeyV (KeyV) -> linux:47 (KEY_V) -> atset1:47 */
|
||||||
|
"KeyW": 0x11, /* html:KeyW (KeyW) -> linux:17 (KEY_W) -> atset1:17 */
|
||||||
|
"KeyX": 0x2d, /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */
|
||||||
|
"KeyY": 0x15, /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */
|
||||||
|
"KeyZ": 0x2c, /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */
|
||||||
|
"Lang1": 0x72, /* html:Lang1 (Lang1) -> linux:122 (KEY_HANGEUL) -> atset1:114 */
|
||||||
|
"Lang2": 0x71, /* html:Lang2 (Lang2) -> linux:123 (KEY_HANJA) -> atset1:113 */
|
||||||
|
"Lang3": 0x78, /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */
|
||||||
|
"Lang4": 0x77, /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */
|
||||||
|
"Lang5": 0x76, /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */
|
||||||
|
"LaunchApp1": 0xe06b, /* html:LaunchApp1 (LaunchApp1) -> linux:157 (KEY_COMPUTER) -> atset1:57451 */
|
||||||
|
"LaunchApp2": 0xe021, /* html:LaunchApp2 (LaunchApp2) -> linux:140 (KEY_CALC) -> atset1:57377 */
|
||||||
|
"LaunchMail": 0xe06c, /* html:LaunchMail (LaunchMail) -> linux:155 (KEY_MAIL) -> atset1:57452 */
|
||||||
|
"MediaPlayPause": 0xe022, /* html:MediaPlayPause (MediaPlayPause) -> linux:164 (KEY_PLAYPAUSE) -> atset1:57378 */
|
||||||
|
"MediaSelect": 0xe06d, /* html:MediaSelect (MediaSelect) -> linux:226 (KEY_MEDIA) -> atset1:57453 */
|
||||||
|
"MediaStop": 0xe024, /* html:MediaStop (MediaStop) -> linux:166 (KEY_STOPCD) -> atset1:57380 */
|
||||||
|
"MediaTrackNext": 0xe019, /* html:MediaTrackNext (MediaTrackNext) -> linux:163 (KEY_NEXTSONG) -> atset1:57369 */
|
||||||
|
"MediaTrackPrevious": 0xe010, /* html:MediaTrackPrevious (MediaTrackPrevious) -> linux:165 (KEY_PREVIOUSSONG) -> atset1:57360 */
|
||||||
|
"MetaLeft": 0xe05b, /* html:MetaLeft (MetaLeft) -> linux:125 (KEY_LEFTMETA) -> atset1:57435 */
|
||||||
|
"MetaRight": 0xe05c, /* html:MetaRight (MetaRight) -> linux:126 (KEY_RIGHTMETA) -> atset1:57436 */
|
||||||
|
"Minus": 0xc, /* html:Minus (Minus) -> linux:12 (KEY_MINUS) -> atset1:12 */
|
||||||
|
"NonConvert": 0x7b, /* html:NonConvert (NonConvert) -> linux:94 (KEY_MUHENKAN) -> atset1:123 */
|
||||||
|
"NumLock": 0x45, /* html:NumLock (NumLock) -> linux:69 (KEY_NUMLOCK) -> atset1:69 */
|
||||||
|
"Numpad0": 0x52, /* html:Numpad0 (Numpad0) -> linux:82 (KEY_KP0) -> atset1:82 */
|
||||||
|
"Numpad1": 0x4f, /* html:Numpad1 (Numpad1) -> linux:79 (KEY_KP1) -> atset1:79 */
|
||||||
|
"Numpad2": 0x50, /* html:Numpad2 (Numpad2) -> linux:80 (KEY_KP2) -> atset1:80 */
|
||||||
|
"Numpad3": 0x51, /* html:Numpad3 (Numpad3) -> linux:81 (KEY_KP3) -> atset1:81 */
|
||||||
|
"Numpad4": 0x4b, /* html:Numpad4 (Numpad4) -> linux:75 (KEY_KP4) -> atset1:75 */
|
||||||
|
"Numpad5": 0x4c, /* html:Numpad5 (Numpad5) -> linux:76 (KEY_KP5) -> atset1:76 */
|
||||||
|
"Numpad6": 0x4d, /* html:Numpad6 (Numpad6) -> linux:77 (KEY_KP6) -> atset1:77 */
|
||||||
|
"Numpad7": 0x47, /* html:Numpad7 (Numpad7) -> linux:71 (KEY_KP7) -> atset1:71 */
|
||||||
|
"Numpad8": 0x48, /* html:Numpad8 (Numpad8) -> linux:72 (KEY_KP8) -> atset1:72 */
|
||||||
|
"Numpad9": 0x49, /* html:Numpad9 (Numpad9) -> linux:73 (KEY_KP9) -> atset1:73 */
|
||||||
|
"NumpadAdd": 0x4e, /* html:NumpadAdd (NumpadAdd) -> linux:78 (KEY_KPPLUS) -> atset1:78 */
|
||||||
|
"NumpadComma": 0x7e, /* html:NumpadComma (NumpadComma) -> linux:121 (KEY_KPCOMMA) -> atset1:126 */
|
||||||
|
"NumpadDecimal": 0x53, /* html:NumpadDecimal (NumpadDecimal) -> linux:83 (KEY_KPDOT) -> atset1:83 */
|
||||||
|
"NumpadDivide": 0xe035, /* html:NumpadDivide (NumpadDivide) -> linux:98 (KEY_KPSLASH) -> atset1:57397 */
|
||||||
|
"NumpadEnter": 0xe01c, /* html:NumpadEnter (NumpadEnter) -> linux:96 (KEY_KPENTER) -> atset1:57372 */
|
||||||
|
"NumpadEqual": 0x59, /* html:NumpadEqual (NumpadEqual) -> linux:117 (KEY_KPEQUAL) -> atset1:89 */
|
||||||
|
"NumpadMultiply": 0x37, /* html:NumpadMultiply (NumpadMultiply) -> linux:55 (KEY_KPASTERISK) -> atset1:55 */
|
||||||
|
"NumpadParenLeft": 0xe076, /* html:NumpadParenLeft (NumpadParenLeft) -> linux:179 (KEY_KPLEFTPAREN) -> atset1:57462 */
|
||||||
|
"NumpadParenRight": 0xe07b, /* html:NumpadParenRight (NumpadParenRight) -> linux:180 (KEY_KPRIGHTPAREN) -> atset1:57467 */
|
||||||
|
"NumpadSubtract": 0x4a, /* html:NumpadSubtract (NumpadSubtract) -> linux:74 (KEY_KPMINUS) -> atset1:74 */
|
||||||
|
"Open": 0x64, /* html:Open (Open) -> linux:134 (KEY_OPEN) -> atset1:100 */
|
||||||
|
"PageDown": 0xe051, /* html:PageDown (PageDown) -> linux:109 (KEY_PAGEDOWN) -> atset1:57425 */
|
||||||
|
"PageUp": 0xe049, /* html:PageUp (PageUp) -> linux:104 (KEY_PAGEUP) -> atset1:57417 */
|
||||||
|
"Paste": 0x65, /* html:Paste (Paste) -> linux:135 (KEY_PASTE) -> atset1:101 */
|
||||||
|
"Pause": 0xe046, /* html:Pause (Pause) -> linux:119 (KEY_PAUSE) -> atset1:57414 */
|
||||||
|
"Period": 0x34, /* html:Period (Period) -> linux:52 (KEY_DOT) -> atset1:52 */
|
||||||
|
"Power": 0xe05e, /* html:Power (Power) -> linux:116 (KEY_POWER) -> atset1:57438 */
|
||||||
|
"PrintScreen": 0x54, /* html:PrintScreen (PrintScreen) -> linux:99 (KEY_SYSRQ) -> atset1:84 */
|
||||||
|
"Props": 0xe006, /* html:Props (Props) -> linux:130 (KEY_PROPS) -> atset1:57350 */
|
||||||
|
"Quote": 0x28, /* html:Quote (Quote) -> linux:40 (KEY_APOSTROPHE) -> atset1:40 */
|
||||||
|
"ScrollLock": 0x46, /* html:ScrollLock (ScrollLock) -> linux:70 (KEY_SCROLLLOCK) -> atset1:70 */
|
||||||
|
"Semicolon": 0x27, /* html:Semicolon (Semicolon) -> linux:39 (KEY_SEMICOLON) -> atset1:39 */
|
||||||
|
"ShiftLeft": 0x2a, /* html:ShiftLeft (ShiftLeft) -> linux:42 (KEY_LEFTSHIFT) -> atset1:42 */
|
||||||
|
"ShiftRight": 0x36, /* html:ShiftRight (ShiftRight) -> linux:54 (KEY_RIGHTSHIFT) -> atset1:54 */
|
||||||
|
"Slash": 0x35, /* html:Slash (Slash) -> linux:53 (KEY_SLASH) -> atset1:53 */
|
||||||
|
"Sleep": 0xe05f, /* html:Sleep (Sleep) -> linux:142 (KEY_SLEEP) -> atset1:57439 */
|
||||||
|
"Space": 0x39, /* html:Space (Space) -> linux:57 (KEY_SPACE) -> atset1:57 */
|
||||||
|
"Suspend": 0xe025, /* html:Suspend (Suspend) -> linux:205 (KEY_SUSPEND) -> atset1:57381 */
|
||||||
|
"Tab": 0xf, /* html:Tab (Tab) -> linux:15 (KEY_TAB) -> atset1:15 */
|
||||||
|
"Undo": 0xe007, /* html:Undo (Undo) -> linux:131 (KEY_UNDO) -> atset1:57351 */
|
||||||
|
"WakeUp": 0xe063, /* html:WakeUp (WakeUp) -> linux:143 (KEY_WAKEUP) -> atset1:57443 */
|
||||||
|
};
|
||||||
@@ -0,0 +1,312 @@
|
|||||||
|
import { encodeUTF8 } from './util/strings.js';
|
||||||
|
import EventTargetMixin from './util/eventtarget.js';
|
||||||
|
import legacyCrypto from './crypto/crypto.js';
|
||||||
|
|
||||||
|
class RA2Cipher {
|
||||||
|
constructor() {
|
||||||
|
this._cipher = null;
|
||||||
|
this._counter = new Uint8Array(16);
|
||||||
|
}
|
||||||
|
|
||||||
|
async setKey(key) {
|
||||||
|
this._cipher = await legacyCrypto.importKey(
|
||||||
|
"raw", key, { name: "AES-EAX" }, false, ["encrypt, decrypt"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async makeMessage(message) {
|
||||||
|
const ad = new Uint8Array([(message.length & 0xff00) >>> 8, message.length & 0xff]);
|
||||||
|
const encrypted = await legacyCrypto.encrypt({
|
||||||
|
name: "AES-EAX",
|
||||||
|
iv: this._counter,
|
||||||
|
additionalData: ad,
|
||||||
|
}, this._cipher, message);
|
||||||
|
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
||||||
|
const res = new Uint8Array(message.length + 2 + 16);
|
||||||
|
res.set(ad);
|
||||||
|
res.set(encrypted, 2);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
async receiveMessage(length, encrypted) {
|
||||||
|
const ad = new Uint8Array([(length & 0xff00) >>> 8, length & 0xff]);
|
||||||
|
const res = await legacyCrypto.decrypt({
|
||||||
|
name: "AES-EAX",
|
||||||
|
iv: this._counter,
|
||||||
|
additionalData: ad,
|
||||||
|
}, this._cipher, encrypted);
|
||||||
|
for (let i = 0; i < 16 && this._counter[i]++ === 255; i++);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class RSAAESAuthenticationState extends EventTargetMixin {
|
||||||
|
constructor(sock, getCredentials) {
|
||||||
|
super();
|
||||||
|
this._hasStarted = false;
|
||||||
|
this._checkSock = null;
|
||||||
|
this._checkCredentials = null;
|
||||||
|
this._approveServerResolve = null;
|
||||||
|
this._sockReject = null;
|
||||||
|
this._credentialsReject = null;
|
||||||
|
this._approveServerReject = null;
|
||||||
|
this._sock = sock;
|
||||||
|
this._getCredentials = getCredentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
_waitSockAsync(len) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const hasData = () => !this._sock.rQwait('RA2', len);
|
||||||
|
if (hasData()) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
this._checkSock = () => {
|
||||||
|
if (hasData()) {
|
||||||
|
resolve();
|
||||||
|
this._checkSock = null;
|
||||||
|
this._sockReject = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._sockReject = reject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_waitApproveKeyAsync() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this._approveServerResolve = resolve;
|
||||||
|
this._approveServerReject = reject;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_waitCredentialsAsync(subtype) {
|
||||||
|
const hasCredentials = () => {
|
||||||
|
if (subtype === 1 && this._getCredentials().username !== undefined &&
|
||||||
|
this._getCredentials().password !== undefined) {
|
||||||
|
return true;
|
||||||
|
} else if (subtype === 2 && this._getCredentials().password !== undefined) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (hasCredentials()) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
this._checkCredentials = () => {
|
||||||
|
if (hasCredentials()) {
|
||||||
|
resolve();
|
||||||
|
this._checkCredentials = null;
|
||||||
|
this._credentialsReject = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this._credentialsReject = reject;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
checkInternalEvents() {
|
||||||
|
if (this._checkSock !== null) {
|
||||||
|
this._checkSock();
|
||||||
|
}
|
||||||
|
if (this._checkCredentials !== null) {
|
||||||
|
this._checkCredentials();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
approveServer() {
|
||||||
|
if (this._approveServerResolve !== null) {
|
||||||
|
this._approveServerResolve();
|
||||||
|
this._approveServerResolve = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
if (this._sockReject !== null) {
|
||||||
|
this._sockReject(new Error("disconnect normally"));
|
||||||
|
this._sockReject = null;
|
||||||
|
}
|
||||||
|
if (this._credentialsReject !== null) {
|
||||||
|
this._credentialsReject(new Error("disconnect normally"));
|
||||||
|
this._credentialsReject = null;
|
||||||
|
}
|
||||||
|
if (this._approveServerReject !== null) {
|
||||||
|
this._approveServerReject(new Error("disconnect normally"));
|
||||||
|
this._approveServerReject = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async negotiateRA2neAuthAsync() {
|
||||||
|
this._hasStarted = true;
|
||||||
|
// 1: Receive server public key
|
||||||
|
await this._waitSockAsync(4);
|
||||||
|
const serverKeyLengthBuffer = this._sock.rQpeekBytes(4);
|
||||||
|
const serverKeyLength = this._sock.rQshift32();
|
||||||
|
if (serverKeyLength < 1024) {
|
||||||
|
throw new Error("RA2: server public key is too short: " + serverKeyLength);
|
||||||
|
} else if (serverKeyLength > 8192) {
|
||||||
|
throw new Error("RA2: server public key is too long: " + serverKeyLength);
|
||||||
|
}
|
||||||
|
const serverKeyBytes = Math.ceil(serverKeyLength / 8);
|
||||||
|
await this._waitSockAsync(serverKeyBytes * 2);
|
||||||
|
const serverN = this._sock.rQshiftBytes(serverKeyBytes);
|
||||||
|
const serverE = this._sock.rQshiftBytes(serverKeyBytes);
|
||||||
|
const serverRSACipher = await legacyCrypto.importKey(
|
||||||
|
"raw", { n: serverN, e: serverE }, { name: "RSA-PKCS1-v1_5" }, false, ["encrypt"]);
|
||||||
|
const serverPublickey = new Uint8Array(4 + serverKeyBytes * 2);
|
||||||
|
serverPublickey.set(serverKeyLengthBuffer);
|
||||||
|
serverPublickey.set(serverN, 4);
|
||||||
|
serverPublickey.set(serverE, 4 + serverKeyBytes);
|
||||||
|
|
||||||
|
// verify server public key
|
||||||
|
let approveKey = this._waitApproveKeyAsync();
|
||||||
|
this.dispatchEvent(new CustomEvent("serververification", {
|
||||||
|
detail: { type: "RSA", publickey: serverPublickey }
|
||||||
|
}));
|
||||||
|
await approveKey;
|
||||||
|
|
||||||
|
// 2: Send client public key
|
||||||
|
const clientKeyLength = 2048;
|
||||||
|
const clientKeyBytes = Math.ceil(clientKeyLength / 8);
|
||||||
|
const clientRSACipher = (await legacyCrypto.generateKey({
|
||||||
|
name: "RSA-PKCS1-v1_5",
|
||||||
|
modulusLength: clientKeyLength,
|
||||||
|
publicExponent: new Uint8Array([1, 0, 1]),
|
||||||
|
}, true, ["encrypt"])).privateKey;
|
||||||
|
const clientExportedRSAKey = await legacyCrypto.exportKey("raw", clientRSACipher);
|
||||||
|
const clientN = clientExportedRSAKey.n;
|
||||||
|
const clientE = clientExportedRSAKey.e;
|
||||||
|
const clientPublicKey = new Uint8Array(4 + clientKeyBytes * 2);
|
||||||
|
clientPublicKey[0] = (clientKeyLength & 0xff000000) >>> 24;
|
||||||
|
clientPublicKey[1] = (clientKeyLength & 0xff0000) >>> 16;
|
||||||
|
clientPublicKey[2] = (clientKeyLength & 0xff00) >>> 8;
|
||||||
|
clientPublicKey[3] = clientKeyLength & 0xff;
|
||||||
|
clientPublicKey.set(clientN, 4);
|
||||||
|
clientPublicKey.set(clientE, 4 + clientKeyBytes);
|
||||||
|
this._sock.sQpushBytes(clientPublicKey);
|
||||||
|
this._sock.flush();
|
||||||
|
|
||||||
|
// 3: Send client random
|
||||||
|
const clientRandom = new Uint8Array(16);
|
||||||
|
window.crypto.getRandomValues(clientRandom);
|
||||||
|
const clientEncryptedRandom = await legacyCrypto.encrypt(
|
||||||
|
{ name: "RSA-PKCS1-v1_5" }, serverRSACipher, clientRandom);
|
||||||
|
const clientRandomMessage = new Uint8Array(2 + serverKeyBytes);
|
||||||
|
clientRandomMessage[0] = (serverKeyBytes & 0xff00) >>> 8;
|
||||||
|
clientRandomMessage[1] = serverKeyBytes & 0xff;
|
||||||
|
clientRandomMessage.set(clientEncryptedRandom, 2);
|
||||||
|
this._sock.sQpushBytes(clientRandomMessage);
|
||||||
|
this._sock.flush();
|
||||||
|
|
||||||
|
// 4: Receive server random
|
||||||
|
await this._waitSockAsync(2);
|
||||||
|
if (this._sock.rQshift16() !== clientKeyBytes) {
|
||||||
|
throw new Error("RA2: wrong encrypted message length");
|
||||||
|
}
|
||||||
|
const serverEncryptedRandom = this._sock.rQshiftBytes(clientKeyBytes);
|
||||||
|
const serverRandom = await legacyCrypto.decrypt(
|
||||||
|
{ name: "RSA-PKCS1-v1_5" }, clientRSACipher, serverEncryptedRandom);
|
||||||
|
if (serverRandom === null || serverRandom.length !== 16) {
|
||||||
|
throw new Error("RA2: corrupted server encrypted random");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5: Compute session keys and set ciphers
|
||||||
|
let clientSessionKey = new Uint8Array(32);
|
||||||
|
let serverSessionKey = new Uint8Array(32);
|
||||||
|
clientSessionKey.set(serverRandom);
|
||||||
|
clientSessionKey.set(clientRandom, 16);
|
||||||
|
serverSessionKey.set(clientRandom);
|
||||||
|
serverSessionKey.set(serverRandom, 16);
|
||||||
|
clientSessionKey = await window.crypto.subtle.digest("SHA-1", clientSessionKey);
|
||||||
|
clientSessionKey = new Uint8Array(clientSessionKey).slice(0, 16);
|
||||||
|
serverSessionKey = await window.crypto.subtle.digest("SHA-1", serverSessionKey);
|
||||||
|
serverSessionKey = new Uint8Array(serverSessionKey).slice(0, 16);
|
||||||
|
const clientCipher = new RA2Cipher();
|
||||||
|
await clientCipher.setKey(clientSessionKey);
|
||||||
|
const serverCipher = new RA2Cipher();
|
||||||
|
await serverCipher.setKey(serverSessionKey);
|
||||||
|
|
||||||
|
// 6: Compute and exchange hashes
|
||||||
|
let serverHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
|
||||||
|
let clientHash = new Uint8Array(8 + serverKeyBytes * 2 + clientKeyBytes * 2);
|
||||||
|
serverHash.set(serverPublickey);
|
||||||
|
serverHash.set(clientPublicKey, 4 + serverKeyBytes * 2);
|
||||||
|
clientHash.set(clientPublicKey);
|
||||||
|
clientHash.set(serverPublickey, 4 + clientKeyBytes * 2);
|
||||||
|
serverHash = await window.crypto.subtle.digest("SHA-1", serverHash);
|
||||||
|
clientHash = await window.crypto.subtle.digest("SHA-1", clientHash);
|
||||||
|
serverHash = new Uint8Array(serverHash);
|
||||||
|
clientHash = new Uint8Array(clientHash);
|
||||||
|
this._sock.sQpushBytes(await clientCipher.makeMessage(clientHash));
|
||||||
|
this._sock.flush();
|
||||||
|
await this._waitSockAsync(2 + 20 + 16);
|
||||||
|
if (this._sock.rQshift16() !== 20) {
|
||||||
|
throw new Error("RA2: wrong server hash");
|
||||||
|
}
|
||||||
|
const serverHashReceived = await serverCipher.receiveMessage(
|
||||||
|
20, this._sock.rQshiftBytes(20 + 16));
|
||||||
|
if (serverHashReceived === null) {
|
||||||
|
throw new Error("RA2: failed to authenticate the message");
|
||||||
|
}
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
if (serverHashReceived[i] !== serverHash[i]) {
|
||||||
|
throw new Error("RA2: wrong server hash");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7: Receive subtype
|
||||||
|
await this._waitSockAsync(2 + 1 + 16);
|
||||||
|
if (this._sock.rQshift16() !== 1) {
|
||||||
|
throw new Error("RA2: wrong subtype");
|
||||||
|
}
|
||||||
|
let subtype = (await serverCipher.receiveMessage(
|
||||||
|
1, this._sock.rQshiftBytes(1 + 16)));
|
||||||
|
if (subtype === null) {
|
||||||
|
throw new Error("RA2: failed to authenticate the message");
|
||||||
|
}
|
||||||
|
subtype = subtype[0];
|
||||||
|
let waitCredentials = this._waitCredentialsAsync(subtype);
|
||||||
|
if (subtype === 1) {
|
||||||
|
if (this._getCredentials().username === undefined ||
|
||||||
|
this._getCredentials().password === undefined) {
|
||||||
|
this.dispatchEvent(new CustomEvent(
|
||||||
|
"credentialsrequired",
|
||||||
|
{ detail: { types: ["username", "password"] } }));
|
||||||
|
}
|
||||||
|
} else if (subtype === 2) {
|
||||||
|
if (this._getCredentials().password === undefined) {
|
||||||
|
this.dispatchEvent(new CustomEvent(
|
||||||
|
"credentialsrequired",
|
||||||
|
{ detail: { types: ["password"] } }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw new Error("RA2: wrong subtype");
|
||||||
|
}
|
||||||
|
await waitCredentials;
|
||||||
|
let username;
|
||||||
|
if (subtype === 1) {
|
||||||
|
username = encodeUTF8(this._getCredentials().username).slice(0, 255);
|
||||||
|
} else {
|
||||||
|
username = "";
|
||||||
|
}
|
||||||
|
const password = encodeUTF8(this._getCredentials().password).slice(0, 255);
|
||||||
|
const credentials = new Uint8Array(username.length + password.length + 2);
|
||||||
|
credentials[0] = username.length;
|
||||||
|
credentials[username.length + 1] = password.length;
|
||||||
|
for (let i = 0; i < username.length; i++) {
|
||||||
|
credentials[i + 1] = username.charCodeAt(i);
|
||||||
|
}
|
||||||
|
for (let i = 0; i < password.length; i++) {
|
||||||
|
credentials[username.length + 2 + i] = password.charCodeAt(i);
|
||||||
|
}
|
||||||
|
this._sock.sQpushBytes(await clientCipher.makeMessage(credentials));
|
||||||
|
this._sock.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
get hasStarted() {
|
||||||
|
return this._hasStarted;
|
||||||
|
}
|
||||||
|
|
||||||
|
set hasStarted(s) {
|
||||||
|
this._hasStarted = s;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
+233
@@ -0,0 +1,233 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*
|
||||||
|
* Browser feature support detection
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Log from './logging.js';
|
||||||
|
import Base64 from '../base64.js';
|
||||||
|
|
||||||
|
// Touch detection
|
||||||
|
export let isTouchDevice = ('ontouchstart' in document.documentElement) ||
|
||||||
|
// required for Chrome debugger
|
||||||
|
(document.ontouchstart !== undefined) ||
|
||||||
|
// required for MS Surface
|
||||||
|
(navigator.maxTouchPoints > 0) ||
|
||||||
|
(navigator.msMaxTouchPoints > 0);
|
||||||
|
window.addEventListener('touchstart', function onFirstTouch() {
|
||||||
|
isTouchDevice = true;
|
||||||
|
window.removeEventListener('touchstart', onFirstTouch, false);
|
||||||
|
}, false);
|
||||||
|
|
||||||
|
|
||||||
|
// The goal is to find a certain physical width, the devicePixelRatio
|
||||||
|
// brings us a bit closer but is not optimal.
|
||||||
|
export let dragThreshold = 10 * (window.devicePixelRatio || 1);
|
||||||
|
|
||||||
|
let _supportsCursorURIs = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const target = document.createElement('canvas');
|
||||||
|
target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default';
|
||||||
|
|
||||||
|
if (target.style.cursor.indexOf("url") === 0) {
|
||||||
|
Log.Info("Data URI scheme cursor supported");
|
||||||
|
_supportsCursorURIs = true;
|
||||||
|
} else {
|
||||||
|
Log.Warn("Data URI scheme cursor not supported");
|
||||||
|
}
|
||||||
|
} catch (exc) {
|
||||||
|
Log.Error("Data URI scheme cursor test exception: " + exc);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supportsCursorURIs = _supportsCursorURIs;
|
||||||
|
|
||||||
|
let _hasScrollbarGutter = true;
|
||||||
|
try {
|
||||||
|
// Create invisible container
|
||||||
|
const container = document.createElement('div');
|
||||||
|
container.style.visibility = 'hidden';
|
||||||
|
container.style.overflow = 'scroll'; // forcing scrollbars
|
||||||
|
document.body.appendChild(container);
|
||||||
|
|
||||||
|
// Create a div and place it in the container
|
||||||
|
const child = document.createElement('div');
|
||||||
|
container.appendChild(child);
|
||||||
|
|
||||||
|
// Calculate the difference between the container's full width
|
||||||
|
// and the child's width - the difference is the scrollbars
|
||||||
|
const scrollbarWidth = (container.offsetWidth - child.offsetWidth);
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
container.parentNode.removeChild(container);
|
||||||
|
|
||||||
|
_hasScrollbarGutter = scrollbarWidth != 0;
|
||||||
|
} catch (exc) {
|
||||||
|
Log.Error("Scrollbar test exception: " + exc);
|
||||||
|
}
|
||||||
|
export const hasScrollbarGutter = _hasScrollbarGutter;
|
||||||
|
|
||||||
|
export let supportsWebCodecsH264Decode = false;
|
||||||
|
|
||||||
|
async function _checkWebCodecsH264DecodeSupport() {
|
||||||
|
if (!('VideoDecoder' in window)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll need to make do with some placeholders here
|
||||||
|
const config = {
|
||||||
|
codec: 'avc1.42401f',
|
||||||
|
codedWidth: 1920,
|
||||||
|
codedHeight: 1080,
|
||||||
|
optimizeForLatency: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
let support = await VideoDecoder.isConfigSupported(config);
|
||||||
|
if (!support.supported) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firefox incorrectly reports supports for H.264 under some
|
||||||
|
// circumstances, so we need to actually test a real frame
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1932392
|
||||||
|
|
||||||
|
const data = new Uint8Array(Base64.decode(
|
||||||
|
'AAAAAWdCwBTZnpuAgICgAAADACAAAAZB4oVNAAAAAWjJYyyAAAABBgX//4Hc' +
|
||||||
|
'Rem95tlIt5Ys2CDZI+7veDI2NCAtIGNvcmUgMTY0IHIzMTA4IDMxZTE5Zjkg' +
|
||||||
|
'LSBILjI2NC9NUEVHLTQgQVZDIGNvZGVjIC0gQ29weWxlZnQgMjAwMy0yMDIz' +
|
||||||
|
'IC0gaHR0cDovL3d3dy52aWRlb2xhbi5vcmcveDI2NC5odG1sIC0gb3B0aW9u' +
|
||||||
|
'czogY2FiYWM9MCByZWY9NSBkZWJsb2NrPTE6MDowIGFuYWx5c2U9MHgxOjB4' +
|
||||||
|
'MTExIG1lPWhleCBzdWJtZT04IHBzeT0xIHBzeV9yZD0xLjAwOjAuMDAgbWl4' +
|
||||||
|
'ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0yIDh4' +
|
||||||
|
'OGRjdD0wIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJv' +
|
||||||
|
'bWFfcXBfb2Zmc2V0PS0yIHRocmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0x' +
|
||||||
|
'IHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9' +
|
||||||
|
'MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVz' +
|
||||||
|
'PTAgd2VpZ2h0cD0wIGtleWludD1pbmZpbml0ZSBrZXlpbnRfbWluPTI1IHNj' +
|
||||||
|
'ZW5lY3V0PTQwIGludHJhX3JlZnJlc2g9MCByY19sb29rYWhlYWQ9NTAgcmM9' +
|
||||||
|
'YWJyIG1idHJlZT0xIGJpdHJhdGU9NDAwIHJhdGV0b2w9MS4wIHFjb21wPTAu' +
|
||||||
|
'NjAgcXBtaW49MCBxcG1heD02OSBxcHN0ZXA9NCBpcF9yYXRpbz0xLjQwIGFx' +
|
||||||
|
'PTE6MS4wMACAAAABZYiEBrxmKAAPVccAAS044AA5DRJMnkycJk4TPw=='));
|
||||||
|
|
||||||
|
let gotframe = false;
|
||||||
|
let error = null;
|
||||||
|
|
||||||
|
let decoder = new VideoDecoder({
|
||||||
|
output: (frame) => { gotframe = true; },
|
||||||
|
error: (e) => { error = e; },
|
||||||
|
});
|
||||||
|
let chunk = new EncodedVideoChunk({
|
||||||
|
timestamp: 0,
|
||||||
|
type: 'key',
|
||||||
|
data: data,
|
||||||
|
});
|
||||||
|
|
||||||
|
decoder.configure(config);
|
||||||
|
decoder.decode(chunk);
|
||||||
|
try {
|
||||||
|
await decoder.flush();
|
||||||
|
} catch (e) {
|
||||||
|
// Firefox incorrectly throws an exception here
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1932566
|
||||||
|
error = e;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Firefox fails to deliver the error on Windows, so we need to
|
||||||
|
// check if we got a frame instead
|
||||||
|
// https://bugzilla.mozilla.org/show_bug.cgi?id=1932579
|
||||||
|
if (!gotframe) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
supportsWebCodecsH264Decode = await _checkWebCodecsH264DecodeSupport();
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The functions for detection of platforms and browsers below are exported
|
||||||
|
* but the use of these should be minimized as much as possible.
|
||||||
|
*
|
||||||
|
* It's better to use feature detection than platform detection.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* OS */
|
||||||
|
|
||||||
|
export function isMac() {
|
||||||
|
return !!(/mac/i).exec(navigator.platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWindows() {
|
||||||
|
return !!(/win/i).exec(navigator.platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isIOS() {
|
||||||
|
return (!!(/ipad/i).exec(navigator.platform) ||
|
||||||
|
!!(/iphone/i).exec(navigator.platform) ||
|
||||||
|
!!(/ipod/i).exec(navigator.platform));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isAndroid() {
|
||||||
|
/* Android sets navigator.platform to Linux :/ */
|
||||||
|
return !!navigator.userAgent.match('Android ');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isChromeOS() {
|
||||||
|
/* ChromeOS sets navigator.platform to Linux :/ */
|
||||||
|
return !!navigator.userAgent.match(' CrOS ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Browser */
|
||||||
|
|
||||||
|
export function isSafari() {
|
||||||
|
return !!navigator.userAgent.match('Safari/...') &&
|
||||||
|
!navigator.userAgent.match('Chrome/...') &&
|
||||||
|
!navigator.userAgent.match('Chromium/...') &&
|
||||||
|
!navigator.userAgent.match('Epiphany/...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isFirefox() {
|
||||||
|
return !!navigator.userAgent.match('Firefox/...') &&
|
||||||
|
!navigator.userAgent.match('Seamonkey/...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isChrome() {
|
||||||
|
return !!navigator.userAgent.match('Chrome/...') &&
|
||||||
|
!navigator.userAgent.match('Chromium/...') &&
|
||||||
|
!navigator.userAgent.match('Edg/...') &&
|
||||||
|
!navigator.userAgent.match('OPR/...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isChromium() {
|
||||||
|
return !!navigator.userAgent.match('Chromium/...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOpera() {
|
||||||
|
return !!navigator.userAgent.match('OPR/...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isEdge() {
|
||||||
|
return !!navigator.userAgent.match('Edg/...');
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Engine */
|
||||||
|
|
||||||
|
export function isGecko() {
|
||||||
|
return !!navigator.userAgent.match('Gecko/...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isWebKit() {
|
||||||
|
return !!navigator.userAgent.match('AppleWebKit/...') &&
|
||||||
|
!navigator.userAgent.match('Chrome/...');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isBlink() {
|
||||||
|
return !!navigator.userAgent.match('Chrome/...');
|
||||||
|
}
|
||||||
+249
@@ -0,0 +1,249 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 or any later version (see LICENSE.txt)
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { supportsCursorURIs, isTouchDevice } from './browser.js';
|
||||||
|
|
||||||
|
const useFallback = !supportsCursorURIs || isTouchDevice;
|
||||||
|
|
||||||
|
export default class Cursor {
|
||||||
|
constructor() {
|
||||||
|
this._target = null;
|
||||||
|
|
||||||
|
this._canvas = document.createElement('canvas');
|
||||||
|
|
||||||
|
if (useFallback) {
|
||||||
|
this._canvas.style.position = 'fixed';
|
||||||
|
this._canvas.style.zIndex = '65535';
|
||||||
|
this._canvas.style.pointerEvents = 'none';
|
||||||
|
// Safari on iOS can select the cursor image
|
||||||
|
// https://bugs.webkit.org/show_bug.cgi?id=249223
|
||||||
|
this._canvas.style.userSelect = 'none';
|
||||||
|
this._canvas.style.WebkitUserSelect = 'none';
|
||||||
|
// Can't use "display" because of Firefox bug #1445997
|
||||||
|
this._canvas.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
|
||||||
|
this._position = { x: 0, y: 0 };
|
||||||
|
this._hotSpot = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
this._eventHandlers = {
|
||||||
|
'mouseover': this._handleMouseOver.bind(this),
|
||||||
|
'mouseleave': this._handleMouseLeave.bind(this),
|
||||||
|
'mousemove': this._handleMouseMove.bind(this),
|
||||||
|
'mouseup': this._handleMouseUp.bind(this),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
attach(target) {
|
||||||
|
if (this._target) {
|
||||||
|
this.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._target = target;
|
||||||
|
|
||||||
|
if (useFallback) {
|
||||||
|
document.body.appendChild(this._canvas);
|
||||||
|
|
||||||
|
const options = { capture: true, passive: true };
|
||||||
|
this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options);
|
||||||
|
this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options);
|
||||||
|
this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options);
|
||||||
|
this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
detach() {
|
||||||
|
if (!this._target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (useFallback) {
|
||||||
|
const options = { capture: true, passive: true };
|
||||||
|
this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options);
|
||||||
|
this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options);
|
||||||
|
this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options);
|
||||||
|
this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options);
|
||||||
|
|
||||||
|
if (document.contains(this._canvas)) {
|
||||||
|
document.body.removeChild(this._canvas);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._target = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
change(rgba, hotx, hoty, w, h) {
|
||||||
|
if ((w === 0) || (h === 0)) {
|
||||||
|
this.clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._position.x = this._position.x + this._hotSpot.x - hotx;
|
||||||
|
this._position.y = this._position.y + this._hotSpot.y - hoty;
|
||||||
|
this._hotSpot.x = hotx;
|
||||||
|
this._hotSpot.y = hoty;
|
||||||
|
|
||||||
|
let ctx = this._canvas.getContext('2d');
|
||||||
|
|
||||||
|
this._canvas.width = w;
|
||||||
|
this._canvas.height = h;
|
||||||
|
|
||||||
|
let img = new ImageData(new Uint8ClampedArray(rgba), w, h);
|
||||||
|
ctx.clearRect(0, 0, w, h);
|
||||||
|
ctx.putImageData(img, 0, 0);
|
||||||
|
|
||||||
|
if (useFallback) {
|
||||||
|
this._updatePosition();
|
||||||
|
} else {
|
||||||
|
let url = this._canvas.toDataURL();
|
||||||
|
this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this._target.style.cursor = 'none';
|
||||||
|
this._canvas.width = 0;
|
||||||
|
this._canvas.height = 0;
|
||||||
|
this._position.x = this._position.x + this._hotSpot.x;
|
||||||
|
this._position.y = this._position.y + this._hotSpot.y;
|
||||||
|
this._hotSpot.x = 0;
|
||||||
|
this._hotSpot.y = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mouse events might be emulated, this allows
|
||||||
|
// moving the cursor in such cases
|
||||||
|
move(clientX, clientY) {
|
||||||
|
if (!useFallback) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// clientX/clientY are relative the _visual viewport_,
|
||||||
|
// but our position is relative the _layout viewport_,
|
||||||
|
// so try to compensate when we can
|
||||||
|
if (window.visualViewport) {
|
||||||
|
this._position.x = clientX + window.visualViewport.offsetLeft;
|
||||||
|
this._position.y = clientY + window.visualViewport.offsetTop;
|
||||||
|
} else {
|
||||||
|
this._position.x = clientX;
|
||||||
|
this._position.y = clientY;
|
||||||
|
}
|
||||||
|
this._updatePosition();
|
||||||
|
let target = document.elementFromPoint(clientX, clientY);
|
||||||
|
this._updateVisibility(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleMouseOver(event) {
|
||||||
|
// This event could be because we're entering the target, or
|
||||||
|
// moving around amongst its sub elements. Let the move handler
|
||||||
|
// sort things out.
|
||||||
|
this._handleMouseMove(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleMouseLeave(event) {
|
||||||
|
// Check if we should show the cursor on the element we are leaving to
|
||||||
|
this._updateVisibility(event.relatedTarget);
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleMouseMove(event) {
|
||||||
|
this._updateVisibility(event.target);
|
||||||
|
|
||||||
|
this._position.x = event.clientX - this._hotSpot.x;
|
||||||
|
this._position.y = event.clientY - this._hotSpot.y;
|
||||||
|
|
||||||
|
this._updatePosition();
|
||||||
|
}
|
||||||
|
|
||||||
|
_handleMouseUp(event) {
|
||||||
|
// We might get this event because of a drag operation that
|
||||||
|
// moved outside of the target. Check what's under the cursor
|
||||||
|
// now and adjust visibility based on that.
|
||||||
|
let target = document.elementFromPoint(event.clientX, event.clientY);
|
||||||
|
this._updateVisibility(target);
|
||||||
|
|
||||||
|
// Captures end with a mouseup but we can't know the event order of
|
||||||
|
// mouseup vs releaseCapture.
|
||||||
|
//
|
||||||
|
// In the cases when releaseCapture comes first, the code above is
|
||||||
|
// enough.
|
||||||
|
//
|
||||||
|
// In the cases when the mouseup comes first, we need wait for the
|
||||||
|
// browser to flush all events and then check again if the cursor
|
||||||
|
// should be visible.
|
||||||
|
if (this._captureIsActive()) {
|
||||||
|
window.setTimeout(() => {
|
||||||
|
// We might have detached at this point
|
||||||
|
if (!this._target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Refresh the target from elementFromPoint since queued events
|
||||||
|
// might have altered the DOM
|
||||||
|
target = document.elementFromPoint(event.clientX,
|
||||||
|
event.clientY);
|
||||||
|
this._updateVisibility(target);
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_showCursor() {
|
||||||
|
if (this._canvas.style.visibility === 'hidden') {
|
||||||
|
this._canvas.style.visibility = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_hideCursor() {
|
||||||
|
if (this._canvas.style.visibility !== 'hidden') {
|
||||||
|
this._canvas.style.visibility = 'hidden';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should we currently display the cursor?
|
||||||
|
// (i.e. are we over the target, or a child of the target without a
|
||||||
|
// different cursor set)
|
||||||
|
_shouldShowCursor(target) {
|
||||||
|
if (!target) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Easy case
|
||||||
|
if (target === this._target) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Other part of the DOM?
|
||||||
|
if (!this._target.contains(target)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Has the child its own cursor?
|
||||||
|
// FIXME: How can we tell that a sub element has an
|
||||||
|
// explicit "cursor: none;"?
|
||||||
|
if (window.getComputedStyle(target).cursor !== 'none') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_updateVisibility(target) {
|
||||||
|
// When the cursor target has capture we want to show the cursor.
|
||||||
|
// So, if a capture is active - look at the captured element instead.
|
||||||
|
if (this._captureIsActive()) {
|
||||||
|
target = document.captureElement;
|
||||||
|
}
|
||||||
|
if (this._shouldShowCursor(target)) {
|
||||||
|
this._showCursor();
|
||||||
|
} else {
|
||||||
|
this._hideCursor();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_updatePosition() {
|
||||||
|
this._canvas.style.left = this._position.x + "px";
|
||||||
|
this._canvas.style.top = this._position.y + "px";
|
||||||
|
}
|
||||||
|
|
||||||
|
_captureIsActive() {
|
||||||
|
return document.captureElement &&
|
||||||
|
document.documentElement.contains(document.captureElement);
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
@@ -0,0 +1,32 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2020 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* HTML element utility functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function clientToElement(x, y, elem) {
|
||||||
|
const bounds = elem.getBoundingClientRect();
|
||||||
|
let pos = { x: 0, y: 0 };
|
||||||
|
// Clip to target bounds
|
||||||
|
if (x < bounds.left) {
|
||||||
|
pos.x = 0;
|
||||||
|
} else if (x >= bounds.right) {
|
||||||
|
pos.x = bounds.width - 1;
|
||||||
|
} else {
|
||||||
|
pos.x = x - bounds.left;
|
||||||
|
}
|
||||||
|
if (y < bounds.top) {
|
||||||
|
pos.y = 0;
|
||||||
|
} else if (y >= bounds.bottom) {
|
||||||
|
pos.y = bounds.height - 1;
|
||||||
|
} else {
|
||||||
|
pos.y = y - bounds.top;
|
||||||
|
}
|
||||||
|
return pos;
|
||||||
|
}
|
||||||
+138
@@ -0,0 +1,138 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2018 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Cross-browser event and position routines
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function getPointerEvent(e) {
|
||||||
|
return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stopEvent(e) {
|
||||||
|
e.stopPropagation();
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emulate Element.setCapture() when not supported
|
||||||
|
let _captureRecursion = false;
|
||||||
|
let _elementForUnflushedEvents = null;
|
||||||
|
document.captureElement = null;
|
||||||
|
function _captureProxy(e) {
|
||||||
|
// Recursion protection as we'll see our own event
|
||||||
|
if (_captureRecursion) return;
|
||||||
|
|
||||||
|
// Clone the event as we cannot dispatch an already dispatched event
|
||||||
|
const newEv = new e.constructor(e.type, e);
|
||||||
|
|
||||||
|
_captureRecursion = true;
|
||||||
|
if (document.captureElement) {
|
||||||
|
document.captureElement.dispatchEvent(newEv);
|
||||||
|
} else {
|
||||||
|
_elementForUnflushedEvents.dispatchEvent(newEv);
|
||||||
|
}
|
||||||
|
_captureRecursion = false;
|
||||||
|
|
||||||
|
// Avoid double events
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// Respect the wishes of the redirected event handlers
|
||||||
|
if (newEv.defaultPrevented) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implicitly release the capture on button release
|
||||||
|
if (e.type === "mouseup") {
|
||||||
|
releaseCapture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Follow cursor style of target element
|
||||||
|
function _capturedElemChanged() {
|
||||||
|
const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
|
||||||
|
proxyElem.style.cursor = window.getComputedStyle(document.captureElement).cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _captureObserver = new MutationObserver(_capturedElemChanged);
|
||||||
|
|
||||||
|
export function setCapture(target) {
|
||||||
|
if (target.setCapture) {
|
||||||
|
|
||||||
|
target.setCapture();
|
||||||
|
document.captureElement = target;
|
||||||
|
} else {
|
||||||
|
// Release any existing capture in case this method is
|
||||||
|
// called multiple times without coordination
|
||||||
|
releaseCapture();
|
||||||
|
|
||||||
|
let proxyElem = document.getElementById("noVNC_mouse_capture_elem");
|
||||||
|
|
||||||
|
if (proxyElem === null) {
|
||||||
|
proxyElem = document.createElement("div");
|
||||||
|
proxyElem.id = "noVNC_mouse_capture_elem";
|
||||||
|
proxyElem.style.position = "fixed";
|
||||||
|
proxyElem.style.top = "0px";
|
||||||
|
proxyElem.style.left = "0px";
|
||||||
|
proxyElem.style.width = "100%";
|
||||||
|
proxyElem.style.height = "100%";
|
||||||
|
proxyElem.style.zIndex = 10000;
|
||||||
|
proxyElem.style.display = "none";
|
||||||
|
document.body.appendChild(proxyElem);
|
||||||
|
|
||||||
|
// This is to make sure callers don't get confused by having
|
||||||
|
// our blocking element as the target
|
||||||
|
proxyElem.addEventListener('contextmenu', _captureProxy);
|
||||||
|
|
||||||
|
proxyElem.addEventListener('mousemove', _captureProxy);
|
||||||
|
proxyElem.addEventListener('mouseup', _captureProxy);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.captureElement = target;
|
||||||
|
|
||||||
|
// Track cursor and get initial cursor
|
||||||
|
_captureObserver.observe(target, {attributes: true});
|
||||||
|
_capturedElemChanged();
|
||||||
|
|
||||||
|
proxyElem.style.display = "";
|
||||||
|
|
||||||
|
// We listen to events on window in order to keep tracking if it
|
||||||
|
// happens to leave the viewport
|
||||||
|
window.addEventListener('mousemove', _captureProxy);
|
||||||
|
window.addEventListener('mouseup', _captureProxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function releaseCapture() {
|
||||||
|
if (document.releaseCapture) {
|
||||||
|
|
||||||
|
document.releaseCapture();
|
||||||
|
document.captureElement = null;
|
||||||
|
|
||||||
|
} else {
|
||||||
|
if (!document.captureElement) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// There might be events already queued. The event proxy needs
|
||||||
|
// access to the captured element for these queued events.
|
||||||
|
// E.g. contextmenu (right-click) in Microsoft Edge
|
||||||
|
//
|
||||||
|
// Before removing the capturedElem pointer we save it to a
|
||||||
|
// temporary variable that the unflushed events can use.
|
||||||
|
_elementForUnflushedEvents = document.captureElement;
|
||||||
|
document.captureElement = null;
|
||||||
|
|
||||||
|
_captureObserver.disconnect();
|
||||||
|
|
||||||
|
const proxyElem = document.getElementById("noVNC_mouse_capture_elem");
|
||||||
|
proxyElem.style.display = "none";
|
||||||
|
|
||||||
|
window.removeEventListener('mousemove', _captureProxy);
|
||||||
|
window.removeEventListener('mouseup', _captureProxy);
|
||||||
|
}
|
||||||
|
}
|
||||||
+35
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export default class EventTargetMixin {
|
||||||
|
constructor() {
|
||||||
|
this._listeners = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener(type, callback) {
|
||||||
|
if (!this._listeners.has(type)) {
|
||||||
|
this._listeners.set(type, new Set());
|
||||||
|
}
|
||||||
|
this._listeners.get(type).add(callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeEventListener(type, callback) {
|
||||||
|
if (this._listeners.has(type)) {
|
||||||
|
this._listeners.get(type).delete(callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatchEvent(event) {
|
||||||
|
if (!this._listeners.has(event.type)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
this._listeners.get(event.type)
|
||||||
|
.forEach(callback => callback.call(this, event));
|
||||||
|
return !event.defaultPrevented;
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
@@ -0,0 +1,15 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2020 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export function toUnsigned32bit(toConvert) {
|
||||||
|
return toConvert >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toSigned32bit(toConvert) {
|
||||||
|
return toConvert | 0;
|
||||||
|
}
|
||||||
+56
@@ -0,0 +1,56 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Logging/debug routines
|
||||||
|
*/
|
||||||
|
|
||||||
|
let _logLevel = 'warn';
|
||||||
|
|
||||||
|
let Debug = () => {};
|
||||||
|
let Info = () => {};
|
||||||
|
let Warn = () => {};
|
||||||
|
let Error = () => {};
|
||||||
|
|
||||||
|
export function initLogging(level) {
|
||||||
|
if (typeof level === 'undefined') {
|
||||||
|
level = _logLevel;
|
||||||
|
} else {
|
||||||
|
_logLevel = level;
|
||||||
|
}
|
||||||
|
|
||||||
|
Debug = Info = Warn = Error = () => {};
|
||||||
|
|
||||||
|
if (typeof window.console !== "undefined") {
|
||||||
|
/* eslint-disable no-console, no-fallthrough */
|
||||||
|
switch (level) {
|
||||||
|
case 'debug':
|
||||||
|
Debug = console.debug.bind(window.console);
|
||||||
|
case 'info':
|
||||||
|
Info = console.info.bind(window.console);
|
||||||
|
case 'warn':
|
||||||
|
Warn = console.warn.bind(window.console);
|
||||||
|
case 'error':
|
||||||
|
Error = console.error.bind(window.console);
|
||||||
|
case 'none':
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new window.Error("invalid logging type '" + level + "'");
|
||||||
|
}
|
||||||
|
/* eslint-enable no-console, no-fallthrough */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLogging() {
|
||||||
|
return _logLevel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Debug, Info, Warn, Error };
|
||||||
|
|
||||||
|
// Initialize logging level
|
||||||
|
initLogging();
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* noVNC: HTML5 VNC client
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* See README.md for usage and integration instructions.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Decode from UTF-8
|
||||||
|
export function decodeUTF8(utf8string, allowLatin1=false) {
|
||||||
|
try {
|
||||||
|
return decodeURIComponent(escape(utf8string));
|
||||||
|
} catch (e) {
|
||||||
|
if (e instanceof URIError) {
|
||||||
|
if (allowLatin1) {
|
||||||
|
// If we allow Latin1 we can ignore any decoding fails
|
||||||
|
// and in these cases return the original string
|
||||||
|
return utf8string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode to UTF-8
|
||||||
|
export function encodeUTF8(DOMString) {
|
||||||
|
return unescape(encodeURIComponent(DOMString));
|
||||||
|
}
|
||||||
+365
@@ -0,0 +1,365 @@
|
|||||||
|
/*
|
||||||
|
* Websock: high-performance buffering wrapper
|
||||||
|
* Copyright (C) 2019 The noVNC authors
|
||||||
|
* Licensed under MPL 2.0 (see LICENSE.txt)
|
||||||
|
*
|
||||||
|
* Websock is similar to the standard WebSocket / RTCDataChannel object
|
||||||
|
* but with extra buffer handling.
|
||||||
|
*
|
||||||
|
* Websock has built-in receive queue buffering; the message event
|
||||||
|
* does not contain actual data but is simply a notification that
|
||||||
|
* there is new data available. Several rQ* methods are available to
|
||||||
|
* read binary data off of the receive queue.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as Log from './util/logging.js';
|
||||||
|
|
||||||
|
// this has performance issues in some versions Chromium, and
|
||||||
|
// doesn't gain a tremendous amount of performance increase in Firefox
|
||||||
|
// at the moment. It may be valuable to turn it on in the future.
|
||||||
|
const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB
|
||||||
|
|
||||||
|
// Constants pulled from RTCDataChannelState enum
|
||||||
|
// https://developer.mozilla.org/en-US/docs/Web/API/RTCDataChannel/readyState#RTCDataChannelState_enum
|
||||||
|
const DataChannel = {
|
||||||
|
CONNECTING: "connecting",
|
||||||
|
OPEN: "open",
|
||||||
|
CLOSING: "closing",
|
||||||
|
CLOSED: "closed"
|
||||||
|
};
|
||||||
|
|
||||||
|
const ReadyStates = {
|
||||||
|
CONNECTING: [WebSocket.CONNECTING, DataChannel.CONNECTING],
|
||||||
|
OPEN: [WebSocket.OPEN, DataChannel.OPEN],
|
||||||
|
CLOSING: [WebSocket.CLOSING, DataChannel.CLOSING],
|
||||||
|
CLOSED: [WebSocket.CLOSED, DataChannel.CLOSED],
|
||||||
|
};
|
||||||
|
|
||||||
|
// Properties a raw channel must have, WebSocket and RTCDataChannel are two examples
|
||||||
|
const rawChannelProps = [
|
||||||
|
"send",
|
||||||
|
"close",
|
||||||
|
"binaryType",
|
||||||
|
"onerror",
|
||||||
|
"onmessage",
|
||||||
|
"onopen",
|
||||||
|
"protocol",
|
||||||
|
"readyState",
|
||||||
|
];
|
||||||
|
|
||||||
|
export default class Websock {
|
||||||
|
constructor() {
|
||||||
|
this._websocket = null; // WebSocket or RTCDataChannel object
|
||||||
|
|
||||||
|
this._rQi = 0; // Receive queue index
|
||||||
|
this._rQlen = 0; // Next write position in the receive queue
|
||||||
|
this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB)
|
||||||
|
// called in init: this._rQ = new Uint8Array(this._rQbufferSize);
|
||||||
|
this._rQ = null; // Receive queue
|
||||||
|
|
||||||
|
this._sQbufferSize = 1024 * 10; // 10 KiB
|
||||||
|
// called in init: this._sQ = new Uint8Array(this._sQbufferSize);
|
||||||
|
this._sQlen = 0;
|
||||||
|
this._sQ = null; // Send queue
|
||||||
|
|
||||||
|
this._eventHandlers = {
|
||||||
|
message: () => {},
|
||||||
|
open: () => {},
|
||||||
|
close: () => {},
|
||||||
|
error: () => {}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and setters
|
||||||
|
|
||||||
|
get readyState() {
|
||||||
|
let subState;
|
||||||
|
|
||||||
|
if (this._websocket === null) {
|
||||||
|
return "unused";
|
||||||
|
}
|
||||||
|
|
||||||
|
subState = this._websocket.readyState;
|
||||||
|
|
||||||
|
if (ReadyStates.CONNECTING.includes(subState)) {
|
||||||
|
return "connecting";
|
||||||
|
} else if (ReadyStates.OPEN.includes(subState)) {
|
||||||
|
return "open";
|
||||||
|
} else if (ReadyStates.CLOSING.includes(subState)) {
|
||||||
|
return "closing";
|
||||||
|
} else if (ReadyStates.CLOSED.includes(subState)) {
|
||||||
|
return "closed";
|
||||||
|
}
|
||||||
|
|
||||||
|
return "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receive queue
|
||||||
|
rQpeek8() {
|
||||||
|
return this._rQ[this._rQi];
|
||||||
|
}
|
||||||
|
|
||||||
|
rQskipBytes(bytes) {
|
||||||
|
this._rQi += bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
rQshift8() {
|
||||||
|
return this._rQshift(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
rQshift16() {
|
||||||
|
return this._rQshift(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
rQshift32() {
|
||||||
|
return this._rQshift(4);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(directxman12): test performance with these vs a DataView
|
||||||
|
_rQshift(bytes) {
|
||||||
|
let res = 0;
|
||||||
|
for (let byte = bytes - 1; byte >= 0; byte--) {
|
||||||
|
res += this._rQ[this._rQi++] << (byte * 8);
|
||||||
|
}
|
||||||
|
return res >>> 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
rQshiftStr(len) {
|
||||||
|
let str = "";
|
||||||
|
// Handle large arrays in steps to avoid long strings on the stack
|
||||||
|
for (let i = 0; i < len; i += 4096) {
|
||||||
|
let part = this.rQshiftBytes(Math.min(4096, len - i), false);
|
||||||
|
str += String.fromCharCode.apply(null, part);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
|
rQshiftBytes(len, copy=true) {
|
||||||
|
this._rQi += len;
|
||||||
|
if (copy) {
|
||||||
|
return this._rQ.slice(this._rQi - len, this._rQi);
|
||||||
|
} else {
|
||||||
|
return this._rQ.subarray(this._rQi - len, this._rQi);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rQshiftTo(target, len) {
|
||||||
|
// TODO: make this just use set with views when using a ArrayBuffer to store the rQ
|
||||||
|
target.set(new Uint8Array(this._rQ.buffer, this._rQi, len));
|
||||||
|
this._rQi += len;
|
||||||
|
}
|
||||||
|
|
||||||
|
rQpeekBytes(len, copy=true) {
|
||||||
|
if (copy) {
|
||||||
|
return this._rQ.slice(this._rQi, this._rQi + len);
|
||||||
|
} else {
|
||||||
|
return this._rQ.subarray(this._rQi, this._rQi + len);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check to see if we must wait for 'num' bytes (default to FBU.bytes)
|
||||||
|
// to be available in the receive queue. Return true if we need to
|
||||||
|
// wait (and possibly print a debug message), otherwise false.
|
||||||
|
rQwait(msg, num, goback) {
|
||||||
|
if (this._rQlen - this._rQi < num) {
|
||||||
|
if (goback) {
|
||||||
|
if (this._rQi < goback) {
|
||||||
|
throw new Error("rQwait cannot backup " + goback + " bytes");
|
||||||
|
}
|
||||||
|
this._rQi -= goback;
|
||||||
|
}
|
||||||
|
return true; // true means need more data
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send queue
|
||||||
|
|
||||||
|
sQpush8(num) {
|
||||||
|
this._sQensureSpace(1);
|
||||||
|
this._sQ[this._sQlen++] = num;
|
||||||
|
}
|
||||||
|
|
||||||
|
sQpush16(num) {
|
||||||
|
this._sQensureSpace(2);
|
||||||
|
this._sQ[this._sQlen++] = (num >> 8) & 0xff;
|
||||||
|
this._sQ[this._sQlen++] = (num >> 0) & 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
sQpush32(num) {
|
||||||
|
this._sQensureSpace(4);
|
||||||
|
this._sQ[this._sQlen++] = (num >> 24) & 0xff;
|
||||||
|
this._sQ[this._sQlen++] = (num >> 16) & 0xff;
|
||||||
|
this._sQ[this._sQlen++] = (num >> 8) & 0xff;
|
||||||
|
this._sQ[this._sQlen++] = (num >> 0) & 0xff;
|
||||||
|
}
|
||||||
|
|
||||||
|
sQpushString(str) {
|
||||||
|
let bytes = str.split('').map(chr => chr.charCodeAt(0));
|
||||||
|
this.sQpushBytes(new Uint8Array(bytes));
|
||||||
|
}
|
||||||
|
|
||||||
|
sQpushBytes(bytes) {
|
||||||
|
for (let offset = 0;offset < bytes.length;) {
|
||||||
|
this._sQensureSpace(1);
|
||||||
|
|
||||||
|
let chunkSize = this._sQbufferSize - this._sQlen;
|
||||||
|
if (chunkSize > bytes.length - offset) {
|
||||||
|
chunkSize = bytes.length - offset;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._sQ.set(bytes.subarray(offset, offset + chunkSize), this._sQlen);
|
||||||
|
this._sQlen += chunkSize;
|
||||||
|
offset += chunkSize;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
if (this._sQlen > 0 && this.readyState === 'open') {
|
||||||
|
this._websocket.send(new Uint8Array(this._sQ.buffer, 0, this._sQlen));
|
||||||
|
this._sQlen = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_sQensureSpace(bytes) {
|
||||||
|
if (this._sQbufferSize - this._sQlen < bytes) {
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event handlers
|
||||||
|
off(evt) {
|
||||||
|
this._eventHandlers[evt] = () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
on(evt, handler) {
|
||||||
|
this._eventHandlers[evt] = handler;
|
||||||
|
}
|
||||||
|
|
||||||
|
_allocateBuffers() {
|
||||||
|
this._rQ = new Uint8Array(this._rQbufferSize);
|
||||||
|
this._sQ = new Uint8Array(this._sQbufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this._allocateBuffers();
|
||||||
|
this._rQi = 0;
|
||||||
|
this._websocket = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
open(uri, protocols) {
|
||||||
|
this.attach(new WebSocket(uri, protocols));
|
||||||
|
}
|
||||||
|
|
||||||
|
attach(rawChannel) {
|
||||||
|
this.init();
|
||||||
|
|
||||||
|
// Must get object and class methods to be compatible with the tests.
|
||||||
|
const channelProps = [...Object.keys(rawChannel), ...Object.getOwnPropertyNames(Object.getPrototypeOf(rawChannel))];
|
||||||
|
for (let i = 0; i < rawChannelProps.length; i++) {
|
||||||
|
const prop = rawChannelProps[i];
|
||||||
|
if (channelProps.indexOf(prop) < 0) {
|
||||||
|
throw new Error('Raw channel missing property: ' + prop);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._websocket = rawChannel;
|
||||||
|
this._websocket.binaryType = "arraybuffer";
|
||||||
|
this._websocket.onmessage = this._recvMessage.bind(this);
|
||||||
|
|
||||||
|
this._websocket.onopen = () => {
|
||||||
|
Log.Debug('>> WebSock.onopen');
|
||||||
|
if (this._websocket.protocol) {
|
||||||
|
Log.Info("Server choose sub-protocol: " + this._websocket.protocol);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._eventHandlers.open();
|
||||||
|
Log.Debug("<< WebSock.onopen");
|
||||||
|
};
|
||||||
|
|
||||||
|
this._websocket.onclose = (e) => {
|
||||||
|
Log.Debug(">> WebSock.onclose");
|
||||||
|
this._eventHandlers.close(e);
|
||||||
|
Log.Debug("<< WebSock.onclose");
|
||||||
|
};
|
||||||
|
|
||||||
|
this._websocket.onerror = (e) => {
|
||||||
|
Log.Debug(">> WebSock.onerror: " + e);
|
||||||
|
this._eventHandlers.error(e);
|
||||||
|
Log.Debug("<< WebSock.onerror: " + e);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (this._websocket) {
|
||||||
|
if (this.readyState === 'connecting' ||
|
||||||
|
this.readyState === 'open') {
|
||||||
|
Log.Info("Closing WebSocket connection");
|
||||||
|
this._websocket.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._websocket.onmessage = () => {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// private methods
|
||||||
|
|
||||||
|
// We want to move all the unread data to the start of the queue,
|
||||||
|
// e.g. compacting.
|
||||||
|
// The function also expands the receive que if needed, and for
|
||||||
|
// performance reasons we combine these two actions to avoid
|
||||||
|
// unnecessary copying.
|
||||||
|
_expandCompactRQ(minFit) {
|
||||||
|
// if we're using less than 1/8th of the buffer even with the incoming bytes, compact in place
|
||||||
|
// instead of resizing
|
||||||
|
const requiredBufferSize = (this._rQlen - this._rQi + minFit) * 8;
|
||||||
|
const resizeNeeded = this._rQbufferSize < requiredBufferSize;
|
||||||
|
|
||||||
|
if (resizeNeeded) {
|
||||||
|
// Make sure we always *at least* double the buffer size, and have at least space for 8x
|
||||||
|
// the current amount of data
|
||||||
|
this._rQbufferSize = Math.max(this._rQbufferSize * 2, requiredBufferSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
// we don't want to grow unboundedly
|
||||||
|
if (this._rQbufferSize > MAX_RQ_GROW_SIZE) {
|
||||||
|
this._rQbufferSize = MAX_RQ_GROW_SIZE;
|
||||||
|
if (this._rQbufferSize - (this._rQlen - this._rQi) < minFit) {
|
||||||
|
throw new Error("Receive queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resizeNeeded) {
|
||||||
|
const oldRQbuffer = this._rQ.buffer;
|
||||||
|
this._rQ = new Uint8Array(this._rQbufferSize);
|
||||||
|
this._rQ.set(new Uint8Array(oldRQbuffer, this._rQi, this._rQlen - this._rQi));
|
||||||
|
} else {
|
||||||
|
this._rQ.copyWithin(0, this._rQi, this._rQlen);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._rQlen = this._rQlen - this._rQi;
|
||||||
|
this._rQi = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// push arraybuffer values onto the end of the receive que
|
||||||
|
_recvMessage(e) {
|
||||||
|
if (this._rQlen == this._rQi) {
|
||||||
|
// All data has now been processed, this means we
|
||||||
|
// can reset the receive queue.
|
||||||
|
this._rQlen = 0;
|
||||||
|
this._rQi = 0;
|
||||||
|
}
|
||||||
|
const u8 = new Uint8Array(e.data);
|
||||||
|
if (u8.length > this._rQbufferSize - this._rQlen) {
|
||||||
|
this._expandCompactRQ(u8.length);
|
||||||
|
}
|
||||||
|
this._rQ.set(u8, this._rQlen);
|
||||||
|
this._rQlen += u8.length;
|
||||||
|
|
||||||
|
if (this._rQlen - this._rQi > 0) {
|
||||||
|
this._eventHandlers.message();
|
||||||
|
} else {
|
||||||
|
Log.Debug("Ignoring empty message");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
@@ -0,0 +1,21 @@
|
|||||||
|
(The MIT License)
|
||||||
|
|
||||||
|
Copyright (C) 2014-2016 by Vitaly Puzrin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
+6
@@ -0,0 +1,6 @@
|
|||||||
|
This is an ES6-modules-compatible version of
|
||||||
|
https://github.com/nodeca/pako, based on pako version 1.0.3.
|
||||||
|
|
||||||
|
It's more-or-less a direct translation of the original, with unused parts
|
||||||
|
removed, and the dynamic support for non-typed arrays removed (since ES6
|
||||||
|
modules don't work well with dynamic exports).
|
||||||
Vendored
+45
@@ -0,0 +1,45 @@
|
|||||||
|
// reduce buffer size, avoiding mem copy
|
||||||
|
export function shrinkBuf (buf, size) {
|
||||||
|
if (buf.length === size) { return buf; }
|
||||||
|
if (buf.subarray) { return buf.subarray(0, size); }
|
||||||
|
buf.length = size;
|
||||||
|
return buf;
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export function arraySet (dest, src, src_offs, len, dest_offs) {
|
||||||
|
if (src.subarray && dest.subarray) {
|
||||||
|
dest.set(src.subarray(src_offs, src_offs + len), dest_offs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fallback to ordinary array
|
||||||
|
for (var i = 0; i < len; i++) {
|
||||||
|
dest[dest_offs + i] = src[src_offs + i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Join array of chunks to single array.
|
||||||
|
export function flattenChunks (chunks) {
|
||||||
|
var i, l, len, pos, chunk, result;
|
||||||
|
|
||||||
|
// calculate data length
|
||||||
|
len = 0;
|
||||||
|
for (i = 0, l = chunks.length; i < l; i++) {
|
||||||
|
len += chunks[i].length;
|
||||||
|
}
|
||||||
|
|
||||||
|
// join chunks
|
||||||
|
result = new Uint8Array(len);
|
||||||
|
pos = 0;
|
||||||
|
for (i = 0, l = chunks.length; i < l; i++) {
|
||||||
|
chunk = chunks[i];
|
||||||
|
result.set(chunk, pos);
|
||||||
|
pos += chunk.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export var Buf8 = Uint8Array;
|
||||||
|
export var Buf16 = Uint16Array;
|
||||||
|
export var Buf32 = Int32Array;
|
||||||
Vendored
+27
@@ -0,0 +1,27 @@
|
|||||||
|
// Note: adler32 takes 12% for level 0 and 2% for level 6.
|
||||||
|
// It doesn't worth to make additional optimizationa as in original.
|
||||||
|
// Small size is preferable.
|
||||||
|
|
||||||
|
export default function adler32(adler, buf, len, pos) {
|
||||||
|
var s1 = (adler & 0xffff) |0,
|
||||||
|
s2 = ((adler >>> 16) & 0xffff) |0,
|
||||||
|
n = 0;
|
||||||
|
|
||||||
|
while (len !== 0) {
|
||||||
|
// Set limit ~ twice less than 5552, to keep
|
||||||
|
// s2 in 31-bits, because we force signed ints.
|
||||||
|
// in other case %= will fail.
|
||||||
|
n = len > 2000 ? 2000 : len;
|
||||||
|
len -= n;
|
||||||
|
|
||||||
|
do {
|
||||||
|
s1 = (s1 + buf[pos++]) |0;
|
||||||
|
s2 = (s2 + s1) |0;
|
||||||
|
} while (--n);
|
||||||
|
|
||||||
|
s1 %= 65521;
|
||||||
|
s2 %= 65521;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (s1 | (s2 << 16)) |0;
|
||||||
|
}
|
||||||
Vendored
+47
@@ -0,0 +1,47 @@
|
|||||||
|
export default {
|
||||||
|
|
||||||
|
/* Allowed flush values; see deflate() and inflate() below for details */
|
||||||
|
Z_NO_FLUSH: 0,
|
||||||
|
Z_PARTIAL_FLUSH: 1,
|
||||||
|
Z_SYNC_FLUSH: 2,
|
||||||
|
Z_FULL_FLUSH: 3,
|
||||||
|
Z_FINISH: 4,
|
||||||
|
Z_BLOCK: 5,
|
||||||
|
Z_TREES: 6,
|
||||||
|
|
||||||
|
/* Return codes for the compression/decompression functions. Negative values
|
||||||
|
* are errors, positive values are used for special but normal events.
|
||||||
|
*/
|
||||||
|
Z_OK: 0,
|
||||||
|
Z_STREAM_END: 1,
|
||||||
|
Z_NEED_DICT: 2,
|
||||||
|
Z_ERRNO: -1,
|
||||||
|
Z_STREAM_ERROR: -2,
|
||||||
|
Z_DATA_ERROR: -3,
|
||||||
|
//Z_MEM_ERROR: -4,
|
||||||
|
Z_BUF_ERROR: -5,
|
||||||
|
//Z_VERSION_ERROR: -6,
|
||||||
|
|
||||||
|
/* compression levels */
|
||||||
|
Z_NO_COMPRESSION: 0,
|
||||||
|
Z_BEST_SPEED: 1,
|
||||||
|
Z_BEST_COMPRESSION: 9,
|
||||||
|
Z_DEFAULT_COMPRESSION: -1,
|
||||||
|
|
||||||
|
|
||||||
|
Z_FILTERED: 1,
|
||||||
|
Z_HUFFMAN_ONLY: 2,
|
||||||
|
Z_RLE: 3,
|
||||||
|
Z_FIXED: 4,
|
||||||
|
Z_DEFAULT_STRATEGY: 0,
|
||||||
|
|
||||||
|
/* Possible values of the data_type field (though see inflate()) */
|
||||||
|
Z_BINARY: 0,
|
||||||
|
Z_TEXT: 1,
|
||||||
|
//Z_ASCII: 1, // = Z_TEXT (deprecated)
|
||||||
|
Z_UNKNOWN: 2,
|
||||||
|
|
||||||
|
/* The deflate compression method */
|
||||||
|
Z_DEFLATED: 8
|
||||||
|
//Z_NULL: null // Use -1 or null inline, depending on var type
|
||||||
|
};
|
||||||
Vendored
+36
@@ -0,0 +1,36 @@
|
|||||||
|
// Note: we can't get significant speed boost here.
|
||||||
|
// So write code to minimize size - no pregenerated tables
|
||||||
|
// and array tools dependencies.
|
||||||
|
|
||||||
|
|
||||||
|
// Use ordinary array, since untyped makes no boost here
|
||||||
|
export default function makeTable() {
|
||||||
|
var c, table = [];
|
||||||
|
|
||||||
|
for (var n = 0; n < 256; n++) {
|
||||||
|
c = n;
|
||||||
|
for (var k = 0; k < 8; k++) {
|
||||||
|
c = ((c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
|
||||||
|
}
|
||||||
|
table[n] = c;
|
||||||
|
}
|
||||||
|
|
||||||
|
return table;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create table on load. Just 255 signed longs. Not a problem.
|
||||||
|
var crcTable = makeTable();
|
||||||
|
|
||||||
|
|
||||||
|
function crc32(crc, buf, len, pos) {
|
||||||
|
var t = crcTable,
|
||||||
|
end = pos + len;
|
||||||
|
|
||||||
|
crc ^= -1;
|
||||||
|
|
||||||
|
for (var i = pos; i < end; i++) {
|
||||||
|
crc = (crc >>> 8) ^ t[(crc ^ buf[i]) & 0xFF];
|
||||||
|
}
|
||||||
|
|
||||||
|
return (crc ^ (-1)); // >>> 0;
|
||||||
|
}
|
||||||
Vendored
+1846
File diff suppressed because it is too large.
Load diff
Vendored
+35
@@ -0,0 +1,35 @@
|
|||||||
|
export default function GZheader() {
|
||||||
|
/* true if compressed data believed to be text */
|
||||||
|
this.text = 0;
|
||||||
|
/* modification time */
|
||||||
|
this.time = 0;
|
||||||
|
/* extra flags (not used when writing a gzip file) */
|
||||||
|
this.xflags = 0;
|
||||||
|
/* operating system */
|
||||||
|
this.os = 0;
|
||||||
|
/* pointer to extra field or Z_NULL if none */
|
||||||
|
this.extra = null;
|
||||||
|
/* extra field length (valid if extra != Z_NULL) */
|
||||||
|
this.extra_len = 0; // Actually, we don't need it in JS,
|
||||||
|
// but leave for few code modifications
|
||||||
|
|
||||||
|
//
|
||||||
|
// Setup limits is not necessary because in js we should not preallocate memory
|
||||||
|
// for inflate use constant limit in 65536 bytes
|
||||||
|
//
|
||||||
|
|
||||||
|
/* space at extra (only when reading header) */
|
||||||
|
// this.extra_max = 0;
|
||||||
|
/* pointer to zero-terminated file name or Z_NULL */
|
||||||
|
this.name = '';
|
||||||
|
/* space at name (only when reading header) */
|
||||||
|
// this.name_max = 0;
|
||||||
|
/* pointer to zero-terminated comment or Z_NULL */
|
||||||
|
this.comment = '';
|
||||||
|
/* space at comment (only when reading header) */
|
||||||
|
// this.comm_max = 0;
|
||||||
|
/* true if there was or will be a header crc */
|
||||||
|
this.hcrc = 0;
|
||||||
|
/* true when done reading gzip header (not used when writing a gzip file) */
|
||||||
|
this.done = false;
|
||||||
|
}
|
||||||
Vendored
+324
@@ -0,0 +1,324 @@
|
|||||||
|
// See state defs from inflate.js
|
||||||
|
var BAD = 30; /* got a data error -- remain here until reset */
|
||||||
|
var TYPE = 12; /* i: waiting for type bits, including last-flag bit */
|
||||||
|
|
||||||
|
/*
|
||||||
|
Decode literal, length, and distance codes and write out the resulting
|
||||||
|
literal and match bytes until either not enough input or output is
|
||||||
|
available, an end-of-block is encountered, or a data error is encountered.
|
||||||
|
When large enough input and output buffers are supplied to inflate(), for
|
||||||
|
example, a 16K input buffer and a 64K output buffer, more than 95% of the
|
||||||
|
inflate execution time is spent in this routine.
|
||||||
|
|
||||||
|
Entry assumptions:
|
||||||
|
|
||||||
|
state.mode === LEN
|
||||||
|
strm.avail_in >= 6
|
||||||
|
strm.avail_out >= 258
|
||||||
|
start >= strm.avail_out
|
||||||
|
state.bits < 8
|
||||||
|
|
||||||
|
On return, state.mode is one of:
|
||||||
|
|
||||||
|
LEN -- ran out of enough output space or enough available input
|
||||||
|
TYPE -- reached end of block code, inflate() to interpret next block
|
||||||
|
BAD -- error in block data
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- The maximum input bits used by a length/distance pair is 15 bits for the
|
||||||
|
length code, 5 bits for the length extra, 15 bits for the distance code,
|
||||||
|
and 13 bits for the distance extra. This totals 48 bits, or six bytes.
|
||||||
|
Therefore if strm.avail_in >= 6, then there is enough input to avoid
|
||||||
|
checking for available input while decoding.
|
||||||
|
|
||||||
|
- The maximum bytes that a single length/distance pair can output is 258
|
||||||
|
bytes, which is the maximum length that can be coded. inflate_fast()
|
||||||
|
requires strm.avail_out >= 258 for each loop to avoid checking for
|
||||||
|
output space.
|
||||||
|
*/
|
||||||
|
export default function inflate_fast(strm, start) {
|
||||||
|
var state;
|
||||||
|
var _in; /* local strm.input */
|
||||||
|
var last; /* have enough input while in < last */
|
||||||
|
var _out; /* local strm.output */
|
||||||
|
var beg; /* inflate()'s initial strm.output */
|
||||||
|
var end; /* while out < end, enough space available */
|
||||||
|
//#ifdef INFLATE_STRICT
|
||||||
|
var dmax; /* maximum distance from zlib header */
|
||||||
|
//#endif
|
||||||
|
var wsize; /* window size or zero if not using window */
|
||||||
|
var whave; /* valid bytes in the window */
|
||||||
|
var wnext; /* window write index */
|
||||||
|
// Use `s_window` instead `window`, avoid conflict with instrumentation tools
|
||||||
|
var s_window; /* allocated sliding window, if wsize != 0 */
|
||||||
|
var hold; /* local strm.hold */
|
||||||
|
var bits; /* local strm.bits */
|
||||||
|
var lcode; /* local strm.lencode */
|
||||||
|
var dcode; /* local strm.distcode */
|
||||||
|
var lmask; /* mask for first level of length codes */
|
||||||
|
var dmask; /* mask for first level of distance codes */
|
||||||
|
var here; /* retrieved table entry */
|
||||||
|
var op; /* code bits, operation, extra bits, or */
|
||||||
|
/* window position, window bytes to copy */
|
||||||
|
var len; /* match length, unused bytes */
|
||||||
|
var dist; /* match distance */
|
||||||
|
var from; /* where to copy match from */
|
||||||
|
var from_source;
|
||||||
|
|
||||||
|
|
||||||
|
var input, output; // JS specific, because we have no pointers
|
||||||
|
|
||||||
|
/* copy state to local variables */
|
||||||
|
state = strm.state;
|
||||||
|
//here = state.here;
|
||||||
|
_in = strm.next_in;
|
||||||
|
input = strm.input;
|
||||||
|
last = _in + (strm.avail_in - 5);
|
||||||
|
_out = strm.next_out;
|
||||||
|
output = strm.output;
|
||||||
|
beg = _out - (start - strm.avail_out);
|
||||||
|
end = _out + (strm.avail_out - 257);
|
||||||
|
//#ifdef INFLATE_STRICT
|
||||||
|
dmax = state.dmax;
|
||||||
|
//#endif
|
||||||
|
wsize = state.wsize;
|
||||||
|
whave = state.whave;
|
||||||
|
wnext = state.wnext;
|
||||||
|
s_window = state.window;
|
||||||
|
hold = state.hold;
|
||||||
|
bits = state.bits;
|
||||||
|
lcode = state.lencode;
|
||||||
|
dcode = state.distcode;
|
||||||
|
lmask = (1 << state.lenbits) - 1;
|
||||||
|
dmask = (1 << state.distbits) - 1;
|
||||||
|
|
||||||
|
|
||||||
|
/* decode literals and length/distances until end-of-block or not enough
|
||||||
|
input data or output space */
|
||||||
|
|
||||||
|
top:
|
||||||
|
do {
|
||||||
|
if (bits < 15) {
|
||||||
|
hold += input[_in++] << bits;
|
||||||
|
bits += 8;
|
||||||
|
hold += input[_in++] << bits;
|
||||||
|
bits += 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
here = lcode[hold & lmask];
|
||||||
|
|
||||||
|
dolen:
|
||||||
|
for (;;) { // Goto emulation
|
||||||
|
op = here >>> 24/*here.bits*/;
|
||||||
|
hold >>>= op;
|
||||||
|
bits -= op;
|
||||||
|
op = (here >>> 16) & 0xff/*here.op*/;
|
||||||
|
if (op === 0) { /* literal */
|
||||||
|
//Tracevv((stderr, here.val >= 0x20 && here.val < 0x7f ?
|
||||||
|
// "inflate: literal '%c'\n" :
|
||||||
|
// "inflate: literal 0x%02x\n", here.val));
|
||||||
|
output[_out++] = here & 0xffff/*here.val*/;
|
||||||
|
}
|
||||||
|
else if (op & 16) { /* length base */
|
||||||
|
len = here & 0xffff/*here.val*/;
|
||||||
|
op &= 15; /* number of extra bits */
|
||||||
|
if (op) {
|
||||||
|
if (bits < op) {
|
||||||
|
hold += input[_in++] << bits;
|
||||||
|
bits += 8;
|
||||||
|
}
|
||||||
|
len += hold & ((1 << op) - 1);
|
||||||
|
hold >>>= op;
|
||||||
|
bits -= op;
|
||||||
|
}
|
||||||
|
//Tracevv((stderr, "inflate: length %u\n", len));
|
||||||
|
if (bits < 15) {
|
||||||
|
hold += input[_in++] << bits;
|
||||||
|
bits += 8;
|
||||||
|
hold += input[_in++] << bits;
|
||||||
|
bits += 8;
|
||||||
|
}
|
||||||
|
here = dcode[hold & dmask];
|
||||||
|
|
||||||
|
dodist:
|
||||||
|
for (;;) { // goto emulation
|
||||||
|
op = here >>> 24/*here.bits*/;
|
||||||
|
hold >>>= op;
|
||||||
|
bits -= op;
|
||||||
|
op = (here >>> 16) & 0xff/*here.op*/;
|
||||||
|
|
||||||
|
if (op & 16) { /* distance base */
|
||||||
|
dist = here & 0xffff/*here.val*/;
|
||||||
|
op &= 15; /* number of extra bits */
|
||||||
|
if (bits < op) {
|
||||||
|
hold += input[_in++] << bits;
|
||||||
|
bits += 8;
|
||||||
|
if (bits < op) {
|
||||||
|
hold += input[_in++] << bits;
|
||||||
|
bits += 8;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dist += hold & ((1 << op) - 1);
|
||||||
|
//#ifdef INFLATE_STRICT
|
||||||
|
if (dist > dmax) {
|
||||||
|
strm.msg = 'invalid distance too far back';
|
||||||
|
state.mode = BAD;
|
||||||
|
break top;
|
||||||
|
}
|
||||||
|
//#endif
|
||||||
|
hold >>>= op;
|
||||||
|
bits -= op;
|
||||||
|
//Tracevv((stderr, "inflate: distance %u\n", dist));
|
||||||
|
op = _out - beg; /* max distance in output */
|
||||||
|
if (dist > op) { /* see if copy from window */
|
||||||
|
op = dist - op; /* distance back in window */
|
||||||
|
if (op > whave) {
|
||||||
|
if (state.sane) {
|
||||||
|
strm.msg = 'invalid distance too far back';
|
||||||
|
state.mode = BAD;
|
||||||
|
break top;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (!) This block is disabled in zlib defailts,
|
||||||
|
// don't enable it for binary compatibility
|
||||||
|
//#ifdef INFLATE_ALLOW_INVALID_DISTANCE_TOOFAR_ARRR
|
||||||
|
// if (len <= op - whave) {
|
||||||
|
// do {
|
||||||
|
// output[_out++] = 0;
|
||||||
|
// } while (--len);
|
||||||
|
// continue top;
|
||||||
|
// }
|
||||||
|
// len -= op - whave;
|
||||||
|
// do {
|
||||||
|
// output[_out++] = 0;
|
||||||
|
// } while (--op > whave);
|
||||||
|
// if (op === 0) {
|
||||||
|
// from = _out - dist;
|
||||||
|
// do {
|
||||||
|
// output[_out++] = output[from++];
|
||||||
|
// } while (--len);
|
||||||
|
// continue top;
|
||||||
|
// }
|
||||||
|
//#endif
|
||||||
|
}
|
||||||
|
from = 0; // window index
|
||||||
|
from_source = s_window;
|
||||||
|
if (wnext === 0) { /* very common case */
|
||||||
|
from += wsize - op;
|
||||||
|
if (op < len) { /* some from window */
|
||||||
|
len -= op;
|
||||||
|
do {
|
||||||
|
output[_out++] = s_window[from++];
|
||||||
|
} while (--op);
|
||||||
|
from = _out - dist; /* rest from output */
|
||||||
|
from_source = output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (wnext < op) { /* wrap around window */
|
||||||
|
from += wsize + wnext - op;
|
||||||
|
op -= wnext;
|
||||||
|
if (op < len) { /* some from end of window */
|
||||||
|
len -= op;
|
||||||
|
do {
|
||||||
|
output[_out++] = s_window[from++];
|
||||||
|
} while (--op);
|
||||||
|
from = 0;
|
||||||
|
if (wnext < len) { /* some from start of window */
|
||||||
|
op = wnext;
|
||||||
|
len -= op;
|
||||||
|
do {
|
||||||
|
output[_out++] = s_window[from++];
|
||||||
|
} while (--op);
|
||||||
|
from = _out - dist; /* rest from output */
|
||||||
|
from_source = output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else { /* contiguous in window */
|
||||||
|
from += wnext - op;
|
||||||
|
if (op < len) { /* some from window */
|
||||||
|
len -= op;
|
||||||
|
do {
|
||||||
|
output[_out++] = s_window[from++];
|
||||||
|
} while (--op);
|
||||||
|
from = _out - dist; /* rest from output */
|
||||||
|
from_source = output;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
while (len > 2) {
|
||||||
|
output[_out++] = from_source[from++];
|
||||||
|
output[_out++] = from_source[from++];
|
||||||
|
output[_out++] = from_source[from++];
|
||||||
|
len -= 3;
|
||||||
|
}
|
||||||
|
if (len) {
|
||||||
|
output[_out++] = from_source[from++];
|
||||||
|
if (len > 1) {
|
||||||
|
output[_out++] = from_source[from++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
from = _out - dist; /* copy direct from output */
|
||||||
|
do { /* minimum length is three */
|
||||||
|
output[_out++] = output[from++];
|
||||||
|
output[_out++] = output[from++];
|
||||||
|
output[_out++] = output[from++];
|
||||||
|
len -= 3;
|
||||||
|
} while (len > 2);
|
||||||
|
if (len) {
|
||||||
|
output[_out++] = output[from++];
|
||||||
|
if (len > 1) {
|
||||||
|
output[_out++] = output[from++];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((op & 64) === 0) { /* 2nd level distance code */
|
||||||
|
here = dcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
|
||||||
|
continue dodist;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
strm.msg = 'invalid distance code';
|
||||||
|
state.mode = BAD;
|
||||||
|
break top;
|
||||||
|
}
|
||||||
|
|
||||||
|
break; // need to emulate goto via "continue"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ((op & 64) === 0) { /* 2nd level length code */
|
||||||
|
here = lcode[(here & 0xffff)/*here.val*/ + (hold & ((1 << op) - 1))];
|
||||||
|
continue dolen;
|
||||||
|
}
|
||||||
|
else if (op & 32) { /* end-of-block */
|
||||||
|
//Tracevv((stderr, "inflate: end of block\n"));
|
||||||
|
state.mode = TYPE;
|
||||||
|
break top;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
strm.msg = 'invalid literal/length code';
|
||||||
|
state.mode = BAD;
|
||||||
|
break top;
|
||||||
|
}
|
||||||
|
|
||||||
|
break; // need to emulate goto via "continue"
|
||||||
|
}
|
||||||
|
} while (_in < last && _out < end);
|
||||||
|
|
||||||
|
/* return unused bytes (on entry, bits < 8, so in won't go too far back) */
|
||||||
|
len = bits >> 3;
|
||||||
|
_in -= len;
|
||||||
|
bits -= len << 3;
|
||||||
|
hold &= (1 << bits) - 1;
|
||||||
|
|
||||||
|
/* update state and return */
|
||||||
|
strm.next_in = _in;
|
||||||
|
strm.next_out = _out;
|
||||||
|
strm.avail_in = (_in < last ? 5 + (last - _in) : 5 - (_in - last));
|
||||||
|
strm.avail_out = (_out < end ? 257 + (end - _out) : 257 - (_out - end));
|
||||||
|
state.hold = hold;
|
||||||
|
state.bits = bits;
|
||||||
|
return;
|
||||||
|
};
|
||||||
Vendored
+1527
File diff suppressed because it is too large.
Load diff
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user