Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
76a73017db | ||
|
|
db06ddc054 | ||
|
|
e0a048fbcc | ||
|
|
9d736bb91f | ||
|
|
914609b758 | ||
|
|
af81c92695 | ||
|
|
a528e9f3b2 | ||
|
|
3b3ce96678 | ||
|
|
42316ba137 | ||
|
|
d34baa67be | ||
|
|
491724beb5 | ||
|
|
b54b87ca6f | ||
|
|
270ba455a8 | ||
|
|
f3b312046f | ||
|
|
436b080deb | ||
|
|
85c40ccfbd | ||
|
|
0ac651a1b9 | ||
|
|
4275c84a6c | ||
|
|
4f4310bfe4 | ||
|
|
1ad2f3f9a5 | ||
|
|
9d5eac1db2 | ||
|
|
2fd78ac26b | ||
|
|
86c09d8a0f | ||
|
|
70f6b005a9 | ||
|
|
57eb7764cc |
No files matched your search
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm"
|
name = "tesm"
|
||||||
version = "2.8.2"
|
version = "2.8.27"
|
||||||
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
|
||||||
|
|
||||||
@@ -39,11 +39,28 @@ def _tiles(*, authenticated: bool) -> dict[str, Any]:
|
|||||||
|
|
||||||
from ..services import inventory, restart
|
from ..services import inventory, restart
|
||||||
|
|
||||||
|
from ..services import vnc as vnc_dienst
|
||||||
|
|
||||||
conn = db()
|
conn = db()
|
||||||
counts = monitor.dashboard_counts(conn)
|
counts = monitor.dashboard_counts(conn)
|
||||||
rows = inventory.list_devices(conn, only_active=not authenticated)
|
rows = inventory.list_devices(conn, only_active=not authenticated)
|
||||||
|
|
||||||
detailed = authenticated and current_user.has("devices.read")
|
detailed = authenticated and current_user.has("devices.read")
|
||||||
|
# Einmal je Seitenaufbau, nicht je Kachel: der Port ist fuer alle derselbe,
|
||||||
|
# und ohne das Recht wird gar nicht erst nachgesehen.
|
||||||
|
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 = (
|
||||||
|
int(core().settings.get(conn, "vnc_port") or vnc_dienst.STANDARD_PORT)
|
||||||
|
if vnc_erlaubt and core().settings.get(conn, "vnc_enabled")
|
||||||
|
else 0
|
||||||
|
)
|
||||||
tiles: list[dict[str, Any]] = []
|
tiles: list[dict[str, Any]] = []
|
||||||
for row in rows:
|
for row in rows:
|
||||||
entry: dict[str, Any] = {
|
entry: dict[str, Any] = {
|
||||||
@@ -74,9 +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,
|
||||||
),
|
),
|
||||||
|
# Der Knopf oeffnet die Sitzung in TESM selbst. Ein
|
||||||
|
# vnc://-Link setzte einen eingerichteten Viewer auf dem
|
||||||
|
# Arbeitsplatz voraus -- und tat dort, wo keiner
|
||||||
|
# registriert ist, schlicht nichts.
|
||||||
|
#
|
||||||
|
# Ob dort ein Server antwortet, steht aus dem letzten
|
||||||
|
# Durchlauf in der Datenbank. Die Uebersicht zeigt viele
|
||||||
|
# Kacheln und laedt sich selbst nach; ein
|
||||||
|
# Verbindungsversuch je Kachel und Aufbau waere genau die
|
||||||
|
# Art Hintergrundlast, die niemand bestellt hat.
|
||||||
|
"vnc": bool(vnc_port and row["vnc_open"]),
|
||||||
|
"vnc_quelle": (
|
||||||
|
url_for("screen.session_json", device_id=int(row["id"]))
|
||||||
|
if vnc_port and row["vnc_open"]
|
||||||
|
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 = [
|
||||||
{
|
{
|
||||||
@@ -121,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),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,23 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from flask import Blueprint, abort, flash, redirect, render_template, request, url_for
|
from flask import (
|
||||||
|
Blueprint,
|
||||||
|
Response,
|
||||||
|
abort,
|
||||||
|
flash,
|
||||||
|
redirect,
|
||||||
|
render_template,
|
||||||
|
request,
|
||||||
|
url_for,
|
||||||
|
)
|
||||||
from tesm_core import audit
|
from tesm_core import audit
|
||||||
from tesm_core.auth.models import current_user, permission_required, reauth_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
|
||||||
|
|
||||||
from ..services import inventory, monitor, restart, ssh
|
from ..services import inventory, monitor, restart, ssh, vnc
|
||||||
|
|
||||||
bp = Blueprint("devices", __name__, url_prefix="/monitoring/clients")
|
bp = Blueprint("devices", __name__, url_prefix="/monitoring/clients")
|
||||||
|
|
||||||
@@ -309,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,
|
||||||
@@ -360,6 +373,70 @@ def detail(device_id: int):
|
|||||||
host_keys=host_keys,
|
host_keys=host_keys,
|
||||||
method_labels=monitor.METHOD_LABELS,
|
method_labels=monitor.METHOD_LABELS,
|
||||||
terminal_enabled=bool(extension.settings.get(conn, "terminal_clients_enabled")),
|
terminal_enabled=bool(extension.settings.get(conn, "terminal_clients_enabled")),
|
||||||
|
vnc=_vnc_zugang(conn, device),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _vnc_zugang(conn: Any, device: dict[str, Any]) -> vnc.Zugang | None:
|
||||||
|
"""Der Bildschirmzugriff dieses Clients -- aus dem, was bekannt ist.
|
||||||
|
|
||||||
|
Diese Seite fragt das Geraet **nicht**. Erreichbarkeit kommt aus dem
|
||||||
|
Ueberwachungsdurchlauf, die Anmeldeverfahren aus der letzten
|
||||||
|
ausdruecklichen Pruefung. Der Grund steht in :func:`vnc.zugang_gemerkt`:
|
||||||
|
jede Messung ist eine angefangene Protokollverhandlung, und ein RealVNC
|
||||||
|
Server zaehlt so etwas mit.
|
||||||
|
"""
|
||||||
|
extension = core()
|
||||||
|
if not current_user.has("device_vnc.execute"):
|
||||||
|
return None
|
||||||
|
if not extension.settings.get(conn, "vnc_enabled"):
|
||||||
|
return None
|
||||||
|
port = int(extension.settings.get(conn, "vnc_port") or vnc.STANDARD_PORT)
|
||||||
|
return vnc.zugang_gemerkt(device, port=port)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.get("/<int:device_id>/vnc")
|
||||||
|
@permission_required("device_vnc.execute")
|
||||||
|
def vnc_connection(device_id: int):
|
||||||
|
"""Die Verbindungsdatei -- drei Zeilen, sonst nichts.
|
||||||
|
|
||||||
|
Kein Passwort, kein Benutzername, keine Verschluesselungsvorgabe: was beim
|
||||||
|
Verbinden verlangt wird, entscheidet der VNC-Server auf dem Geraet. Die
|
||||||
|
Datei traegt nur, wohin es geht und wie die Verbindung im Viewer heisst.
|
||||||
|
"""
|
||||||
|
conn = db()
|
||||||
|
device = inventory.get_device(conn, device_id)
|
||||||
|
if device is None:
|
||||||
|
abort(404)
|
||||||
|
extension = core()
|
||||||
|
if not extension.settings.get(conn, "vnc_enabled"):
|
||||||
|
abort(404)
|
||||||
|
port = int(extension.settings.get(conn, "vnc_port") or vnc.STANDARD_PORT)
|
||||||
|
ziel = vnc.ziel(device)
|
||||||
|
if not ziel:
|
||||||
|
abort(404)
|
||||||
|
# Kein Eintrag in der Aenderungskette: eine Verbindungsdatei ist derselbe
|
||||||
|
# Vorgang wie die Sitzung im Browser, nur fuer einen Viewer auf dem
|
||||||
|
# Arbeitsplatz -- und sie aendert nichts. Wer zusieht, steht im laufenden
|
||||||
|
# Protokoll; das Aenderungsprotokoll bleibt die Liste der Eingriffe.
|
||||||
|
live = core().registry.get("live_log")
|
||||||
|
if live is not None:
|
||||||
|
live.info(
|
||||||
|
"vnc", f"{current_user.username} lädt die Verbindungsdatei für {device['name']}"
|
||||||
|
)
|
||||||
|
return Response(
|
||||||
|
vnc.verbindungsdatei(
|
||||||
|
device,
|
||||||
|
port=port,
|
||||||
|
warnung_aus=bool(extension.settings.get(conn, "vnc_warn_unencrypted_off")),
|
||||||
|
),
|
||||||
|
mimetype="application/octet-stream",
|
||||||
|
headers={
|
||||||
|
"Content-Disposition": f'attachment; filename="{vnc.dateiname(device)}"',
|
||||||
|
# Eine Verbindungsdatei ist kein Dokument, das ein Proxy aufheben
|
||||||
|
# soll -- und sie ist billig genug, sie jedes Mal neu zu erzeugen.
|
||||||
|
"Cache-Control": "no-store",
|
||||||
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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 ""),
|
||||||
|
)
|
||||||
@@ -18,6 +18,8 @@ from tesm_core.auth.service import client_ip
|
|||||||
from tesm_core.extension import core, db, transaction
|
from tesm_core.extension import core, db, transaction
|
||||||
from tesm_core.licensing_client import LicenseError
|
from tesm_core.licensing_client import LicenseError
|
||||||
|
|
||||||
|
import tesm_licensing as lic
|
||||||
|
|
||||||
bp = Blueprint("settings", __name__, url_prefix="/administration")
|
bp = Blueprint("settings", __name__, url_prefix="/administration")
|
||||||
|
|
||||||
|
|
||||||
@@ -46,6 +48,28 @@ def _license_hint(feature: str) -> str:
|
|||||||
return ""
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _module_rows(conn: Any) -> list[dict[str, Any]]:
|
||||||
|
"""Die drei Module mit ihrem Zustand -- lizenziert, benutzt.
|
||||||
|
|
||||||
|
Zwei verschiedene Fragen, die die Seite auseinanderhalten muss: was die
|
||||||
|
Lizenz erlaubt (dagegen hilft kein Schalter) und was dieses Haus benutzen
|
||||||
|
will (dafuer ist der Schalter da).
|
||||||
|
"""
|
||||||
|
manager = core().license
|
||||||
|
if manager is None:
|
||||||
|
return []
|
||||||
|
state = manager.reload(conn)
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
"key": name,
|
||||||
|
"label": lic.MODULE_LABELS.get(name, name),
|
||||||
|
"licensed": name in state.modules,
|
||||||
|
"enabled": name not in state.modules_off,
|
||||||
|
}
|
||||||
|
for name in lic.ALL_MODULES
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
def _current_timezone() -> str:
|
def _current_timezone() -> str:
|
||||||
sysops = core().sysops
|
sysops = core().sysops
|
||||||
if sysops is None:
|
if sysops is None:
|
||||||
@@ -79,6 +103,10 @@ def system():
|
|||||||
"monitor_failures_before_restart",
|
"monitor_failures_before_restart",
|
||||||
"monitor_auto_restart",
|
"monitor_auto_restart",
|
||||||
"dashboard_public",
|
"dashboard_public",
|
||||||
|
"vnc_enabled",
|
||||||
|
"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",
|
||||||
@@ -94,11 +122,16 @@ def system():
|
|||||||
hostname=(sysops.read_command(["hostname"]).stdout.strip() if sysops else ""),
|
hostname=(sysops.read_command(["hostname"]).stdout.strip() if sysops else ""),
|
||||||
timezone=_current_timezone(),
|
timezone=_current_timezone(),
|
||||||
timezones=_timezone_choices(),
|
timezones=_timezone_choices(),
|
||||||
nav_items=extension.nav.items,
|
# Nicht ``nav_items``: dieser Name gehoert dem Seitenrahmen. Wer ihn hier
|
||||||
|
# belegt, ueberschreibt die gefilterte Navigation aus dem Kontext --
|
||||||
|
# und die Seitenleiste zeigte dann jeden Punkt, auch den, den dieser
|
||||||
|
# Benutzer nicht darf und den die Lizenz nicht freischaltet.
|
||||||
|
nav_baum=extension.nav.items,
|
||||||
nav_order=extension.settings.get(conn, "nav_order") or extension.nav.default_order(),
|
nav_order=extension.settings.get(conn, "nav_order") or extension.nav.default_order(),
|
||||||
nav_child_order=extension.settings.get(conn, "nav_child_order") or {},
|
nav_child_order=extension.settings.get(conn, "nav_child_order") or {},
|
||||||
license_hint=_license_hint("monitor_interval"),
|
license_hint=_license_hint("monitor_interval"),
|
||||||
sysops_available=bool(sysops and sysops.available),
|
sysops_available=bool(sysops and sysops.available),
|
||||||
|
module_rows=_module_rows(conn),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -114,6 +147,10 @@ def system_save():
|
|||||||
"monitor_failures_before_restart",
|
"monitor_failures_before_restart",
|
||||||
"monitor_auto_restart",
|
"monitor_auto_restart",
|
||||||
"dashboard_public",
|
"dashboard_public",
|
||||||
|
"vnc_enabled",
|
||||||
|
"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",
|
||||||
@@ -167,6 +204,56 @@ def system_save():
|
|||||||
return redirect(url_for("settings.system"))
|
return redirect(url_for("settings.system"))
|
||||||
|
|
||||||
|
|
||||||
|
@bp.post("/system/modules")
|
||||||
|
@permission_required("settings_system.write")
|
||||||
|
def modules_save():
|
||||||
|
"""Module ein- und ausschalten.
|
||||||
|
|
||||||
|
Abgeschaltet heisst wirklich abgeschaltet: der Punkt verschwindet aus der
|
||||||
|
Navigation, und die Routen antworten mit 404 -- dieselbe Sperre, die auch
|
||||||
|
eine fehlende Lizenz zieht. Daten bleiben unangetastet; wer das Modul
|
||||||
|
wieder einschaltet, findet alles vor wie zuvor.
|
||||||
|
"""
|
||||||
|
extension = core()
|
||||||
|
manager = extension.license
|
||||||
|
if manager is None:
|
||||||
|
flash("Diese Installation nutzt keine Lizenzierung.", "info")
|
||||||
|
return redirect(url_for("settings.system"))
|
||||||
|
|
||||||
|
aus = [name for name in lic.ALL_MODULES if request.form.get(f"module_{name}") != "1"]
|
||||||
|
with transaction() as conn:
|
||||||
|
eingeschaltet, abgeschaltet = manager.module_abschalten(
|
||||||
|
conn, aus, actor=current_user.username
|
||||||
|
)
|
||||||
|
if eingeschaltet or abgeschaltet:
|
||||||
|
teile = []
|
||||||
|
if abgeschaltet:
|
||||||
|
teile.append(
|
||||||
|
"abgeschaltet: "
|
||||||
|
+ ", ".join(lic.MODULE_LABELS.get(n, n) for n in sorted(abgeschaltet))
|
||||||
|
)
|
||||||
|
if eingeschaltet:
|
||||||
|
teile.append(
|
||||||
|
"eingeschaltet: "
|
||||||
|
+ ", ".join(lic.MODULE_LABELS.get(n, n) for n in sorted(eingeschaltet))
|
||||||
|
)
|
||||||
|
audit.log(
|
||||||
|
conn,
|
||||||
|
actor=current_user.username,
|
||||||
|
actor_id=current_user.id,
|
||||||
|
action="settings.modules_changed",
|
||||||
|
detail="; ".join(teile),
|
||||||
|
ip=client_ip(),
|
||||||
|
severity="notice",
|
||||||
|
)
|
||||||
|
|
||||||
|
if eingeschaltet or abgeschaltet:
|
||||||
|
flash("Module gespeichert.", "success")
|
||||||
|
else:
|
||||||
|
flash("Keine Änderungen.", "info")
|
||||||
|
return redirect(url_for("settings.system"))
|
||||||
|
|
||||||
|
|
||||||
@bp.post("/system/hostname")
|
@bp.post("/system/hostname")
|
||||||
@permission_required("settings_system.execute")
|
@permission_required("settings_system.execute")
|
||||||
def set_hostname():
|
def set_hostname():
|
||||||
@@ -226,7 +313,7 @@ def run_check_now():
|
|||||||
extension = core()
|
extension = core()
|
||||||
|
|
||||||
def worker() -> None:
|
def worker() -> None:
|
||||||
from ..services import inventory, monitor
|
from ..services import inventory, monitor, vnc
|
||||||
|
|
||||||
allowed = extension.license is None or extension.license.feature_allowed(
|
allowed = extension.license is None or extension.license.feature_allowed(
|
||||||
"auto_poe_restart"
|
"auto_poe_restart"
|
||||||
@@ -236,6 +323,7 @@ def run_check_now():
|
|||||||
live=extension.registry["live_log"],
|
live=extension.registry["live_log"],
|
||||||
keystore=extension.keystore,
|
keystore=extension.keystore,
|
||||||
license_allows_auto_restart=allowed,
|
license_allows_auto_restart=allowed,
|
||||||
|
vnc_port=vnc.port_aus_einstellungen(extension),
|
||||||
)
|
)
|
||||||
|
|
||||||
threading.Thread(target=worker, name="manual-sweep", daemon=True).start()
|
threading.Thread(target=worker, name="manual-sweep", daemon=True).start()
|
||||||
|
|||||||
@@ -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)",
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -211,7 +221,17 @@ def init_app(app) -> None: # type: ignore[no-untyped-def]
|
|||||||
|
|
||||||
|
|
||||||
def _send(ws, kind: str, text: str) -> None: # type: ignore[no-untyped-def]
|
def _send(ws, kind: str, text: str) -> None: # type: ignore[no-untyped-def]
|
||||||
|
"""Schickt eine Nachricht. Ein geschlossener Socket ist kein Fehler.
|
||||||
|
|
||||||
|
Bricht der Browser die Sitzung ab, schlaegt das Senden fehl -- und die
|
||||||
|
Ausnahme daraus verdeckte bisher den eigentlichen Grund, warum die
|
||||||
|
Sitzung endete: im Protokoll stand ein Stapelverlauf aus ``ws.send``,
|
||||||
|
nicht die Meldung des Switches.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
ws.send(json.dumps({"type": kind, "data": text}))
|
ws.send(json.dumps({"type": kind, "data": text}))
|
||||||
|
except Exception: # noqa: BLE001 - der Browser ist weg, das ist keine Stoerung
|
||||||
|
logger.debug("Konsolennachricht nicht mehr zustellbar (%s)", kind)
|
||||||
|
|
||||||
|
|
||||||
def _transcript_name(label: str) -> str:
|
def _transcript_name(label: str) -> str:
|
||||||
@@ -220,8 +240,17 @@ def _transcript_name(label: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def _handle_socket(ws, kind: str, target_id: int) -> None: # type: ignore[no-untyped-def]
|
def _handle_socket(ws, kind: str, target_id: int) -> None: # type: ignore[no-untyped-def]
|
||||||
|
"""Eine Konsolensitzung, von der Berechtigung bis zum Abmelden.
|
||||||
|
|
||||||
|
Jede Stufe schickt eine Zeile in die Oberflaeche. Das ist keine Zierde:
|
||||||
|
eine Konsole, die nichts sagt, laesst offen, ob die Berechtigung fehlt,
|
||||||
|
die Zugangsdaten, die Verbindung oder die Antwort des Geraets -- und genau
|
||||||
|
diese Frage stand im Betrieb im Raum. Sichtbar sind die Zeilen erst
|
||||||
|
nuetzlich, wenn sie **vor** dem Warten kommen, nicht danach.
|
||||||
|
"""
|
||||||
extension = core()
|
extension = core()
|
||||||
conn = db()
|
conn = db()
|
||||||
|
_send(ws, "info", "Sitzung wird geöffnet …")
|
||||||
|
|
||||||
# -- Berechtigungen: dieselben Pruefungen wie in der HTTP-Route. Ein
|
# -- Berechtigungen: dieselben Pruefungen wie in der HTTP-Route. Ein
|
||||||
# WebSocket umgeht Dekoratoren, also stehen sie hier ausdruecklich.
|
# WebSocket umgeht Dekoratoren, also stehen sie hier ausdruecklich.
|
||||||
@@ -255,6 +284,7 @@ def _handle_socket(ws, kind: str, target_id: int) -> None: # type: ignore[no-un
|
|||||||
/ f"{stamp}-{_transcript_name(current_user.username)}-{_transcript_name(target.label)}.log"
|
/ f"{stamp}-{_transcript_name(current_user.username)}-{_transcript_name(target.label)}.log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_send(ws, "info", f"Ziel: {target.label} ({target.host}:{target.port})")
|
||||||
try:
|
try:
|
||||||
username, password = inventory.credential_secret(
|
username, password = inventory.credential_secret(
|
||||||
conn, extension.keystore, target.credential_id
|
conn, extension.keystore, target.credential_id
|
||||||
@@ -262,6 +292,7 @@ def _handle_socket(ws, kind: str, target_id: int) -> None: # type: ignore[no-un
|
|||||||
except inventory.ValidationError as exc:
|
except inventory.ValidationError as exc:
|
||||||
_send(ws, "error", str(exc))
|
_send(ws, "error", str(exc))
|
||||||
return
|
return
|
||||||
|
_send(ws, "info", f"Anmeldung als {username} …")
|
||||||
|
|
||||||
with extension.database.transaction(conn):
|
with extension.database.transaction(conn):
|
||||||
audit.log(
|
audit.log(
|
||||||
@@ -343,6 +374,7 @@ def _run_switch_session( # type: ignore[no-untyped-def]
|
|||||||
ws, conn, target: Target, username: str, password: str, transcript, idle_timeout: int
|
ws, conn, target: Target, username: str, password: str, transcript, idle_timeout: int
|
||||||
) -> int:
|
) -> int:
|
||||||
commands = 0
|
commands = 0
|
||||||
|
_send(ws, "info", "Warte auf die Eingabeaufforderung des Switches …")
|
||||||
with switchcli.SwitchCli(
|
with switchcli.SwitchCli(
|
||||||
conn, host=target.host, port=target.port, username=username, password=password
|
conn, host=target.host, port=target.port, username=username, password=password
|
||||||
) as cli:
|
) as cli:
|
||||||
@@ -375,6 +407,7 @@ def _run_client_session( # type: ignore[no-untyped-def]
|
|||||||
ws, conn, target: Target, username: str, password: str, transcript, idle_timeout: int
|
ws, conn, target: Target, username: str, password: str, transcript, idle_timeout: int
|
||||||
) -> int:
|
) -> int:
|
||||||
commands = 0
|
commands = 0
|
||||||
|
_send(ws, "info", "Verbindung wird aufgebaut …")
|
||||||
with shellcli.ShellSession(
|
with shellcli.ShellSession(
|
||||||
conn, host=target.host, port=target.port, username=username, password=password
|
conn, host=target.host, port=target.port, username=username, password=password
|
||||||
) as session:
|
) as session:
|
||||||
|
|||||||
+164
-2
@@ -69,13 +69,41 @@ def cmd_create_admin(args: argparse.Namespace) -> int:
|
|||||||
)
|
)
|
||||||
user_id = int(cursor.lastrowid or 0)
|
user_id = int(cursor.lastrowid or 0)
|
||||||
action = "angelegt"
|
action = "angelegt"
|
||||||
|
# Der Vollzugriff gehoert dazu. Seit 2.8.0 gibt das
|
||||||
|
# Administratorkennzeichen den Verwaltungsbereich und sonst
|
||||||
|
# nichts; Funktionsrechte kommen ueber Gruppen. Ein Konto, das
|
||||||
|
# jemand an der Kommandozeile anlegt, ist aber der Notausgang --
|
||||||
|
# es entsteht, wenn sonst niemand mehr hineinkommt, und dann
|
||||||
|
# nuetzt eine Verwaltung ohne Geraete, Freigaben und Protokolle
|
||||||
|
# wenig. Wer das nicht will, nimmt die Gruppe in einem Klick
|
||||||
|
# wieder weg.
|
||||||
|
from .permissions import FUNKTIONEN_VOLLZUGRIFF, PERMISSIONS
|
||||||
|
|
||||||
|
gruppe = rbac.funktionsgruppe_anlegen(
|
||||||
|
conn, PERMISSIONS, FUNKTIONEN_VOLLZUGRIFF
|
||||||
|
)
|
||||||
|
# Liegt sie im Papierkorb, kommt sie zurueck. Sonst haette dieser
|
||||||
|
# Aufruf nichts bewirkt: das Konto waere in einer geloeschten
|
||||||
|
# Gruppe -- und das faellt genau dann auf, wenn man es am
|
||||||
|
# wenigsten gebrauchen kann.
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE groups SET deleted_at=NULL WHERE id=? AND deleted_at IS NOT NULL",
|
||||||
|
(gruppe,),
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?,?)",
|
||||||
|
(user_id, gruppe),
|
||||||
|
)
|
||||||
audit.log(
|
audit.log(
|
||||||
conn,
|
conn,
|
||||||
actor="cli",
|
actor="cli",
|
||||||
actor_id=user_id,
|
actor_id=user_id,
|
||||||
action="user.admin_created",
|
action="user.admin_created",
|
||||||
target=username,
|
target=username,
|
||||||
detail=f"über die Kommandozeile {action}",
|
detail=(
|
||||||
|
f"über die Kommandozeile {action}, Gruppe "
|
||||||
|
f"{FUNKTIONEN_VOLLZUGRIFF.name} zugewiesen"
|
||||||
|
),
|
||||||
severity="warning",
|
severity="warning",
|
||||||
)
|
)
|
||||||
print(f"Administrator {username!r} {action}.")
|
print(f"Administrator {username!r} {action}.")
|
||||||
@@ -191,7 +219,7 @@ def cmd_restore_signatures(_args: argparse.Namespace) -> int:
|
|||||||
|
|
||||||
def cmd_check_now(_args: argparse.Namespace) -> int:
|
def cmd_check_now(_args: argparse.Namespace) -> int:
|
||||||
app, extension = _app_and_core()
|
app, extension = _app_and_core()
|
||||||
from .services import inventory, monitor
|
from .services import inventory, monitor, vnc
|
||||||
|
|
||||||
with app.app_context():
|
with app.app_context():
|
||||||
allowed = extension.license is None or extension.license.feature_allowed(
|
allowed = extension.license is None or extension.license.feature_allowed(
|
||||||
@@ -202,6 +230,7 @@ def cmd_check_now(_args: argparse.Namespace) -> int:
|
|||||||
live=extension.registry["live_log"],
|
live=extension.registry["live_log"],
|
||||||
keystore=extension.keystore,
|
keystore=extension.keystore,
|
||||||
license_allows_auto_restart=allowed,
|
license_allows_auto_restart=allowed,
|
||||||
|
vnc_port=vnc.port_aus_einstellungen(extension),
|
||||||
)
|
)
|
||||||
print(
|
print(
|
||||||
f"{report.checked} geprüft, {report.online} online, {report.offline} offline, "
|
f"{report.checked} geprüft, {report.online} online, {report.offline} offline, "
|
||||||
@@ -216,6 +245,123 @@ def cmd_web_setup(args: argparse.Namespace) -> int:
|
|||||||
return run_web_setup(app, extension, args)
|
return run_web_setup(app, extension, args)
|
||||||
|
|
||||||
|
|
||||||
|
def cmd_switch_test(args: argparse.Namespace) -> int:
|
||||||
|
"""Prueft Stufe fuer Stufe, wo eine Switch-Sitzung haengt.
|
||||||
|
|
||||||
|
Warum ein eigener Befehl und nicht die Konsole im Browser: dort liegen
|
||||||
|
zwischen dem Switch und dem Auge ein WebSocket, ein Reverse Proxy und
|
||||||
|
JavaScript. Wenn nichts ankommt, ist die erste Frage, *welche* dieser
|
||||||
|
Schichten schweigt. Hier gibt es keine davon -- und jede Stufe sagt, was
|
||||||
|
sie bekommen hat, notfalls als rohe Bytes.
|
||||||
|
|
||||||
|
Liest nur, ausser mit ``--zyklus``: dann wird der Port wirklich
|
||||||
|
geschaltet.
|
||||||
|
"""
|
||||||
|
from .services import inventory, switchcli
|
||||||
|
from .services.ssh import SshError
|
||||||
|
from tesm_core.db import Database
|
||||||
|
from tesm_core.extension import core
|
||||||
|
|
||||||
|
app = create_app()
|
||||||
|
erweiterung = core(app)
|
||||||
|
|
||||||
|
def sagen(stufe: str, text: str = "") -> None:
|
||||||
|
print(f" {stufe:<28} {text}".rstrip())
|
||||||
|
|
||||||
|
with app.app_context():
|
||||||
|
with erweiterung.database.session() as conn:
|
||||||
|
zeile = Database.one(
|
||||||
|
conn,
|
||||||
|
"SELECT id, hostname, ip, ssh_port, credential_id FROM switches "
|
||||||
|
"WHERE (hostname=? COLLATE NOCASE OR ip=?) AND deleted_at IS NULL",
|
||||||
|
(args.switch, args.switch),
|
||||||
|
)
|
||||||
|
if zeile is None:
|
||||||
|
print(f"Kein Switch mit Name oder Adresse {args.switch!r}.")
|
||||||
|
print("Vorhanden:")
|
||||||
|
for andere in Database.all(
|
||||||
|
conn, "SELECT hostname, ip FROM switches WHERE deleted_at IS NULL"
|
||||||
|
):
|
||||||
|
print(f" {andere['hostname']} ({andere['ip']})")
|
||||||
|
return 2
|
||||||
|
|
||||||
|
host = str(zeile["ip"])
|
||||||
|
ssh_port = int(zeile["ssh_port"] or 22)
|
||||||
|
print(f"Switch {zeile['hostname']} ({host}:{ssh_port})")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if not zeile["credential_id"]:
|
||||||
|
sagen("Zugangsdaten", "FEHLT -- dem Switch sind keine zugeordnet")
|
||||||
|
return 1
|
||||||
|
try:
|
||||||
|
benutzer, passwort = inventory.credential_secret(
|
||||||
|
conn, erweiterung.keystore, int(zeile["credential_id"])
|
||||||
|
)
|
||||||
|
except Exception as fehler: # noqa: BLE001
|
||||||
|
sagen("Zugangsdaten", f"nicht lesbar: {fehler}")
|
||||||
|
return 1
|
||||||
|
sagen("Zugangsdaten", f"{benutzer} (Passwort {len(passwort)} Zeichen)")
|
||||||
|
|
||||||
|
from .services import ssh as ssh_modul
|
||||||
|
|
||||||
|
for record in ssh_modul.all_known_keys(conn, host, ssh_port):
|
||||||
|
sagen("Freigegebener Schluessel", f"{record.key_type} {record.fingerprint}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
sitzung = switchcli.SwitchCli(
|
||||||
|
conn,
|
||||||
|
host=host,
|
||||||
|
port=ssh_port,
|
||||||
|
username=benutzer,
|
||||||
|
password=passwort,
|
||||||
|
)
|
||||||
|
with sitzung as cli:
|
||||||
|
sagen("Anmeldung", "erfolgreich")
|
||||||
|
erstes = cli.full_transcript
|
||||||
|
sagen("Erste Antwort", f"{len(erstes)} Zeichen")
|
||||||
|
print()
|
||||||
|
print("--- was der Switch nach der Anmeldung geschickt hat ---")
|
||||||
|
print(erstes if erstes.strip() else "(nichts)")
|
||||||
|
print("--- Ende ---")
|
||||||
|
print()
|
||||||
|
|
||||||
|
if args.port:
|
||||||
|
ausgabe = cli.send(
|
||||||
|
f"show power-over-ethernet {args.port}", tolerate_error=True
|
||||||
|
)
|
||||||
|
sagen("Zustandsabfrage", f"{len(ausgabe)} Zeichen")
|
||||||
|
print("--- show power-over-ethernet ---")
|
||||||
|
print(ausgabe)
|
||||||
|
print("--- Ende ---")
|
||||||
|
sagen("Erkannter Zustand", cli.poe_state(args.port))
|
||||||
|
|
||||||
|
if args.zyklus:
|
||||||
|
if not args.port:
|
||||||
|
print("Fuer --zyklus braucht es --port.")
|
||||||
|
return 2
|
||||||
|
ergebnis = cli.cycle_poe(args.port)
|
||||||
|
sagen("Portzyklus", "erfolgreich" if ergebnis.ok else ergebnis.error)
|
||||||
|
for schritt in ergebnis.steps:
|
||||||
|
sagen("", schritt)
|
||||||
|
if not ergebnis.ok:
|
||||||
|
return 1
|
||||||
|
except SshError as fehler:
|
||||||
|
sagen("FEHLGESCHLAGEN", str(fehler))
|
||||||
|
# Gerade dann sind die Bytes interessant: "kein Prompt" kann
|
||||||
|
# heissen, dass nichts kam -- oder dass etwas kam, das nicht
|
||||||
|
# erkannt wurde. Das sieht man nur im Rohtext, und
|
||||||
|
# Steuerzeichen sieht man nur als repr().
|
||||||
|
roh = "".join(sitzung.transcript)
|
||||||
|
print()
|
||||||
|
print("--- rohe Antwort des Switches (mit Steuerzeichen) ---")
|
||||||
|
print(repr(roh) if roh else "(nichts)")
|
||||||
|
print("--- Ende ---")
|
||||||
|
return 1
|
||||||
|
print()
|
||||||
|
print("Fertig.")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def cmd_convert_legacy(args: argparse.Namespace) -> int:
|
def cmd_convert_legacy(args: argparse.Namespace) -> int:
|
||||||
"""Uebersetzt einen Export des alten TESM in das neue Format.
|
"""Uebersetzt einen Export des alten TESM in das neue Format.
|
||||||
|
|
||||||
@@ -418,6 +564,22 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
check = sub.add_parser("check-now", help="Einen Überwachungsdurchlauf sofort ausführen")
|
check = sub.add_parser("check-now", help="Einen Überwachungsdurchlauf sofort ausführen")
|
||||||
check.set_defaults(func=cmd_check_now)
|
check.set_defaults(func=cmd_check_now)
|
||||||
|
|
||||||
|
pruefung = sub.add_parser(
|
||||||
|
"switch-test",
|
||||||
|
help="Eine Switch-Sitzung Stufe fuer Stufe pruefen",
|
||||||
|
description=(
|
||||||
|
"Meldet sich am Switch an und zeigt, was er schickt -- ohne Browser, "
|
||||||
|
"ohne WebSocket, ohne Reverse Proxy. Liest nur; mit --zyklus wird der "
|
||||||
|
"Port wirklich geschaltet."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
pruefung.add_argument("switch", help="Name oder IP des Switches, wie in TESM hinterlegt")
|
||||||
|
pruefung.add_argument("--port", help="Portbezeichnung, z. B. 1/8 -- fragt den Zustand ab")
|
||||||
|
pruefung.add_argument(
|
||||||
|
"--zyklus", action="store_true", help="PoE wirklich aus- und wieder einschalten"
|
||||||
|
)
|
||||||
|
pruefung.set_defaults(func=cmd_switch_test)
|
||||||
|
|
||||||
legacy = sub.add_parser(
|
legacy = sub.add_parser(
|
||||||
"convert-legacy",
|
"convert-legacy",
|
||||||
help="Export des alten TESM in das neue Format übersetzen",
|
help="Export des alten TESM in das neue Format übersetzen",
|
||||||
|
|||||||
@@ -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",),
|
||||||
|
|||||||
@@ -78,6 +78,26 @@ DEVICE_RESTART_RPC = Resource(
|
|||||||
action_hints={A.EXECUTE: "Neustartauftrag anlegen (wie 'shutdown /m')."},
|
action_hints={A.EXECUTE: "Neustartauftrag anlegen (wie 'shutdown /m')."},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: Bildschirmzugriff ist **nicht** dasselbe wie die Konsole, auch wenn beides
|
||||||
|
#: "auf das Geraet" fuehrt: hier sieht jemand den Bildschirm, wie er ist --
|
||||||
|
#: samt dem, was ein Mensch dort gerade offen hat. Das ist eine eigene
|
||||||
|
#: Entscheidung, also eine eigene Zeile. Sie wird **niemandem** mitgegeben:
|
||||||
|
#: keine Migration setzt sie, auch nicht bei denen, die schon eine Konsole
|
||||||
|
#: oeffnen duerfen. Wer sie haben soll, bekommt sie in der Matrix -- einmal,
|
||||||
|
#: sichtbar, mit Zeile im Aenderungsprotokoll.
|
||||||
|
DEVICE_VNC = Resource(
|
||||||
|
key="device_vnc",
|
||||||
|
parent="devices",
|
||||||
|
label="Clients: Bildschirm (VNC)",
|
||||||
|
actions=(A.READ, A.EXECUTE),
|
||||||
|
hint="Verbindungsdatei bzw. vnc://-Adresse für den Bildschirm des Clients",
|
||||||
|
action_hints={
|
||||||
|
A.READ: "Den Knopf sehen, wo ein VNC-Server antwortet.",
|
||||||
|
A.EXECUTE: "Verbindung öffnen. Die Datei enthält nur Ziel und Namen – "
|
||||||
|
"was beim Verbinden verlangt wird, entscheidet der VNC-Server auf dem Gerät.",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
#: Die Client-Konsole lief unter ``maintenance.execute`` mit -- demselben Recht
|
#: Die Client-Konsole lief unter ``maintenance.execute`` mit -- demselben Recht
|
||||||
#: wie die Paketaktualisierung. Eine Sitzung auf einem Geraet ist aber etwas
|
#: wie die Paketaktualisierung. Eine Sitzung auf einem Geraet ist aber etwas
|
||||||
#: anderes als ein Auftrag, den der Server abarbeitet: hier tippt ein Mensch.
|
#: anderes als ein Auftrag, den der Server abarbeitet: hier tippt ein Mensch.
|
||||||
@@ -167,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.
|
||||||
@@ -174,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,
|
||||||
@@ -316,6 +355,7 @@ MONITORING_AREA = Area(
|
|||||||
DEVICE_RESTART_SSH,
|
DEVICE_RESTART_SSH,
|
||||||
DEVICE_RESTART_RPC,
|
DEVICE_RESTART_RPC,
|
||||||
DEVICE_CONSOLE,
|
DEVICE_CONSOLE,
|
||||||
|
DEVICE_VNC,
|
||||||
SWITCHES,
|
SWITCHES,
|
||||||
SWITCH_CONSOLE,
|
SWITCH_CONSOLE,
|
||||||
HOSTKEYS,
|
HOSTKEYS,
|
||||||
@@ -333,7 +373,7 @@ 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(
|
||||||
@@ -388,6 +428,15 @@ FUNKTIONEN_VOLLZUGRIFF = RolePreset(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: Die Gruppen, die eine frische Installation mitbringt -- drei, nicht sieben.
|
||||||
|
#:
|
||||||
|
#: Vorher standen hier zusaetzlich Betrieb, Support, Netzwerk und Revision.
|
||||||
|
#: Gut gemeint, aber sie beantworten eine Frage, die jeder Betrieb anders
|
||||||
|
#: beantwortet: welche Rolle es gibt, weiss der Kunde, nicht die Anwendung.
|
||||||
|
#: Was bleibt, sind die drei, ohne die man nicht anfangen kann -- eine
|
||||||
|
#: Standardgruppe fuer jeden neuen Nutzer, eine fuer die Verwaltung und eine,
|
||||||
|
#: die alle Funktionen buendelt. Weitere legt man in der Matrix an, und die
|
||||||
|
#: Matrix zeigt seit 2.8.0 je Funktion eine Zeile.
|
||||||
ROLE_PRESETS: tuple[RolePreset, ...] = (
|
ROLE_PRESETS: tuple[RolePreset, ...] = (
|
||||||
RolePreset(
|
RolePreset(
|
||||||
key="viewer",
|
key="viewer",
|
||||||
@@ -400,78 +449,6 @@ ROLE_PRESETS: tuple[RolePreset, ...] = (
|
|||||||
),
|
),
|
||||||
is_default=True,
|
is_default=True,
|
||||||
),
|
),
|
||||||
RolePreset(
|
|
||||||
key="operations",
|
|
||||||
name="Betrieb",
|
|
||||||
description="Geräte und Switche pflegen, Neustarts auslösen, Wartung fahren.",
|
|
||||||
grants=(
|
|
||||||
("devices", "*"),
|
|
||||||
("device_restart_poe", "*"),
|
|
||||||
("device_restart_ssh", "*"),
|
|
||||||
("device_restart_rpc", "*"),
|
|
||||||
("device_console", "*"),
|
|
||||||
("switches", (A.READ, A.CREATE, A.WRITE)),
|
|
||||||
("switch_console", "*"),
|
|
||||||
("hostkeys", "*"),
|
|
||||||
("credentials", (A.READ,)),
|
|
||||||
("servicing_update", "*"),
|
|
||||||
("servicing_reboot", "*"),
|
|
||||||
("servicing_tool", "*"),
|
|
||||||
("logs_live", (A.READ,)),
|
|
||||||
("logs_history", (A.READ,)),
|
|
||||||
("trash", (A.READ, A.WRITE)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
RolePreset(
|
|
||||||
key="support",
|
|
||||||
name="Support",
|
|
||||||
description="Erste Hilfe am Telefon: Status sehen, PoE-Neustart auslösen.",
|
|
||||||
grants=(
|
|
||||||
("devices", (A.READ,)),
|
|
||||||
# Nur der PoE-Weg: der wirkt auch bei einem haengenden Geraet und
|
|
||||||
# unterbricht nichts Laufendes. SSH und RPC fahren ein arbeitendes
|
|
||||||
# System herunter -- das gehoert nicht in die erste Hilfe.
|
|
||||||
("device_restart_poe", "*"),
|
|
||||||
("switches", (A.READ,)),
|
|
||||||
("logs_live", (A.READ,)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
RolePreset(
|
|
||||||
key="network",
|
|
||||||
name="Netzwerk",
|
|
||||||
description="DHCP und Host-Netzwerkeinstellungen.",
|
|
||||||
grants=(
|
|
||||||
("dhcp_subnets", "*"),
|
|
||||||
("dhcp_options", "*"),
|
|
||||||
("dhcp_reservations", "*"),
|
|
||||||
("dhcp_config", "*"),
|
|
||||||
("dhcp_service", "*"),
|
|
||||||
("settings_network", "*"),
|
|
||||||
("logs_dhcp", (A.READ,)),
|
|
||||||
("devices", (A.READ,)),
|
|
||||||
("switches", (A.READ,)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
RolePreset(
|
|
||||||
key="auditor",
|
|
||||||
name="Revision",
|
|
||||||
description="Nur lesen – inklusive Änderungsprotokoll und Diagnose.",
|
|
||||||
grants=(
|
|
||||||
("devices", (A.READ,)),
|
|
||||||
("switches", (A.READ,)),
|
|
||||||
("credentials", (A.READ,)),
|
|
||||||
("dhcp_subnets", (A.READ,)),
|
|
||||||
("dhcp_options", (A.READ,)),
|
|
||||||
("dhcp_reservations", (A.READ,)),
|
|
||||||
("users", (A.READ,)),
|
|
||||||
("groups", (A.READ,)),
|
|
||||||
("logs_live", (A.READ,)),
|
|
||||||
("logs_history", (A.READ, A.EXPORT)),
|
|
||||||
("logs_audit", (A.READ, A.EXPORT)),
|
|
||||||
("diagnostics", (A.READ,)),
|
|
||||||
("trash", (A.READ,)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
RolePreset(
|
RolePreset(
|
||||||
key="administration",
|
key="administration",
|
||||||
name="Administration",
|
name="Administration",
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ Frische Zeitlinie ab Version 1 -- kein Nachbau der 35 gewachsenen
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from tesm_core.migrations import Migration
|
from tesm_core.migrations import Migration
|
||||||
|
|
||||||
V1 = """
|
V1 = """
|
||||||
@@ -393,6 +395,185 @@ def uebergang_funktionsgruppe(conn) -> None: # type: ignore[no-untyped-def]
|
|||||||
rbac.funktionsgruppe_fuer_administratoren(conn, PERMISSIONS, FUNKTIONEN_VOLLZUGRIFF)
|
rbac.funktionsgruppe_fuer_administratoren(conn, PERMISSIONS, FUNKTIONEN_VOLLZUGRIFF)
|
||||||
|
|
||||||
|
|
||||||
|
#: Die vier Vorlagen, die es bis 2.8.6 gab und ab 2.8.7 nicht mehr.
|
||||||
|
#:
|
||||||
|
#: Stehen hier als eingefrorene Kopie, weil eine Migration ihre eigenen Daten
|
||||||
|
#: mitbringen muss: in ``permissions.py`` sind sie weg, und trotzdem muss
|
||||||
|
#: dieser Schritt noch erkennen koennen, ob eine vorgefundene Gruppe wirklich
|
||||||
|
#: unveraendert die ausgelieferte ist.
|
||||||
|
ABGELEGTE_VORLAGEN: tuple[tuple[str, str, tuple[tuple[str, Any], ...]], ...] = (
|
||||||
|
(
|
||||||
|
"operations",
|
||||||
|
"Betrieb",
|
||||||
|
(
|
||||||
|
("devices", "*"),
|
||||||
|
("device_restart_poe", "*"),
|
||||||
|
("device_restart_ssh", "*"),
|
||||||
|
("device_restart_rpc", "*"),
|
||||||
|
("device_console", "*"),
|
||||||
|
("switches", ("read", "create", "write")),
|
||||||
|
("switch_console", "*"),
|
||||||
|
("hostkeys", "*"),
|
||||||
|
("credentials", ("read",)),
|
||||||
|
("servicing_update", "*"),
|
||||||
|
("servicing_reboot", "*"),
|
||||||
|
("servicing_tool", "*"),
|
||||||
|
("logs_live", ("read",)),
|
||||||
|
("logs_history", ("read",)),
|
||||||
|
("trash", ("read", "write")),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"support",
|
||||||
|
"Support",
|
||||||
|
(
|
||||||
|
("devices", ("read",)),
|
||||||
|
("device_restart_poe", "*"),
|
||||||
|
("switches", ("read",)),
|
||||||
|
("logs_live", ("read",)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"network",
|
||||||
|
"Netzwerk",
|
||||||
|
(
|
||||||
|
("dhcp_subnets", "*"),
|
||||||
|
("dhcp_options", "*"),
|
||||||
|
("dhcp_reservations", "*"),
|
||||||
|
("dhcp_config", "*"),
|
||||||
|
("dhcp_service", "*"),
|
||||||
|
("settings_network", "*"),
|
||||||
|
("logs_dhcp", ("read",)),
|
||||||
|
("devices", ("read",)),
|
||||||
|
("switches", ("read",)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"auditor",
|
||||||
|
"Revision",
|
||||||
|
(
|
||||||
|
("devices", ("read",)),
|
||||||
|
("switches", ("read",)),
|
||||||
|
("credentials", ("read",)),
|
||||||
|
("dhcp_subnets", ("read",)),
|
||||||
|
("dhcp_options", ("read",)),
|
||||||
|
("dhcp_reservations", ("read",)),
|
||||||
|
("users", ("read",)),
|
||||||
|
("groups", ("read",)),
|
||||||
|
("logs_live", ("read",)),
|
||||||
|
("logs_history", ("read", "export")),
|
||||||
|
("logs_audit", ("read", "export")),
|
||||||
|
("diagnostics", ("read",)),
|
||||||
|
("trash", ("read",)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def eingefrorene_vorlagen(
|
||||||
|
beschreibung: tuple[tuple[str, str, tuple[tuple[str, Any], ...]], ...],
|
||||||
|
) -> list[Any]:
|
||||||
|
"""Baut aus der Kopie oben wieder ``RolePreset``-Objekte.
|
||||||
|
|
||||||
|
Die Aktionen stehen dort als Text und nicht als ``Action``, damit diese
|
||||||
|
Migration auch dann noch lesbar bleibt, wenn das Enum sich weiterentwickelt.
|
||||||
|
"""
|
||||||
|
from tesm_core.rbac.model import Action, RolePreset
|
||||||
|
|
||||||
|
vorlagen = []
|
||||||
|
for key, name, grants in beschreibung:
|
||||||
|
gewandelt = tuple(
|
||||||
|
(ressource, aktionen if aktionen == "*" else tuple(Action(a) for a in aktionen))
|
||||||
|
for ressource, aktionen in grants
|
||||||
|
)
|
||||||
|
vorlagen.append(RolePreset(key=key, name=name, description="", grants=gewandelt))
|
||||||
|
return vorlagen
|
||||||
|
|
||||||
|
|
||||||
|
# Version 10 ----------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Wo antwortet ein VNC-Server? Die Frage beantwortet der Ueberwachungs-
|
||||||
|
# durchlauf mit, der ohnehin jedes Geraet anfasst -- einmal je Intervall statt
|
||||||
|
# einmal je Seitenaufruf. Die Kacheln der Statusuebersicht koennen den Knopf
|
||||||
|
# damit zeigen, ohne dass das Rendern der Seite ein Dutzend Verbindungen
|
||||||
|
# aufbaut.
|
||||||
|
V10 = """
|
||||||
|
ALTER TABLE device_status ADD COLUMN vnc_open INTEGER NOT NULL DEFAULT 0;
|
||||||
|
ALTER TABLE device_status ADD COLUMN vnc_checked_at TEXT;
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
# Version 11 ----------------------------------------------------------------
|
||||||
|
#
|
||||||
|
# Nicht nur *ob* dort ein VNC-Server antwortet, sondern *wohin* gefragt wurde.
|
||||||
|
# Die Kacheln der Statusuebersicht verlinken die Verbindung unmittelbar
|
||||||
|
# (vnc://hap02); das Ziel dafuer aus dem Namen neu herzuleiten hiesse, bei
|
||||||
|
# jedem Seitenaufbau je Kachel eine Namensaufloesung zu versuchen. Der
|
||||||
|
# Durchlauf hat sie ohnehin schon gemacht -- also schreibt er sie hin.
|
||||||
|
V11 = """
|
||||||
|
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]
|
||||||
|
"""Aus sieben Standardgruppen werden drei -- ohne jemandem etwas wegzunehmen.
|
||||||
|
|
||||||
|
Was eine Anwendung an Rollen mitbringt, ist eine Vermutung darueber, wie
|
||||||
|
ein Betrieb organisiert ist. Sieben davon sind sechs zu viele: wer sie
|
||||||
|
nicht braucht, muss sie trotzdem jedes Mal ueberlesen, und in der
|
||||||
|
Rechtematrix stand fuer jede eine Spalte. Es bleiben "Benutzer",
|
||||||
|
"Administration" und "Funktionen (Vollzugriff)".
|
||||||
|
|
||||||
|
Weggeraeumt wird nur, was unberuehrt ist -- keine Mitglieder, Rechte
|
||||||
|
unveraendert. Alles andere bleibt stehen, wo es steht. Und auch das
|
||||||
|
Weggeraeumte liegt nur im Papierkorb.
|
||||||
|
"""
|
||||||
|
from tesm_core.rbac import service as rbac
|
||||||
|
|
||||||
|
from .permissions import PERMISSIONS
|
||||||
|
|
||||||
|
rbac.unberuehrte_vorlagen_entfernen(
|
||||||
|
conn, PERMISSIONS, eingefrorene_vorlagen(ABGELEGTE_VORLAGEN)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
TESM_MIGRATIONS: tuple[Migration, ...] = (
|
TESM_MIGRATIONS: tuple[Migration, ...] = (
|
||||||
Migration(version=1, name="baseline", sql=V1),
|
Migration(version=1, name="baseline", sql=V1),
|
||||||
Migration(version=2, name="seed-dhcp-options", fn=seed_dhcp_options),
|
Migration(version=2, name="seed-dhcp-options", fn=seed_dhcp_options),
|
||||||
@@ -412,4 +593,10 @@ TESM_MIGRATIONS: tuple[Migration, ...] = (
|
|||||||
),
|
),
|
||||||
Migration(version=7, name="signaturverzeichnis", sql=V7, fn=uebernehme_signaturen),
|
Migration(version=7, name="signaturverzeichnis", sql=V7, fn=uebernehme_signaturen),
|
||||||
Migration(version=8, name="funktionsgruppe-fuer-admins", fn=uebergang_funktionsgruppe),
|
Migration(version=8, name="funktionsgruppe-fuer-admins", fn=uebergang_funktionsgruppe),
|
||||||
|
Migration(version=9, name="standardgruppen-aufraeumen", fn=uebergang_standardgruppen),
|
||||||
|
Migration(version=10, name="vnc-erreichbarkeit", sql=V10),
|
||||||
|
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),
|
||||||
)
|
)
|
||||||
@@ -542,7 +542,11 @@ def list_devices(
|
|||||||
WHERE k.host = d.ip AND k.port = d.ssh_port) AS host_key_known,
|
WHERE k.host = d.ip AND k.port = d.ssh_port) AS host_key_known,
|
||||||
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
|
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_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,6 +950,7 @@ def run_reboot(
|
|||||||
f"{wait_seconds}s nicht wieder geantwortet."
|
f"{wait_seconds}s nicht wieder geantwortet."
|
||||||
),
|
),
|
||||||
method=method,
|
method=method,
|
||||||
|
live=live,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -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,6 +585,48 @@ class SweepReport:
|
|||||||
duration_ms: int = 0
|
duration_ms: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
def merke_vnc(conn: sqlite3.Connection, device_id: int, offen: bool, ziel: str = "") -> None:
|
||||||
|
"""Haelt fest, ob dort ein VNC-Server antwortet -- und unter welchem Ziel.
|
||||||
|
|
||||||
|
Das Ziel gehoert dazu, weil die Statusuebersicht die Verbindung unmittelbar
|
||||||
|
verlinkt. Es aus dem Namen neu herzuleiten hiesse, je Kachel und
|
||||||
|
Seitenaufbau eine Namensaufloesung zu versuchen; hier steht es einmal
|
||||||
|
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(
|
||||||
|
"UPDATE device_status SET vnc_open=?, vnc_checked_at=?, vnc_target=? WHERE 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,
|
||||||
@@ -514,13 +634,25 @@ def run_sweep(
|
|||||||
keystore: Any,
|
keystore: Any,
|
||||||
license_allows_auto_restart: bool,
|
license_allows_auto_restart: bool,
|
||||||
failures_before_restart: int = DEFAULT_FAILURES_BEFORE_RESTART,
|
failures_before_restart: int = DEFAULT_FAILURES_BEFORE_RESTART,
|
||||||
|
vnc_port: int = 0,
|
||||||
) -> SweepReport:
|
) -> SweepReport:
|
||||||
"""Ein vollstaendiger Ueberwachungsdurchlauf.
|
"""Ein vollstaendiger Ueberwachungsdurchlauf.
|
||||||
|
|
||||||
Die Pruefungen laufen parallel (I/O-gebunden), die Datenbankschreibvorgaenge
|
Die Pruefungen laufen parallel (I/O-gebunden), die Datenbankschreibvorgaenge
|
||||||
danach in **einer** Transaktion -- so gibt es keinen Schreib-Wettlauf und
|
danach in **einer** Transaktion -- so gibt es keinen Schreib-Wettlauf und
|
||||||
keine halb geschriebenen Durchlaeufe.
|
keine halb geschriebenen Durchlaeufe.
|
||||||
|
|
||||||
|
``vnc_port`` (0 = aus) haengt eine zweite, sehr kurze Frage an jedes
|
||||||
|
erreichbare Geraet: nimmt dort ein VNC-Server Verbindungen an? Sie gehoert
|
||||||
|
hierher und nicht in den Seitenaufbau -- einmal je Intervall statt einmal
|
||||||
|
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
|
||||||
|
|
||||||
report = SweepReport()
|
report = SweepReport()
|
||||||
started = time.monotonic()
|
started = time.monotonic()
|
||||||
|
|
||||||
@@ -531,15 +663,37 @@ def run_sweep(
|
|||||||
report.duration_ms = int((time.monotonic() - started) * 1000)
|
report.duration_ms = int((time.monotonic() - started) * 1000)
|
||||||
return report
|
return report
|
||||||
|
|
||||||
|
def pruefen(
|
||||||
|
device: dict[str, Any],
|
||||||
|
) -> tuple[dict[str, Any], PingResult, vnc_dienst.Zugang | None]:
|
||||||
|
ergebnis = ping(str(device["ip"]))
|
||||||
|
if not vnc_port or not ergebnis.ok:
|
||||||
|
# Kein Ergebnis heisst "nicht gefragt" -- und laesst den zuletzt
|
||||||
|
# bekannten Stand stehen. Ein Geraet, das gerade neu startet, soll
|
||||||
|
# seinen Knopf nicht verlieren.
|
||||||
|
return device, ergebnis, None
|
||||||
|
try:
|
||||||
|
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(lambda device: (device, ping(str(device["ip"]))), devices))
|
results = list(pool.map(pruefen, devices))
|
||||||
|
|
||||||
restart_candidates: list[dict[str, Any]] = []
|
restart_candidates: list[dict[str, Any]] = []
|
||||||
with database.session() as conn:
|
with database.session() as conn:
|
||||||
with database.transaction(conn):
|
with database.transaction(conn):
|
||||||
for device, result in results:
|
for device, result, vnc_offen in results:
|
||||||
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:
|
||||||
|
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:
|
||||||
@@ -565,9 +719,21 @@ 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
|
||||||
|
|||||||
@@ -30,14 +30,37 @@ from .ssh import SshError, open_transport
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
#: Steuerzeichen eines Terminals -- ANSI-Sequenzen, Glocke, Einzelbefehle.
|
||||||
|
#:
|
||||||
|
#: Ein ArubaOS-Switch an einem Pseudoterminal zeichnet seine Zeile neu und
|
||||||
|
#: haengt dabei Sequenzen wie ``ESC [ K`` ("bis Zeilenende loeschen") an. Im
|
||||||
|
#: Terminal sieht man davon **nichts** -- und genau das war der Fehler, der
|
||||||
|
#: Konsole und PoE-Neustart lahmgelegt hat: der Prompt stand da, aber hinter
|
||||||
|
#: ihm standen drei unsichtbare Bytes, und das verankerte Muster traf nicht
|
||||||
|
#: mehr. Im Protokoll las man "kein Prompt", waehrend der Prompt im Text
|
||||||
|
#: stand. Deshalb kommen die Steuerzeichen weg, bevor irgendetwas gesucht
|
||||||
|
#: wird -- das macht zugleich die Ausgabe in der Konsole lesbar.
|
||||||
|
ANSI_RE = re.compile(rb"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b[@-Z\\-_]|[\x07\x00]")
|
||||||
|
|
||||||
#: Erkennt Operator- (``>``) und privilegierten Modus (``#``) sowie den
|
#: Erkennt Operator- (``>``) und privilegierten Modus (``#``) sowie den
|
||||||
#: Konfigurationsmodus (``switch(config)#``).
|
#: Konfigurationsmodus (``switch(config)#``).
|
||||||
PROMPT_RE = re.compile(rb"[\r\n][^\r\n]{0,80}?[>#]\s?$")
|
#:
|
||||||
|
#: Zwei Lockerungen gegenueber der ersten Fassung, beide aus dem Betrieb:
|
||||||
|
#: der Prompt darf am Anfang des Puffers stehen (nicht nur nach einem
|
||||||
|
#: Zeilenumbruch -- so kommt er nach einem geleerten Puffer), und hinter ihm
|
||||||
|
#: darf beliebiger Leerraum stehen (nicht genau eines).
|
||||||
|
PROMPT_RE = re.compile(rb"(?:^|[\r\n])[^\r\n]{0,120}?[>#]\s*$")
|
||||||
PAGER_RE = re.compile(rb"(--\s?MORE\s?--|Press any key to continue)", re.IGNORECASE)
|
PAGER_RE = re.compile(rb"(--\s?MORE\s?--|Press any key to continue)", re.IGNORECASE)
|
||||||
ERROR_RE = re.compile(
|
ERROR_RE = re.compile(
|
||||||
r"(invalid input|unknown command|incomplete|ambiguous|not supported|% *error)", re.IGNORECASE
|
r"(invalid input|unknown command|incomplete|ambiguous|not supported|% *error)", re.IGNORECASE
|
||||||
)
|
)
|
||||||
|
|
||||||
|
#: Der Weg nach draussen -- zweimal "exit" fuehrt aus jedem Modus in den
|
||||||
|
#: privilegierten zurueck, das dritte beendet die Sitzung. Genau so macht es
|
||||||
|
#: das bewaehrte Skript der Vorgaengerfassung.
|
||||||
|
ABMELDUNG = ("exit", "exit", "exit")
|
||||||
|
ABMELDE_PAUSE = 0.15
|
||||||
|
|
||||||
READ_CHUNK = 65536
|
READ_CHUNK = 65536
|
||||||
DEFAULT_COMMAND_TIMEOUT = 20
|
DEFAULT_COMMAND_TIMEOUT = 20
|
||||||
POE_SETTLE_SECONDS = 2
|
POE_SETTLE_SECONDS = 2
|
||||||
@@ -104,7 +127,42 @@ class SwitchCli:
|
|||||||
self.close()
|
self.close()
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
for closer in (self._channel, self._transport):
|
"""Meldet sich ab und legt dann erst auf.
|
||||||
|
|
||||||
|
Der entscheidende Teil, und der einzige, den das ``expect``-Skript der
|
||||||
|
Fassung 1.2.8 hatte und diese Fassung nicht: ``exit`` bis heraus aus
|
||||||
|
dem Konfigurationsmodus, dann ``exit``, und auf die Frage
|
||||||
|
"Do you want to log out (y/n)?" ein ``y``.
|
||||||
|
|
||||||
|
Warum das mehr ist als Hoeflichkeit: ArubaOS erlaubt nur eine Handvoll
|
||||||
|
gleichzeitiger SSH-Sitzungen (``show ip ssh``, Vorgabe fuenf) und
|
||||||
|
raeumt eine weggebrochene Verbindung **nicht sofort** ab. Wer die
|
||||||
|
Verbindung nur fallen laesst, belegt den Platz weiter. Nach ein paar
|
||||||
|
Vorgaengen -- ein Ueberwachungsdurchlauf mit mehreren Geraeten am
|
||||||
|
selben Switch reicht -- sind alle Plaetze belegt. Danach kommt eine
|
||||||
|
neue Sitzung noch durch Anmeldung und Authentisierung, bekommt aber
|
||||||
|
**keine Shell**: kein Prompt, keine Antwort, nichts. Im Betrieb sieht
|
||||||
|
das aus wie ein stummer Switch, und der Portzyklus passiert nicht.
|
||||||
|
|
||||||
|
Fehler werden hier geschluckt: eine Abmeldung, die nicht klappt, darf
|
||||||
|
das Ergebnis des Vorgangs nicht umwerfen -- aufgelegt wird ohnehin.
|
||||||
|
"""
|
||||||
|
kanal = self._channel
|
||||||
|
if kanal is not None:
|
||||||
|
try:
|
||||||
|
for zeile in ABMELDUNG:
|
||||||
|
kanal.send(zeile + "\r\n")
|
||||||
|
# Kurz warten, damit der Switch die Zeile verarbeitet --
|
||||||
|
# auf die Antwort zu warten wuerde beim Abmelden nur
|
||||||
|
# Zeitlimits sammeln.
|
||||||
|
time.sleep(ABMELDE_PAUSE)
|
||||||
|
# "Do you want to log out (y/n)?"
|
||||||
|
kanal.send("y\r\n")
|
||||||
|
time.sleep(ABMELDE_PAUSE)
|
||||||
|
except Exception: # noqa: BLE001 - die Verbindung ist ohnehin am Ende
|
||||||
|
logger.debug("Abmeldung von %s nicht mehr moeglich", self.host)
|
||||||
|
|
||||||
|
for closer in (kanal, self._transport):
|
||||||
try:
|
try:
|
||||||
if closer is not None:
|
if closer is not None:
|
||||||
closer.close()
|
closer.close()
|
||||||
@@ -116,28 +174,62 @@ class SwitchCli:
|
|||||||
# ------------------------------------------------------------------ #
|
# ------------------------------------------------------------------ #
|
||||||
|
|
||||||
def _read_until_prompt(self, *, timeout: int, initial: bool = False) -> str:
|
def _read_until_prompt(self, *, timeout: int, initial: bool = False) -> str:
|
||||||
|
"""Liest bis zum Prompt. Ohne Prompt ist es ein Fehler, keine Antwort.
|
||||||
|
|
||||||
|
Bis 2.8.2 lief diese Schleife nach dem Zeitlimit einfach aus und gab
|
||||||
|
zurueck, was bis dahin gekommen war -- oft nichts. Der Aufrufer sah
|
||||||
|
eine leere Antwort und machte weiter: ``configure terminal`` ins
|
||||||
|
Nichts, ``no power-over-ethernet`` ins Nichts, und weil die
|
||||||
|
Zustandsabfrage dann "unknown" ergab und "unknown" als
|
||||||
|
"nicht widersprochen" durchging, meldete TESM am Ende **Erfolg** --
|
||||||
|
waehrend am Switch nichts geschaltet wurde. Genau das war im Betrieb
|
||||||
|
zu sehen: der Client blieb durchgehend erreichbar, das Protokoll sagte
|
||||||
|
"durchgefuehrt".
|
||||||
|
|
||||||
|
Ein Kommando ohne Prompt ist ein Kommando ohne Antwort. Das ist ein
|
||||||
|
Fehlschlag und wird als solcher gemeldet -- mit dem, was zuletzt
|
||||||
|
hereinkam, denn daran erkennt man, wo die Sitzung steht.
|
||||||
|
"""
|
||||||
assert self._channel is not None
|
assert self._channel is not None
|
||||||
deadline = time.monotonic() + timeout
|
deadline = time.monotonic() + timeout
|
||||||
collected = b""
|
# Zwei Puffer: das Rohe kommt herein, gesucht wird im Sichtbaren.
|
||||||
|
# Ohne diese Trennung entscheiden unsichtbare Steuerzeichen darueber,
|
||||||
|
# ob ein Prompt erkannt wird.
|
||||||
|
sichtbar = b""
|
||||||
|
prompt_gesehen = False
|
||||||
while time.monotonic() < deadline:
|
while time.monotonic() < deadline:
|
||||||
if self._channel.recv_ready():
|
if self._channel.recv_ready():
|
||||||
chunk = self._channel.recv(READ_CHUNK)
|
chunk = self._channel.recv(READ_CHUNK)
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
collected += chunk
|
sichtbar = ANSI_RE.sub(b"", sichtbar + chunk)
|
||||||
if PAGER_RE.search(collected[-200:]):
|
if PAGER_RE.search(sichtbar[-300:]):
|
||||||
self._channel.send(" ")
|
self._channel.send(" ")
|
||||||
collected = PAGER_RE.sub(b"", collected)
|
sichtbar = PAGER_RE.sub(b"", sichtbar)
|
||||||
continue
|
continue
|
||||||
if PROMPT_RE.search(collected[-200:]):
|
if PROMPT_RE.search(sichtbar[-300:]):
|
||||||
|
prompt_gesehen = True
|
||||||
break
|
break
|
||||||
# Der Login-Banner mancher Geraete verlangt eine Bestaetigung.
|
# Der Login-Banner mancher Geraete verlangt eine Bestaetigung.
|
||||||
if initial and collected.rstrip().endswith(b"continue"):
|
if initial and sichtbar.rstrip().endswith(b"continue"):
|
||||||
self._channel.send("\r\n")
|
self._channel.send("\r\n")
|
||||||
else:
|
else:
|
||||||
time.sleep(0.05)
|
time.sleep(0.05)
|
||||||
text = collected.decode("utf-8", errors="replace")
|
text = sichtbar.decode("utf-8", errors="replace")
|
||||||
self.transcript.append(text)
|
self.transcript.append(text)
|
||||||
|
if not prompt_gesehen:
|
||||||
|
letzte = " ".join(text.split())[-200:]
|
||||||
|
if initial:
|
||||||
|
raise SshError(
|
||||||
|
f"{self.host} hat sich nach der Anmeldung nicht gemeldet "
|
||||||
|
f"(kein Prompt innerhalb von {timeout}s)."
|
||||||
|
+ (f" Zuletzt kam: {letzte}" if letzte else " Es kam nichts.")
|
||||||
|
)
|
||||||
|
raise SshError(
|
||||||
|
f"{self.host} hat auf das Kommando nicht geantwortet "
|
||||||
|
f"(kein Prompt innerhalb von {timeout}s)."
|
||||||
|
+ (f" Zuletzt kam: {letzte}" if letzte else " Es kam nichts.")
|
||||||
|
)
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def send(
|
def send(
|
||||||
@@ -212,6 +304,17 @@ class SwitchCli:
|
|||||||
)
|
)
|
||||||
result.transcript = self.full_transcript
|
result.transcript = self.full_transcript
|
||||||
return result
|
return result
|
||||||
|
if before == "enabled" and off_state == "unknown":
|
||||||
|
# Vorher lesbar, nachher nicht: dann ist nicht die Firmware
|
||||||
|
# unbekannt, sondern die Sitzung aus dem Tritt. Das als Erfolg
|
||||||
|
# zu zaehlen war der Fehler, der einen Neustart meldete, den
|
||||||
|
# es nicht gab.
|
||||||
|
result.error = (
|
||||||
|
"Der Zustand des Ports ließ sich nach dem Abschalten nicht mehr "
|
||||||
|
"lesen – vorher ging es. Der Portzyklus ist damit nicht belegt."
|
||||||
|
)
|
||||||
|
result.transcript = self.full_transcript
|
||||||
|
return result
|
||||||
result.add(f"Zustand im ausgeschalteten Zustand: {off_state}")
|
result.add(f"Zustand im ausgeschalteten Zustand: {off_state}")
|
||||||
|
|
||||||
self.set_poe(port, enabled=True)
|
self.set_poe(port, enabled=True)
|
||||||
@@ -224,6 +327,17 @@ class SwitchCli:
|
|||||||
result.transcript = self.full_transcript
|
result.transcript = self.full_transcript
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
if before == "unknown" and off_state == "unknown" and after == "unknown":
|
||||||
|
# Kein einziger Zustand lesbar: die Kommandos hat der Switch
|
||||||
|
# zwar mit einem Prompt bestaetigt (sonst waere die Sitzung
|
||||||
|
# oben abgebrochen), aber belegt ist der Zyklus nicht. Das
|
||||||
|
# gehoert in die Meldung -- "durchgefuehrt" waere zu viel
|
||||||
|
# gesagt.
|
||||||
|
result.add(
|
||||||
|
"Der Zustand war nicht lesbar – die Kommandos hat der Switch "
|
||||||
|
"angenommen, bestätigt ist der Zyklus damit nicht."
|
||||||
|
)
|
||||||
|
|
||||||
result.ok = True
|
result.ok = True
|
||||||
except SshError as exc:
|
except SshError as exc:
|
||||||
result.error = str(exc)
|
result.error = str(exc)
|
||||||
|
|||||||
@@ -0,0 +1,414 @@
|
|||||||
|
"""Bildschirmzugriff per VNC: Ziel bestimmen, Erreichbarkeit pruefen, Datei bauen.
|
||||||
|
|
||||||
|
Auf den HAPs (Raspberry Pis) laeuft ein VNC-Server. TESM kennt von jedem
|
||||||
|
Client Namen und Adresse -- daraus laesst sich die Verbindung erzeugen, statt
|
||||||
|
sie in jedem Viewer von Hand zu pflegen.
|
||||||
|
|
||||||
|
Zwei Wege, beide aus denselben zwei Angaben:
|
||||||
|
|
||||||
|
* eine **Verbindungsdatei** (``.vnc``), wie der RealVNC Viewer sie schreibt.
|
||||||
|
Sie enthaelt genau drei Zeilen -- ``ConnMethod``, ``FriendlyName``, ``Host``
|
||||||
|
-- und **nichts weiter**. Jede zusaetzliche Angabe ist eine Gelegenheit,
|
||||||
|
etwas zu verlangen, das hier niemand verlangen soll: keine
|
||||||
|
Verschluesselungsvorgabe, keine Identitaetspruefung, kein Benutzername, kein
|
||||||
|
Passwortfeld. Was am Ende gefragt wird, entscheidet ohnehin der VNC-Server
|
||||||
|
auf dem Geraet; die Datei fragt von sich aus nichts.
|
||||||
|
* eine **Adresse** (``vnc://host``) fuer den Fall, dass der Viewer als
|
||||||
|
Handler registriert ist -- ein Klick statt Herunterladen und Oeffnen.
|
||||||
|
|
||||||
|
Der Zugang erscheint nur dort, wo wirklich ein VNC-Server antwortet
|
||||||
|
(:func:`erreichbar`). Das ist der Unterschied zu einem Schalter am Geraet:
|
||||||
|
niemand muss ihn pflegen, und er zeigt nie ins Leere.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Mapping
|
||||||
|
|
||||||
|
from .monitor import tcp_reachable
|
||||||
|
|
||||||
|
#: Vorgabeport von VNC (Anzeige :0). Ueberschreibbar in den Einstellungen.
|
||||||
|
STANDARD_PORT = 5900
|
||||||
|
|
||||||
|
#: So kurz, dass die Detailseite es nicht merkt, und so lang, dass ein Geraet
|
||||||
|
#: im selben Netz antworten kann. Gefragt wird ohnehin nur, wenn der Client
|
||||||
|
#: gerade als erreichbar gilt.
|
||||||
|
ZEITLIMIT = 0.6
|
||||||
|
|
||||||
|
#: Ein Name, den ein Rechner aufloesen kann: Buchstaben, Ziffern, Bindestrich,
|
||||||
|
#: Punkt als Trenner. "HAP 02" oder "Kamera Eingang" ist keiner -- dann bleibt
|
||||||
|
#: die IP-Adresse, die im Datensatz ohnehin steht.
|
||||||
|
HOSTNAME_RE = re.compile(r"^(?!-)[A-Za-z0-9-]{1,63}(?<!-)(\.(?!-)[A-Za-z0-9-]{1,63}(?<!-))*$")
|
||||||
|
|
||||||
|
#: Zeilenende der Datei: CRLF. Der Viewer liest beides, aber die Datei landet
|
||||||
|
#: auf einem Windows-Arbeitsplatz -- und dort soll sie in jedem Editor
|
||||||
|
#: vernuenftig aussehen, wenn jemand nachsieht, was drinsteht.
|
||||||
|
ZEILE = "\r\n"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True, slots=True)
|
||||||
|
class Zugang:
|
||||||
|
"""Was die Detailseite ueber den Bildschirmzugriff eines Clients weiss."""
|
||||||
|
|
||||||
|
ziel: str
|
||||||
|
port: int
|
||||||
|
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
|
||||||
|
def adresse(self) -> str:
|
||||||
|
"""``vnc://host`` -- den Standardport schreibt niemand dazu."""
|
||||||
|
if self.port == STANDARD_PORT:
|
||||||
|
return f"vnc://{self.ziel}"
|
||||||
|
return f"vnc://{self.ziel}:{self.port}"
|
||||||
|
|
||||||
|
|
||||||
|
#: Aufloesungen, die wir schon einmal versucht haben: ``name -> (Zeit, Ergebnis)``.
|
||||||
|
#:
|
||||||
|
#: Nicht wegen der Geschwindigkeit -- ein arbeitender Resolver antwortet in
|
||||||
|
#: Bruchteilen einer Millisekunde --, sondern wegen des Gegenteils: faellt der
|
||||||
|
#: Nameserver aus, laeuft jede Abfrage in sein eigenes Zeitlimit. Ohne diesen
|
||||||
|
#: Puffer haette das der Seitenaufbau ausgebadet, und zwar bei jedem Aufruf.
|
||||||
|
_AUFLOESUNG: dict[str, tuple[float, bool]] = {}
|
||||||
|
|
||||||
|
#: Fuenf Minuten. Lang genug, damit ein kaputter Resolver nicht jede Seite
|
||||||
|
#: aufhaelt; kurz genug, dass ein neuer DNS-Eintrag nicht bis zum Neustart
|
||||||
|
#: wartet.
|
||||||
|
AUFLOESUNG_GILT = 300.0
|
||||||
|
|
||||||
|
|
||||||
|
def aufloesbar(name: str) -> bool:
|
||||||
|
"""Kennt der Resolver diesen Namen?"""
|
||||||
|
jetzt = time.monotonic()
|
||||||
|
gemerkt = _AUFLOESUNG.get(name)
|
||||||
|
if gemerkt is not None and jetzt - gemerkt[0] < AUFLOESUNG_GILT:
|
||||||
|
return gemerkt[1]
|
||||||
|
try:
|
||||||
|
socket.getaddrinfo(name, None)
|
||||||
|
ergebnis = True
|
||||||
|
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
|
||||||
|
_AUFLOESUNG[name] = (jetzt, ergebnis)
|
||||||
|
return ergebnis
|
||||||
|
|
||||||
|
|
||||||
|
def ziel(device: Mapping[str, Any]) -> str:
|
||||||
|
"""Der Name des Clients, sonst seine IP-Adresse.
|
||||||
|
|
||||||
|
Der Name ist die bessere Wahl, wo er gilt: er ueberlebt einen
|
||||||
|
Adresswechsel, und genau so stehen die gewachsenen Verbindungen im Viewer
|
||||||
|
(``Host=hap02``). Zwei Dinge muessen dafuer stimmen -- er muss als Hostname
|
||||||
|
**taugen** ("Kamera Eingang" tut es nicht) und er muss sich **aufloesen**
|
||||||
|
lassen. Sonst zeigte die Datei auf einen Namen, den der Arbeitsplatz nicht
|
||||||
|
kennt, und niemand saehe, woran es liegt; dann ist die Adresse richtig, die
|
||||||
|
ohnehin im Datensatz steht.
|
||||||
|
|
||||||
|
Loest der Name auf eine **andere** Adresse auf als die gespeicherte, gewinnt
|
||||||
|
trotzdem der Name: dann ist der Eintrag im Bestand veraltet, nicht das DNS.
|
||||||
|
"""
|
||||||
|
name = str(device.get("name") or "").strip()
|
||||||
|
if name and HOSTNAME_RE.match(name) and aufloesbar(name):
|
||||||
|
return name.lower()
|
||||||
|
return str(device.get("ip") or "").strip()
|
||||||
|
|
||||||
|
|
||||||
|
def erreichbar(host: str, port: int = STANDARD_PORT) -> bool:
|
||||||
|
"""Nimmt auf diesem Host ein VNC-Server Verbindungen an?"""
|
||||||
|
if not host:
|
||||||
|
return False
|
||||||
|
return tcp_reachable(host, port, timeout=ZEITLIMIT)
|
||||||
|
|
||||||
|
|
||||||
|
#: Was der Browser-Client (noVNC 1.6) aushandeln kann. Aus dessen
|
||||||
|
#: ``_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)
|
||||||
|
return Zugang(ziel=host, port=port, erreichbar=erreichbar(host, port))
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
``ConnMethod=tcp`` ist die direkte Verbindung (nicht ueber den Cloud-Dienst
|
||||||
|
von RealVNC), ``FriendlyName`` der Name, den der Viewer anzeigt, ``Host``
|
||||||
|
das Ziel. Ein abweichender Port gehoert in dieser Datei an den Host, mit
|
||||||
|
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)
|
||||||
|
if port != STANDARD_PORT:
|
||||||
|
host = f"{host}:{port}"
|
||||||
|
zeilen = [
|
||||||
|
"ConnMethod=tcp",
|
||||||
|
f"FriendlyName={str(device.get('name') or host).strip()}",
|
||||||
|
f"Host={host}",
|
||||||
|
]
|
||||||
|
if warnung_aus:
|
||||||
|
zeilen.append("WarnUnencrypted=False")
|
||||||
|
return ZEILE.join(zeilen) + ZEILE
|
||||||
|
|
||||||
|
|
||||||
|
def dateiname(device: Mapping[str, Any]) -> str:
|
||||||
|
"""``HAP02.vnc`` -- aus dem Namen, auf Unbedenkliches beschraenkt.
|
||||||
|
|
||||||
|
Auch der Punkt faellt weg, nicht nur der Schraegstrich: ein Geraetename ist
|
||||||
|
Freitext, und ein Dateiname, in dem ".." vorkommen kann, ist eine Frage,
|
||||||
|
die man sich gar nicht erst stellen muss.
|
||||||
|
"""
|
||||||
|
name = re.sub(r"[^A-Za-z0-9_-]+", "-", str(device.get("name") or "client")).strip("-")
|
||||||
|
return f"{name or 'client'}.vnc"
|
||||||
|
|
||||||
|
|
||||||
|
def port_aus_einstellungen(extension: Any) -> int:
|
||||||
|
"""Der eingestellte Port -- oder 0, wenn der Zugang abgeschaltet ist.
|
||||||
|
|
||||||
|
Nimmt die Erweiterung statt der Verbindung, weil der Ueberwachungsprozess
|
||||||
|
ausserhalb eines Anfragekontextes laeuft und seine eigene Sitzung oeffnet.
|
||||||
|
"""
|
||||||
|
with extension.database.session() as conn:
|
||||||
|
if not extension.settings.get(conn, "vnc_enabled"):
|
||||||
|
return 0
|
||||||
|
return int(extension.settings.get(conn, "vnc_port") or STANDARD_PORT)
|
||||||
@@ -45,6 +45,49 @@ TESM_SETTINGS: tuple[SettingSpec, ...] = (
|
|||||||
"IP-Adressen, keine Switch-Zuordnung, keine Verwaltungsfunktionen.",
|
"IP-Adressen, keine Switch-Zuordnung, keine Verwaltungsfunktionen.",
|
||||||
section="monitoring",
|
section="monitoring",
|
||||||
),
|
),
|
||||||
|
# -- Bildschirmzugriff (VNC) -------------------------------------------- #
|
||||||
|
SettingSpec(
|
||||||
|
"vnc_enabled",
|
||||||
|
True,
|
||||||
|
"bool",
|
||||||
|
"Bildschirmzugriff (VNC) anbieten",
|
||||||
|
hint="Der Überwachungsdurchlauf fragt bei jedem erreichbaren Client kurz, ob "
|
||||||
|
"auf diesem Port ein VNC-Server antwortet; nur dort erscheint der Knopf. "
|
||||||
|
"Sehen darf ihn trotzdem nur, wer das Recht dafür hat.",
|
||||||
|
section="monitoring",
|
||||||
|
),
|
||||||
|
SettingSpec(
|
||||||
|
"vnc_port",
|
||||||
|
5900,
|
||||||
|
"int",
|
||||||
|
"VNC-Port",
|
||||||
|
hint="5900 ist Anzeige :0 – die Vorgabe eines Raspberry Pi.",
|
||||||
|
section="monitoring",
|
||||||
|
minimum=1,
|
||||||
|
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(
|
||||||
|
|||||||
@@ -84,6 +84,46 @@
|
|||||||
<a class="btn btn--sm btn--ghost" href="{{ url_for('devices.detail', device_id=tile.id) }}">
|
<a class="btn btn--sm btn--ghost" href="{{ url_for('devices.detail', device_id=tile.id) }}">
|
||||||
Details
|
Details
|
||||||
</a>
|
</a>
|
||||||
|
{% if current_user.has('devices.write') and tile.state == 'disabled' %}
|
||||||
|
<form method="post" action="{{ url_for('devices.toggle_active', device_id=tile.id) }}"
|
||||||
|
data-no-guard>
|
||||||
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
<input type="hidden" name="active" value="1">
|
||||||
|
<button class="btn btn--sm" type="submit">Aktivieren</button>
|
||||||
|
</form>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{# Die beiden Eingriffe stehen unten und immer an derselben
|
||||||
|
Stelle: links der Bildschirm, rechts der Neustart. Wer
|
||||||
|
eine Reihe Kacheln abarbeitet, trifft denselben Knopf,
|
||||||
|
ohne hinzusehen -- und verwechselt ihn nicht mit dem
|
||||||
|
anderen. #}
|
||||||
|
{% if tile.vnc or tile.restart_methods %}
|
||||||
|
<div class="tile__actions">
|
||||||
|
{% if tile.vnc %}
|
||||||
|
{# Die Sitzung laeuft in TESM selbst: ein Klick, kein
|
||||||
|
Viewer auf dem Arbeitsplatz, keine Datei.
|
||||||
|
|
||||||
|
Mit frischer Passwortbestätigung öffnet sie sich hier
|
||||||
|
im Fenster -- man bleibt bei der Übersicht und sieht
|
||||||
|
die anderen Geräte weiter. Ohne sie führt der Knopf
|
||||||
|
auf die Geräteseite, wo danach gefragt wird. #}
|
||||||
|
{% 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 %}
|
||||||
|
<span class="tile__spacer" aria-hidden="true"></span>
|
||||||
|
{% endif %}
|
||||||
{% if tile.restart_methods %}
|
{% if tile.restart_methods %}
|
||||||
{% set method = tile.restart_methods[0] %}
|
{% set method = tile.restart_methods[0] %}
|
||||||
<form method="post" data-no-guard
|
<form method="post" data-no-guard
|
||||||
@@ -99,16 +139,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if current_user.has('devices.write') and tile.state == 'disabled' %}
|
|
||||||
<form method="post" action="{{ url_for('devices.toggle_active', device_id=tile.id) }}"
|
|
||||||
data-no-guard>
|
|
||||||
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
|
||||||
<input type="hidden" name="active" value="1">
|
|
||||||
<button class="btn btn--sm" type="submit">Aktivieren</button>
|
|
||||||
</form>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</article>
|
</article>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -16,6 +16,12 @@
|
|||||||
{{ icon('terminal', size=15) }} Konsole
|
{{ icon('terminal', size=15) }} Konsole
|
||||||
</a>
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if vnc and vnc.erreichbar %}
|
||||||
|
<a class="btn btn--sm" href="{{ url_for('screen.screen', device_id=device.id) }}"
|
||||||
|
title="Bildschirm im Browser öffnen">
|
||||||
|
{{ icon('monitor', size=15) }} VNC
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
{% if restart_methods %}
|
{% if restart_methods %}
|
||||||
{% for method in restart_methods %}
|
{% for method in restart_methods %}
|
||||||
<form method="post" style="display:inline"
|
<form method="post" style="display:inline"
|
||||||
@@ -225,6 +231,29 @@
|
|||||||
<dt>Kategorie</dt><dd>{{ device.category or '–' | safe }}</dd>
|
<dt>Kategorie</dt><dd>{{ device.category or '–' | safe }}</dd>
|
||||||
<dt>Standort</dt><dd>{{ device.location or '–' | safe }}</dd>
|
<dt>Standort</dt><dd>{{ device.location or '–' | safe }}</dd>
|
||||||
<dt>Zugangsdaten</dt><dd>{{ device.credential_name or 'keine' }}</dd>
|
<dt>Zugangsdaten</dt><dd>{{ device.credential_name or 'keine' }}</dd>
|
||||||
|
{% if vnc %}
|
||||||
|
<dt>Bildschirm (VNC)</dt>
|
||||||
|
<dd>
|
||||||
|
{% if vnc.erreichbar %}
|
||||||
|
<span class="mono">{{ vnc.ziel }}:{{ vnc.port }}</span>
|
||||||
|
<div class="fs-xs faint">
|
||||||
|
<a href="{{ url_for('screen.screen', device_id=device.id) }}">
|
||||||
|
Im Browser öffnen</a> – oder die
|
||||||
|
<a href="{{ url_for('devices.vnc_connection', device_id=device.id) }}">
|
||||||
|
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>
|
||||||
|
{% else %}
|
||||||
|
<span class="faint">
|
||||||
|
kein VNC-Server auf {{ vnc.ziel }}:{{ vnc.port }}
|
||||||
|
{%- if device.state != 'online' %} (Client antwortet gerade nicht){% endif %}
|
||||||
|
</span>
|
||||||
|
{% endif %}
|
||||||
|
</dd>
|
||||||
|
{% endif %}
|
||||||
<dt>Automatischer Neustart</dt><dd>{{ device.auto_restart | yes_no }}</dd>
|
<dt>Automatischer Neustart</dt><dd>{{ device.auto_restart | yes_no }}</dd>
|
||||||
<dt>Angelegt</dt><dd>{{ device.created_at | dt }}</dd>
|
<dt>Angelegt</dt><dd>{{ device.created_at | dt }}</dd>
|
||||||
<dt>Geändert</dt>
|
<dt>Geändert</dt>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -60,9 +60,18 @@
|
|||||||
<dd>
|
<dd>
|
||||||
{% if state.modules %}
|
{% if state.modules %}
|
||||||
{% for module in modules %}
|
{% for module in modules %}
|
||||||
{% if module in state.modules %}{{ pill(module_labels[module], 'online') }}
|
{% if module in state.modules and module in state.modules_off %}
|
||||||
|
{{ pill(module_labels[module] ~ ' (abgeschaltet)', 'warning') }}
|
||||||
|
{% elif module in state.modules %}{{ pill(module_labels[module], 'online') }}
|
||||||
{% else %}{{ pill(module_labels[module], '') }}{% endif %}
|
{% else %}{{ pill(module_labels[module], '') }}{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
{% if state.modules_off %}
|
||||||
|
<div class="fs-xs faint">
|
||||||
|
Abgeschaltet wurde in
|
||||||
|
<a href="{{ url_for('settings.system') }}">Verwaltung → System</a>;
|
||||||
|
die Lizenz ist davon unberührt.
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% else %}<span class="faint">keine Zusatzmodule</span>{% endif %}
|
{% else %}<span class="faint">keine Zusatzmodule</span>{% endif %}
|
||||||
</dd>
|
</dd>
|
||||||
<dt>Lizenzserver</dt>
|
<dt>Lizenzserver</dt>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -51,6 +51,34 @@
|
|||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
{% if module_rows %}
|
||||||
|
<section class="card">
|
||||||
|
<div class="card__header">
|
||||||
|
<div class="flex-1">
|
||||||
|
<div class="card__title">{{ icon('puzzle', size=17) }} Module</div>
|
||||||
|
<div class="card__sub">
|
||||||
|
Die Lizenz sagt, was erlaubt ist – hier steht, was dieses Haus benutzt.
|
||||||
|
Ein abgeschaltetes Modul verschwindet aus der Navigation, und seine Seiten
|
||||||
|
antworten wie ohne Lizenz. Bestehende Daten bleiben unverändert stehen und
|
||||||
|
sind nach dem Wiedereinschalten wieder da.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="post" action="{{ url_for('settings.modules_save') }}">
|
||||||
|
<input type="hidden" name="{{ csrf_field_name }}" value="{{ csrf_token() }}">
|
||||||
|
<div class="card__body stack">
|
||||||
|
{% for modul in module_rows %}
|
||||||
|
{{ checkbox('module_' ~ modul.key, modul.label, checked=modul.enabled,
|
||||||
|
hint_text=('In der Lizenz enthalten.' if modul.licensed
|
||||||
|
else 'Nicht lizenziert – bleibt aus, bis die Lizenz es
|
||||||
|
freischaltet. Der Haken gilt dann sofort.')) }}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
<div class="card__footer"><button class="btn btn--primary" type="submit">Speichern</button></div>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<section class="card">
|
<section class="card">
|
||||||
<div class="card__header">
|
<div class="card__header">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@@ -78,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">
|
||||||
@@ -189,7 +250,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{% if current_user.is_admin %}
|
{% if current_user.is_admin %}
|
||||||
{{ nav_order_card(nav_items, nav_order, nav_child_order,
|
{{ nav_order_card(nav_baum, nav_order, nav_child_order,
|
||||||
url_for('admin.nav_order'), csrf_field_name, csrf_token()) }}
|
url_for('admin.nav_order'), csrf_field_name, csrf_token()) }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ from types import FrameType
|
|||||||
from tesm_core.extension import core
|
from tesm_core.extension import core
|
||||||
|
|
||||||
from . import create_app
|
from . import create_app
|
||||||
from .services import inventory, monitor
|
from .services import inventory, monitor, vnc
|
||||||
|
|
||||||
logger = logging.getLogger("tesm.monitor")
|
logger = logging.getLogger("tesm.monitor")
|
||||||
|
|
||||||
@@ -82,6 +82,7 @@ class MonitorDaemon:
|
|||||||
keystore=self.extension.keystore,
|
keystore=self.extension.keystore,
|
||||||
license_allows_auto_restart=self._auto_restart_allowed(),
|
license_allows_auto_restart=self._auto_restart_allowed(),
|
||||||
failures_before_restart=self._failures_before_restart(),
|
failures_before_restart=self._failures_before_restart(),
|
||||||
|
vnc_port=vnc.port_aus_einstellungen(self.extension),
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
"Durchlauf: %s geprüft, %s online, %s offline, %s Neustarts (%s erfolgreich), "
|
"Durchlauf: %s geprüft, %s online, %s offline, %s Neustarts (%s erfolgreich), "
|
||||||
@@ -112,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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "tesm-core"
|
name = "tesm-core"
|
||||||
version = "2.8.1"
|
version = "2.8.16"
|
||||||
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 = [
|
||||||
|
|||||||
@@ -102,6 +102,14 @@ CORE_SETTINGS: tuple[SettingSpec, ...] = (
|
|||||||
),
|
),
|
||||||
# -- Webserver/TLS (Host-Ebene, in beiden Anwendungen gleich) ----------- #
|
# -- Webserver/TLS (Host-Ebene, in beiden Anwendungen gleich) ----------- #
|
||||||
# Lizenzserver: eigenes Zertifikat statt abgeschalteter Pruefung.
|
# Lizenzserver: eigenes Zertifikat statt abgeschalteter Pruefung.
|
||||||
|
SettingSpec(
|
||||||
|
"modules_disabled",
|
||||||
|
[],
|
||||||
|
"json",
|
||||||
|
"Vor Ort abgeschaltete Module",
|
||||||
|
hint="Die Lizenz sagt, was erlaubt ist; das hier sagt, was benutzt wird.",
|
||||||
|
section="system",
|
||||||
|
),
|
||||||
SettingSpec(
|
SettingSpec(
|
||||||
"license_server_ca",
|
"license_server_ca",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import re
|
|||||||
import socket
|
import socket
|
||||||
import sqlite3
|
import sqlite3
|
||||||
import ssl
|
import ssl
|
||||||
|
import time
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
@@ -162,19 +163,82 @@ def _tls_object(config: dict[str, Any]):
|
|||||||
|
|
||||||
|
|
||||||
def _server(config: dict[str, Any]):
|
def _server(config: dict[str, Any]):
|
||||||
|
"""Das Server-Objekt -- so sparsam wie moeglich.
|
||||||
|
|
||||||
|
``get_info`` entscheidet, was ldap3 beim Verbinden **zusaetzlich** holt.
|
||||||
|
Mit ``ALL`` ist das die Beschreibung des Verzeichnisses *und sein ganzes
|
||||||
|
Schema*: bei einem Active Directory sind das Tausende Attribut- und
|
||||||
|
Klassendefinitionen, mehrere Megabyte, bei jeder Verbindung neu. Fuer eine
|
||||||
|
Anmeldung braucht davon nichts -- gelesen wird hoechstens der
|
||||||
|
``defaultNamingContext``, wenn kein Basis-DN eingetragen ist, und dafuer
|
||||||
|
genuegt ``DSA`` (nur die Serverinformation, ein Datensatz).
|
||||||
|
|
||||||
|
Liegt der Domain-Controller in einem anderen Netz, ist genau das der
|
||||||
|
Unterschied zwischen "geht sofort" und "dauert manchmal".
|
||||||
|
"""
|
||||||
from ldap3 import Server
|
from ldap3 import Server
|
||||||
|
|
||||||
security = str(config["ldap_security"])
|
security = str(config["ldap_security"])
|
||||||
|
braucht_basis = not str(config.get("ldap_base_dn") or "").strip()
|
||||||
return Server(
|
return Server(
|
||||||
str(config["ldap_host"]),
|
str(config["ldap_host"]),
|
||||||
port=int(config["ldap_port"]),
|
port=int(config["ldap_port"]),
|
||||||
use_ssl=security == "ldaps",
|
use_ssl=security == "ldaps",
|
||||||
tls=_tls_object(config) if security in {"ldaps", "starttls"} else None,
|
tls=_tls_object(config) if security in {"ldaps", "starttls"} else None,
|
||||||
get_info="ALL",
|
get_info="DSA" if braucht_basis else "NO_INFO",
|
||||||
connect_timeout=int(config["ldap_timeout"]),
|
connect_timeout=int(config["ldap_timeout"]),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#: Fehler, die nur "gerade jetzt nicht" bedeuten -- nicht "falsches Passwort".
|
||||||
|
#:
|
||||||
|
#: Die Liste stammt aus der Fassung 1.2.8, wo dieses Problem schon einmal
|
||||||
|
#: geloest war. Der Kommentar dort beschreibt es genauer, als ich es koennte:
|
||||||
|
#: ein Domain-Controller in einem anderen, langsameren Netzsegment -- die
|
||||||
|
#: allererste Verbindung dorthin ist oft spuerbar langsamer als jede folgende,
|
||||||
|
#: und der erste Login schlug mit "Benutzer/Passwort ungueltig" fehl, waehrend
|
||||||
|
#: ein sofortiger zweiter Versuch klappte.
|
||||||
|
TRANSIENTE_FEHLER: tuple[type[Exception], ...] = ()
|
||||||
|
|
||||||
|
|
||||||
|
def _transiente_fehler() -> tuple[type[Exception], ...]:
|
||||||
|
"""Die Ausnahmeklassen von ldap3, die einen Wiederholversuch verdienen."""
|
||||||
|
global TRANSIENTE_FEHLER
|
||||||
|
if TRANSIENTE_FEHLER:
|
||||||
|
return TRANSIENTE_FEHLER
|
||||||
|
from ldap3.core.exceptions import (
|
||||||
|
LDAPResponseTimeoutError,
|
||||||
|
LDAPSocketOpenError,
|
||||||
|
LDAPSocketReceiveError,
|
||||||
|
LDAPSocketSendError,
|
||||||
|
)
|
||||||
|
|
||||||
|
TRANSIENTE_FEHLER = (
|
||||||
|
LDAPSocketOpenError,
|
||||||
|
LDAPSocketReceiveError,
|
||||||
|
LDAPSocketSendError,
|
||||||
|
LDAPResponseTimeoutError,
|
||||||
|
)
|
||||||
|
return TRANSIENTE_FEHLER
|
||||||
|
|
||||||
|
|
||||||
|
def mit_wiederholung(aufbau: Any) -> Any:
|
||||||
|
"""Genau ein zweiter Versuch -- und nur bei einem Netzfehler.
|
||||||
|
|
||||||
|
Nicht bei einer abgelehnten Anmeldung: ein Tippfehler im Passwort wuerde
|
||||||
|
sonst den Sperrzaehler des Kontos zweimal statt einmal hochzaehlen. Das
|
||||||
|
ist die Regel aus 1.2.8, und sie ist richtig.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return aufbau()
|
||||||
|
except _transiente_fehler() as fehler:
|
||||||
|
logger.info(
|
||||||
|
"Verzeichnisdienst antwortete nicht (%s) -- ein zweiter Versuch",
|
||||||
|
type(fehler).__name__,
|
||||||
|
)
|
||||||
|
return aufbau()
|
||||||
|
|
||||||
|
|
||||||
def _connect(config: dict[str, Any], user: str, password: str):
|
def _connect(config: dict[str, Any], user: str, password: str):
|
||||||
"""Baut eine Verbindung auf und bindet. Wirft bei Netz-/TLS-Problemen."""
|
"""Baut eine Verbindung auf und bindet. Wirft bei Netz-/TLS-Problemen."""
|
||||||
from ldap3 import Connection
|
from ldap3 import Connection
|
||||||
@@ -192,6 +256,12 @@ def _connect(config: dict[str, Any], user: str, password: str):
|
|||||||
auto_bind=False,
|
auto_bind=False,
|
||||||
raise_exceptions=False,
|
raise_exceptions=False,
|
||||||
receive_timeout=int(config["ldap_timeout"]),
|
receive_timeout=int(config["ldap_timeout"]),
|
||||||
|
# Ein Active Directory antwortet auf eine Suche ab der Domaenenwurzel
|
||||||
|
# mit Verweisen auf andere Controller. ldap3 folgt ihnen von sich aus
|
||||||
|
# -- und baut dafuer neue Verbindungen auf, womoeglich in ein anderes
|
||||||
|
# Netz. Fuer eine Anmeldung ist das nicht nur unnoetig, sondern die
|
||||||
|
# Quelle genau der Wartezeiten, die niemand erklaeren kann.
|
||||||
|
auto_referrals=False,
|
||||||
)
|
)
|
||||||
if security == "starttls":
|
if security == "starttls":
|
||||||
if not connection.open():
|
if not connection.open():
|
||||||
@@ -253,8 +323,19 @@ def authenticate(conn: sqlite3.Connection, username: str, password: str) -> Dire
|
|||||||
if not config["ldap_verify_cert"]:
|
if not config["ldap_verify_cert"]:
|
||||||
warnings.append("Serverzertifikat des Verzeichnisdienstes wird nicht geprüft")
|
warnings.append("Serverzertifikat des Verzeichnisdienstes wird nicht geprüft")
|
||||||
|
|
||||||
|
begonnen = time.monotonic()
|
||||||
|
zeiten: list[str] = []
|
||||||
|
|
||||||
|
def gemessen(name: str, seit: float) -> float:
|
||||||
|
"""Haelt eine Stufe mit ihrer Dauer fest und gibt die neue Marke."""
|
||||||
|
jetzt = time.monotonic()
|
||||||
|
zeiten.append(f"{name} {int((jetzt - seit) * 1000)}ms")
|
||||||
|
return jetzt
|
||||||
|
|
||||||
try:
|
try:
|
||||||
service_conn, bound = _connect(config, bind_user, bind_password)
|
service_conn, bound = mit_wiederholung(
|
||||||
|
lambda: _connect(config, bind_user, bind_password)
|
||||||
|
)
|
||||||
except (PermissionError, ConnectionError) as exc:
|
except (PermissionError, ConnectionError) as exc:
|
||||||
logger.error("Verzeichnisdienst: %s", exc)
|
logger.error("Verzeichnisdienst: %s", exc)
|
||||||
return DirectoryResult("unavailable", message=str(exc), warnings=tuple(warnings))
|
return DirectoryResult("unavailable", message=str(exc), warnings=tuple(warnings))
|
||||||
@@ -271,8 +352,10 @@ def authenticate(conn: sqlite3.Connection, username: str, password: str) -> Dire
|
|||||||
warnings=tuple(warnings),
|
warnings=tuple(warnings),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
marke = gemessen("Dienstbindung", begonnen)
|
||||||
try:
|
try:
|
||||||
base = _base_dn(service_conn, config)
|
base = _base_dn(service_conn, config)
|
||||||
|
marke = gemessen("Basis-DN", marke)
|
||||||
if not base:
|
if not base:
|
||||||
return DirectoryResult(
|
return DirectoryResult(
|
||||||
"unavailable", message="Basis-DN konnte nicht ermittelt werden.", warnings=tuple(warnings)
|
"unavailable", message="Basis-DN konnte nicht ermittelt werden.", warnings=tuple(warnings)
|
||||||
@@ -288,8 +371,10 @@ def authenticate(conn: sqlite3.Connection, username: str, password: str) -> Dire
|
|||||||
attributes=list(dict.fromkeys(wanted)),
|
attributes=list(dict.fromkeys(wanted)),
|
||||||
size_limit=2,
|
size_limit=2,
|
||||||
)
|
)
|
||||||
|
marke = gemessen("Benutzersuche", marke)
|
||||||
entries = list(service_conn.entries)
|
entries = list(service_conn.entries)
|
||||||
if len(entries) != 1:
|
if len(entries) != 1:
|
||||||
|
logger.info("Verzeichnisanmeldung %s: %s", username, ", ".join(zeiten))
|
||||||
return DirectoryResult("invalid", warnings=tuple(warnings))
|
return DirectoryResult("invalid", warnings=tuple(warnings))
|
||||||
|
|
||||||
entry = entries[0]
|
entry = entries[0]
|
||||||
@@ -300,9 +385,18 @@ def authenticate(conn: sqlite3.Connection, username: str, password: str) -> Dire
|
|||||||
|
|
||||||
# Zweite, unabhaengige Verbindung mit den Benutzerdaten -- die eigentliche
|
# Zweite, unabhaengige Verbindung mit den Benutzerdaten -- die eigentliche
|
||||||
# Passwortpruefung. Niemals ueber die Dienstkonto-Verbindung rebinden.
|
# Passwortpruefung. Niemals ueber die Dienstkonto-Verbindung rebinden.
|
||||||
user_conn, user_bound = _connect(config, dn, password)
|
#
|
||||||
|
# Mit Wiederholversuch, und genau hier zeigte sich das Symptom: bei
|
||||||
|
# einem Domain-Controller in einem anderen Netz schlug der erste
|
||||||
|
# Versuch mit "Benutzer/Passwort ungueltig" fehl, der zweite gleich
|
||||||
|
# darauf klappte. Wiederholt wird nur bei einem Netzfehler -- eine
|
||||||
|
# abgelehnte Anmeldung bleibt abgelehnt, sonst zaehlt ein Tippfehler
|
||||||
|
# den Sperrzaehler des Kontos zweimal hoch.
|
||||||
|
user_conn, user_bound = mit_wiederholung(lambda: _connect(config, dn, password))
|
||||||
|
marke = gemessen("Passwortpruefung", marke)
|
||||||
try:
|
try:
|
||||||
if not user_bound:
|
if not user_bound:
|
||||||
|
logger.info("Verzeichnisanmeldung %s: %s", username, ", ".join(zeiten))
|
||||||
return DirectoryResult("invalid", warnings=tuple(warnings))
|
return DirectoryResult("invalid", warnings=tuple(warnings))
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
@@ -313,6 +407,7 @@ def authenticate(conn: sqlite3.Connection, username: str, password: str) -> Dire
|
|||||||
group_dns: tuple[str, ...] = ()
|
group_dns: tuple[str, ...] = ()
|
||||||
if config["ldap_group_sync"]:
|
if config["ldap_group_sync"]:
|
||||||
group_dns = _resolve_groups(service_conn, base, dn)
|
group_dns = _resolve_groups(service_conn, base, dn)
|
||||||
|
marke = gemessen("Gruppen", marke)
|
||||||
|
|
||||||
profile = DirectoryProfile(
|
profile = DirectoryProfile(
|
||||||
username=_attr(entry, attribute) or username,
|
username=_attr(entry, attribute) or username,
|
||||||
@@ -322,6 +417,15 @@ def authenticate(conn: sqlite3.Connection, username: str, password: str) -> Dire
|
|||||||
email=_attr(entry, email_attribute),
|
email=_attr(entry, email_attribute),
|
||||||
group_dns=group_dns,
|
group_dns=group_dns,
|
||||||
)
|
)
|
||||||
|
# Eine Zeile je Anmeldung mit den Dauern der Stufen. Aus "dauert
|
||||||
|
# manchmal laenger" wird damit eine Zahl -- und man sieht, ob es die
|
||||||
|
# Verbindung ist, die Suche oder die Gruppen.
|
||||||
|
logger.info(
|
||||||
|
"Verzeichnisanmeldung %s: %s, gesamt %dms",
|
||||||
|
username,
|
||||||
|
", ".join(zeiten),
|
||||||
|
int((time.monotonic() - begonnen) * 1000),
|
||||||
|
)
|
||||||
return DirectoryResult("ok", profile=profile, warnings=tuple(warnings))
|
return DirectoryResult("ok", profile=profile, warnings=tuple(warnings))
|
||||||
finally:
|
finally:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import urllib.request
|
|||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any, Callable
|
from typing import Any, Callable, Iterable
|
||||||
|
|
||||||
import tesm_licensing as lic
|
import tesm_licensing as lic
|
||||||
|
|
||||||
@@ -54,6 +54,21 @@ SETTING_HEARTBEAT_ERROR = "license_heartbeat_error"
|
|||||||
EXPIRY_HINT_DAYS = 90
|
EXPIRY_HINT_DAYS = 90
|
||||||
SETTING_EXPORT_GRACE = "license_export_grace"
|
SETTING_EXPORT_GRACE = "license_export_grace"
|
||||||
|
|
||||||
|
#: Module, die der Betreiber vor Ort abgeschaltet hat (Liste von Namen).
|
||||||
|
#:
|
||||||
|
#: Die Lizenz sagt, *was erlaubt ist*; sie sagt nicht, was ein Haus auch
|
||||||
|
#: benutzen will. Wer kein DHCP ueber TESM fahren will, weil ein anderer Server
|
||||||
|
#: das tut, soll den Punkt nicht wegdruecken muessen -- und vor allem soll
|
||||||
|
#: niemand versehentlich eine fremde Kea-Konfiguration ueberschreiben koennen.
|
||||||
|
#: Abschalten ist deshalb mehr als Aufraeumen der Navigation: die Sperre sitzt
|
||||||
|
#: an derselben Stelle wie die Lizenzpruefung, also vor jeder Route.
|
||||||
|
#: Bewusst **ohne** das Praefix ``license_``: Sicherungen lassen alles mit
|
||||||
|
#: diesem Praefix draussen, weil Lizenz- und Aktivierungszustand zur Maschine
|
||||||
|
#: gehoeren und nicht in ein Backup. Der Schalter ist aber keine Lizenzangabe,
|
||||||
|
#: sondern eine Entscheidung des Hauses -- die soll eine Wiederherstellung
|
||||||
|
#: mitbringen.
|
||||||
|
SETTING_MODULES_OFF = "modules_disabled"
|
||||||
|
|
||||||
STATUS_INACTIVE = "inactive"
|
STATUS_INACTIVE = "inactive"
|
||||||
STATUS_PENDING = "pending"
|
STATUS_PENDING = "pending"
|
||||||
STATUS_ACTIVE = "active"
|
STATUS_ACTIVE = "active"
|
||||||
@@ -121,6 +136,8 @@ class LicenseState:
|
|||||||
last_heartbeat_at: str = ""
|
last_heartbeat_at: str = ""
|
||||||
heartbeat_error: str = ""
|
heartbeat_error: str = ""
|
||||||
pending_action: str = ""
|
pending_action: str = ""
|
||||||
|
#: Vor Ort abgeschaltet -- unabhaengig davon, was die Lizenz erlaubt.
|
||||||
|
modules_off: frozenset[str] = frozenset()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def role(self) -> str:
|
def role(self) -> str:
|
||||||
@@ -128,8 +145,14 @@ class LicenseState:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def modules(self) -> frozenset[str]:
|
def modules(self) -> frozenset[str]:
|
||||||
|
"""Was die Lizenz freischaltet -- ohne Ruecksicht auf den Schalter."""
|
||||||
return self.status.modules if self.status else frozenset()
|
return self.status.modules if self.status else frozenset()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def modules_in_use(self) -> frozenset[str]:
|
||||||
|
"""Was tatsaechlich laeuft: lizenziert **und** nicht abgeschaltet."""
|
||||||
|
return self.modules - self.modules_off
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def customer_name(self) -> str:
|
def customer_name(self) -> str:
|
||||||
return str((self.status.customer if self.status else {}).get("name") or "")
|
return str((self.status.customer if self.status else {}).get("name") or "")
|
||||||
@@ -179,6 +202,7 @@ class LicenseManager:
|
|||||||
"""Liest Lizenzdatei und Status neu ein."""
|
"""Liest Lizenzdatei und Status neu ein."""
|
||||||
with self._lock:
|
with self._lock:
|
||||||
state = LicenseState()
|
state = LicenseState()
|
||||||
|
state.modules_off = self.abgeschaltete_module(conn)
|
||||||
state.last_heartbeat_at = str(self.settings.get(conn, SETTING_LAST_HEARTBEAT) or "")
|
state.last_heartbeat_at = str(self.settings.get(conn, SETTING_LAST_HEARTBEAT) or "")
|
||||||
state.heartbeat_error = str(self.settings.get(conn, SETTING_HEARTBEAT_ERROR) or "")
|
state.heartbeat_error = str(self.settings.get(conn, SETTING_HEARTBEAT_ERROR) or "")
|
||||||
pending = self._pending(conn)
|
pending = self._pending(conn)
|
||||||
@@ -239,9 +263,51 @@ class LicenseManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def module_active(self, name: str) -> bool:
|
def module_active(self, name: str) -> bool:
|
||||||
"""Ist ein Vollmodul (dhcp/fileshare/maintenance) freigeschaltet?"""
|
"""Ist ein Vollmodul (dhcp/fileshare/maintenance) in Betrieb?
|
||||||
|
|
||||||
|
Zwei Bedingungen, und beide muessen gelten: die Lizenz muss es
|
||||||
|
freischalten, und der Betreiber darf es nicht abgeschaltet haben. Diese
|
||||||
|
eine Funktion fragen die Navigation, jede Route und die Hintergrund-
|
||||||
|
aufgaben -- darum sitzt der Schalter hier und nicht in einer Vorlage.
|
||||||
|
"""
|
||||||
|
return (
|
||||||
|
self.active()
|
||||||
|
and name in self._state.modules
|
||||||
|
and name not in self._state.modules_off
|
||||||
|
)
|
||||||
|
|
||||||
|
def module_licensed(self, name: str) -> bool:
|
||||||
|
"""Schaltet die Lizenz das Modul frei? (Ohne den Schalter vor Ort.)"""
|
||||||
return self.active() and name in self._state.modules
|
return self.active() and name in self._state.modules
|
||||||
|
|
||||||
|
def abgeschaltete_module(self, conn: sqlite3.Connection) -> frozenset[str]:
|
||||||
|
"""Liest die Liste aus den Einstellungen."""
|
||||||
|
gespeichert = self.settings.get(conn, SETTING_MODULES_OFF) or []
|
||||||
|
if isinstance(gespeichert, str): # Altbestand: ein einzelner Name
|
||||||
|
gespeichert = [gespeichert]
|
||||||
|
return frozenset(str(name) for name in gespeichert)
|
||||||
|
|
||||||
|
def module_abschalten(
|
||||||
|
self, conn: sqlite3.Connection, namen: Iterable[str], *, actor: str = ""
|
||||||
|
) -> tuple[frozenset[str], frozenset[str]]:
|
||||||
|
"""Setzt die Liste der abgeschalteten Module neu.
|
||||||
|
|
||||||
|
Gibt ``(eingeschaltet, abgeschaltet)`` zurueck -- also nur das, was sich
|
||||||
|
wirklich geaendert hat; der Aufrufer schreibt daraus seinen
|
||||||
|
Protokolleintrag.
|
||||||
|
|
||||||
|
Der Zustand wird hier nicht neu eingelesen, sondern nur an dieser einen
|
||||||
|
Stelle nachgezogen: ein voller Neuaufbau wuerde die Lizenzdatei erneut
|
||||||
|
pruefen, obwohl sich an ihr nichts geaendert hat -- und der Schalter ist
|
||||||
|
das Einzige, was jetzt anders ist.
|
||||||
|
"""
|
||||||
|
vorher = self.abgeschaltete_module(conn)
|
||||||
|
nachher = frozenset(str(name) for name in namen)
|
||||||
|
self.settings.set(conn, SETTING_MODULES_OFF, sorted(nachher), actor=actor)
|
||||||
|
with self._lock:
|
||||||
|
self._state.modules_off = nachher
|
||||||
|
return vorher - nachher, nachher - vorher
|
||||||
|
|
||||||
def feature_allowed(self, feature: str) -> bool:
|
def feature_allowed(self, feature: str) -> bool:
|
||||||
"""Lizenzpflichtige Einzelfunktion.
|
"""Lizenzpflichtige Einzelfunktion.
|
||||||
|
|
||||||
|
|||||||
@@ -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,),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -328,6 +328,83 @@ def ensure_presets(
|
|||||||
return created
|
return created
|
||||||
|
|
||||||
|
|
||||||
|
def unberuehrte_vorlagen_entfernen(
|
||||||
|
conn: sqlite3.Connection, tree: PermissionTree, presets: Sequence[RolePreset]
|
||||||
|
) -> list[str]:
|
||||||
|
"""Legt Vorlagengruppen in den Papierkorb -- aber nur unberuehrte.
|
||||||
|
|
||||||
|
Wird gebraucht, wenn eine Vorlage aus der Auslieferung verschwindet. Auf
|
||||||
|
einer frischen Installation entsteht sie dann gar nicht erst; auf einer
|
||||||
|
bestehenden steht sie in der Datenbank, und eine Migration, die sie einfach
|
||||||
|
loescht, nimmt einem Kunden womoeglich eine Gruppe weg, mit der er
|
||||||
|
arbeitet.
|
||||||
|
|
||||||
|
Darum diese fuenf Bedingungen, und alle fuenf muessen zutreffen:
|
||||||
|
|
||||||
|
* Die Gruppe stammt **aus der Auslieferung**: ihr ``preset_key`` ist der der
|
||||||
|
Vorlage. Diese Kennung setzt nur :func:`ensure_presets`; eine selbst
|
||||||
|
angelegte Gruppe hat sie nicht, auch wenn sie zufaellig denselben Namen
|
||||||
|
traegt. Ueber den Namen wird bewusst *nicht* gesucht -- sonst raeumte
|
||||||
|
dieser Schritt jemandem seine eigene Gruppe "Betrieb" weg.
|
||||||
|
* Sie heisst noch so, wie sie ausgeliefert wurde. Ein anderer Name heisst:
|
||||||
|
jemand hat sie uebernommen.
|
||||||
|
* Die Gruppe hat **keine Mitglieder**. Wer Mitglieder hat, wird benutzt.
|
||||||
|
* Ihre Rechte sind **unveraendert** die der Vorlage. Jede Abweichung heisst:
|
||||||
|
jemand hat sie angepasst, also ist sie seine, nicht unsere.
|
||||||
|
* Sie ist keine Systemgruppe.
|
||||||
|
|
||||||
|
Geloescht wird weich: die Gruppe liegt im Papierkorb und kommt mit einem
|
||||||
|
Klick zurueck. Gibt die Namen der einsortierten Gruppen zurueck.
|
||||||
|
"""
|
||||||
|
entfernt: list[str] = []
|
||||||
|
gruppen = list_groups(conn)
|
||||||
|
for preset in presets:
|
||||||
|
gruppe = next(
|
||||||
|
(
|
||||||
|
g
|
||||||
|
for g in gruppen
|
||||||
|
if g.preset_key
|
||||||
|
and g.preset_key == preset.key
|
||||||
|
and g.name.lower() == preset.name.lower()
|
||||||
|
),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
if gruppe is None or gruppe.is_system or gruppe.member_count:
|
||||||
|
continue
|
||||||
|
if set(gruppe.permissions) != set(tree.expand_preset(preset)):
|
||||||
|
continue
|
||||||
|
conn.execute("UPDATE groups SET deleted_at=? WHERE id=?", (_now(), gruppe.id))
|
||||||
|
entfernt.append(gruppe.name)
|
||||||
|
return entfernt
|
||||||
|
|
||||||
|
|
||||||
|
def funktionsgruppe_anlegen(
|
||||||
|
conn: sqlite3.Connection, tree: PermissionTree, preset: RolePreset
|
||||||
|
) -> int:
|
||||||
|
"""Legt die Vollzugriffsgruppe an, falls sie fehlt, und gibt ihre Nummer.
|
||||||
|
|
||||||
|
Getrennt von :func:`funktionsgruppe_fuer_administratoren`, weil das zwei
|
||||||
|
verschiedene Dinge sind: hier entsteht nur die Gruppe, dort bekommt sie
|
||||||
|
**jeder** Administrator. Wer ein einzelnes Konto aufnehmen will -- etwa
|
||||||
|
das, das an der Kommandozeile entsteht --, braucht das erste ohne das
|
||||||
|
zweite; sonst faengt sich ein Administrator, der die Gruppe bewusst
|
||||||
|
abgelegt hat, sie beim naechsten Aufruf wieder ein.
|
||||||
|
"""
|
||||||
|
zeile = Database.one(
|
||||||
|
conn, "SELECT id FROM groups WHERE name=? COLLATE NOCASE", (preset.name,)
|
||||||
|
)
|
||||||
|
if zeile is not None:
|
||||||
|
return int(zeile["id"])
|
||||||
|
cursor = conn.execute(
|
||||||
|
"INSERT INTO groups (name, description, is_system, is_default, preset_key, "
|
||||||
|
"created_at, updated_at) VALUES (?,?,0,0,?,?,?)",
|
||||||
|
(preset.name, preset.description, preset.key, _now(), _now()),
|
||||||
|
)
|
||||||
|
gruppe = int(cursor.lastrowid or 0)
|
||||||
|
_write_permissions(conn, gruppe, tree.expand_preset(preset))
|
||||||
|
return gruppe
|
||||||
|
|
||||||
|
|
||||||
def funktionsgruppe_fuer_administratoren(
|
def funktionsgruppe_fuer_administratoren(
|
||||||
conn: sqlite3.Connection, tree: PermissionTree, preset: RolePreset
|
conn: sqlite3.Connection, tree: PermissionTree, preset: RolePreset
|
||||||
) -> int:
|
) -> int:
|
||||||
@@ -345,20 +422,7 @@ def funktionsgruppe_fuer_administratoren(
|
|||||||
|
|
||||||
Gibt die Nummer der Gruppe zurueck.
|
Gibt die Nummer der Gruppe zurueck.
|
||||||
"""
|
"""
|
||||||
zeile = Database.one(
|
gruppe = funktionsgruppe_anlegen(conn, tree, preset)
|
||||||
conn, "SELECT id FROM groups WHERE name=? COLLATE NOCASE", (preset.name,)
|
|
||||||
)
|
|
||||||
if zeile is None:
|
|
||||||
cursor = conn.execute(
|
|
||||||
"INSERT INTO groups (name, description, is_system, is_default, preset_key, "
|
|
||||||
"created_at, updated_at) VALUES (?,?,0,0,?,?,?)",
|
|
||||||
(preset.name, preset.description, preset.key, _now(), _now()),
|
|
||||||
)
|
|
||||||
gruppe = int(cursor.lastrowid or 0)
|
|
||||||
_write_permissions(conn, gruppe, tree.expand_preset(preset))
|
|
||||||
else:
|
|
||||||
gruppe = int(zeile["id"])
|
|
||||||
|
|
||||||
conn.executemany(
|
conn.executemany(
|
||||||
"INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?,?)",
|
"INSERT OR IGNORE INTO user_groups (user_id, group_id) VALUES (?,?)",
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -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; }
|
||||||
@@ -1208,6 +1218,12 @@
|
|||||||
--tile-accent: var(--text-faint);
|
--tile-accent: var(--text-faint);
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
/* Spalte, damit die Fusszeile unten haengen kann (siehe .tile__actions).
|
||||||
|
Die Kacheln eines Rasters sind gleich hoch, ihr Inhalt aber nicht: mal
|
||||||
|
fehlt der Switch, mal der Standort. Ohne diesen Halt stuenden die Knoepfe
|
||||||
|
in jeder Kachel auf einer anderen Hoehe -- und das Auge sucht sie. */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
padding: var(--sp-4);
|
padding: var(--sp-4);
|
||||||
padding-left: calc(var(--sp-4) + 3px);
|
padding-left: calc(var(--sp-4) + 3px);
|
||||||
border: 1px solid var(--border-subtle);
|
border: 1px solid var(--border-subtle);
|
||||||
@@ -1269,6 +1285,73 @@
|
|||||||
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.
|
||||||
|
``margin-top: auto`` schiebt sie an den unteren Rand -- unabhaengig
|
||||||
|
davon, wie viel darueber steht. Die beiden Eingriffe stehen damit in
|
||||||
|
jeder Kachel an derselben Stelle, und der eine liegt nicht dort, wo eben
|
||||||
|
noch der andere war. */
|
||||||
|
.tile__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--sp-2);
|
||||||
|
margin-top: auto;
|
||||||
|
padding-top: var(--sp-3);
|
||||||
|
}
|
||||||
|
.tile__actions > :last-child { margin-left: auto; }
|
||||||
|
/* Steht nur ein Knopf da, bleibt er links -- sonst spraenge der Neustart bei
|
||||||
|
einem Geraet ohne Bildschirmzugang an die Stelle, an der nebenan VNC
|
||||||
|
steht. Das Fuellstueck haelt den Platz. */
|
||||||
|
.tile__spacer { flex: 1 1 auto; }
|
||||||
|
|
||||||
/* Marke und Karte stehen uebereinander im normalen Fluss. Vorher lag die
|
/* Marke und Karte stehen uebereinander im normalen Fluss. Vorher lag die
|
||||||
Marke absolut positioniert hinter der Karte -- auf einem flachen Fenster
|
Marke absolut positioniert hinter der Karte -- auf einem flachen Fenster
|
||||||
schnitt die Karte mitten durch den Schriftzug und die Funktionszeile. Ein
|
schnitt die Karte mitten durch den Schriftzug und die Funktionszeile. Ein
|
||||||
|
|||||||
@@ -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
Vendored
+322
@@ -0,0 +1,322 @@
|
|||||||
|
import * as utils from "../utils/common.js";
|
||||||
|
|
||||||
|
var MAXBITS = 15;
|
||||||
|
var ENOUGH_LENS = 852;
|
||||||
|
var ENOUGH_DISTS = 592;
|
||||||
|
//var ENOUGH = (ENOUGH_LENS+ENOUGH_DISTS);
|
||||||
|
|
||||||
|
var CODES = 0;
|
||||||
|
var LENS = 1;
|
||||||
|
var DISTS = 2;
|
||||||
|
|
||||||
|
var lbase = [ /* Length codes 257..285 base */
|
||||||
|
3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 15, 17, 19, 23, 27, 31,
|
||||||
|
35, 43, 51, 59, 67, 83, 99, 115, 131, 163, 195, 227, 258, 0, 0
|
||||||
|
];
|
||||||
|
|
||||||
|
var lext = [ /* Length codes 257..285 extra */
|
||||||
|
16, 16, 16, 16, 16, 16, 16, 16, 17, 17, 17, 17, 18, 18, 18, 18,
|
||||||
|
19, 19, 19, 19, 20, 20, 20, 20, 21, 21, 21, 21, 16, 72, 78
|
||||||
|
];
|
||||||
|
|
||||||
|
var dbase = [ /* Distance codes 0..29 base */
|
||||||
|
1, 2, 3, 4, 5, 7, 9, 13, 17, 25, 33, 49, 65, 97, 129, 193,
|
||||||
|
257, 385, 513, 769, 1025, 1537, 2049, 3073, 4097, 6145,
|
||||||
|
8193, 12289, 16385, 24577, 0, 0
|
||||||
|
];
|
||||||
|
|
||||||
|
var dext = [ /* Distance codes 0..29 extra */
|
||||||
|
16, 16, 16, 16, 17, 17, 18, 18, 19, 19, 20, 20, 21, 21, 22, 22,
|
||||||
|
23, 23, 24, 24, 25, 25, 26, 26, 27, 27,
|
||||||
|
28, 28, 29, 29, 64, 64
|
||||||
|
];
|
||||||
|
|
||||||
|
export default function inflate_table(type, lens, lens_index, codes, table, table_index, work, opts)
|
||||||
|
{
|
||||||
|
var bits = opts.bits;
|
||||||
|
//here = opts.here; /* table entry for duplication */
|
||||||
|
|
||||||
|
var len = 0; /* a code's length in bits */
|
||||||
|
var sym = 0; /* index of code symbols */
|
||||||
|
var min = 0, max = 0; /* minimum and maximum code lengths */
|
||||||
|
var root = 0; /* number of index bits for root table */
|
||||||
|
var curr = 0; /* number of index bits for current table */
|
||||||
|
var drop = 0; /* code bits to drop for sub-table */
|
||||||
|
var left = 0; /* number of prefix codes available */
|
||||||
|
var used = 0; /* code entries in table used */
|
||||||
|
var huff = 0; /* Huffman code */
|
||||||
|
var incr; /* for incrementing code, index */
|
||||||
|
var fill; /* index for replicating entries */
|
||||||
|
var low; /* low bits for current root entry */
|
||||||
|
var mask; /* mask for low root bits */
|
||||||
|
var next; /* next available space in table */
|
||||||
|
var base = null; /* base value table to use */
|
||||||
|
var base_index = 0;
|
||||||
|
// var shoextra; /* extra bits table to use */
|
||||||
|
var end; /* use base and extra for symbol > end */
|
||||||
|
var count = new utils.Buf16(MAXBITS + 1); //[MAXBITS+1]; /* number of codes of each length */
|
||||||
|
var offs = new utils.Buf16(MAXBITS + 1); //[MAXBITS+1]; /* offsets in table for each length */
|
||||||
|
var extra = null;
|
||||||
|
var extra_index = 0;
|
||||||
|
|
||||||
|
var here_bits, here_op, here_val;
|
||||||
|
|
||||||
|
/*
|
||||||
|
Process a set of code lengths to create a canonical Huffman code. The
|
||||||
|
code lengths are lens[0..codes-1]. Each length corresponds to the
|
||||||
|
symbols 0..codes-1. The Huffman code is generated by first sorting the
|
||||||
|
symbols by length from short to long, and retaining the symbol order
|
||||||
|
for codes with equal lengths. Then the code starts with all zero bits
|
||||||
|
for the first code of the shortest length, and the codes are integer
|
||||||
|
increments for the same length, and zeros are appended as the length
|
||||||
|
increases. For the deflate format, these bits are stored backwards
|
||||||
|
from their more natural integer increment ordering, and so when the
|
||||||
|
decoding tables are built in the large loop below, the integer codes
|
||||||
|
are incremented backwards.
|
||||||
|
|
||||||
|
This routine assumes, but does not check, that all of the entries in
|
||||||
|
lens[] are in the range 0..MAXBITS. The caller must assure this.
|
||||||
|
1..MAXBITS is interpreted as that code length. zero means that that
|
||||||
|
symbol does not occur in this code.
|
||||||
|
|
||||||
|
The codes are sorted by computing a count of codes for each length,
|
||||||
|
creating from that a table of starting indices for each length in the
|
||||||
|
sorted table, and then entering the symbols in order in the sorted
|
||||||
|
table. The sorted table is work[], with that space being provided by
|
||||||
|
the caller.
|
||||||
|
|
||||||
|
The length counts are used for other purposes as well, i.e. finding
|
||||||
|
the minimum and maximum length codes, determining if there are any
|
||||||
|
codes at all, checking for a valid set of lengths, and looking ahead
|
||||||
|
at length counts to determine sub-table sizes when building the
|
||||||
|
decoding tables.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* accumulate lengths for codes (assumes lens[] all in 0..MAXBITS) */
|
||||||
|
for (len = 0; len <= MAXBITS; len++) {
|
||||||
|
count[len] = 0;
|
||||||
|
}
|
||||||
|
for (sym = 0; sym < codes; sym++) {
|
||||||
|
count[lens[lens_index + sym]]++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* bound code lengths, force root to be within code lengths */
|
||||||
|
root = bits;
|
||||||
|
for (max = MAXBITS; max >= 1; max--) {
|
||||||
|
if (count[max] !== 0) { break; }
|
||||||
|
}
|
||||||
|
if (root > max) {
|
||||||
|
root = max;
|
||||||
|
}
|
||||||
|
if (max === 0) { /* no symbols to code at all */
|
||||||
|
//table.op[opts.table_index] = 64; //here.op = (var char)64; /* invalid code marker */
|
||||||
|
//table.bits[opts.table_index] = 1; //here.bits = (var char)1;
|
||||||
|
//table.val[opts.table_index++] = 0; //here.val = (var short)0;
|
||||||
|
table[table_index++] = (1 << 24) | (64 << 16) | 0;
|
||||||
|
|
||||||
|
|
||||||
|
//table.op[opts.table_index] = 64;
|
||||||
|
//table.bits[opts.table_index] = 1;
|
||||||
|
//table.val[opts.table_index++] = 0;
|
||||||
|
table[table_index++] = (1 << 24) | (64 << 16) | 0;
|
||||||
|
|
||||||
|
opts.bits = 1;
|
||||||
|
return 0; /* no symbols, but wait for decoding to report error */
|
||||||
|
}
|
||||||
|
for (min = 1; min < max; min++) {
|
||||||
|
if (count[min] !== 0) { break; }
|
||||||
|
}
|
||||||
|
if (root < min) {
|
||||||
|
root = min;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check for an over-subscribed or incomplete set of lengths */
|
||||||
|
left = 1;
|
||||||
|
for (len = 1; len <= MAXBITS; len++) {
|
||||||
|
left <<= 1;
|
||||||
|
left -= count[len];
|
||||||
|
if (left < 0) {
|
||||||
|
return -1;
|
||||||
|
} /* over-subscribed */
|
||||||
|
}
|
||||||
|
if (left > 0 && (type === CODES || max !== 1)) {
|
||||||
|
return -1; /* incomplete set */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* generate offsets into symbol table for each length for sorting */
|
||||||
|
offs[1] = 0;
|
||||||
|
for (len = 1; len < MAXBITS; len++) {
|
||||||
|
offs[len + 1] = offs[len] + count[len];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* sort symbols by length, by symbol order within each length */
|
||||||
|
for (sym = 0; sym < codes; sym++) {
|
||||||
|
if (lens[lens_index + sym] !== 0) {
|
||||||
|
work[offs[lens[lens_index + sym]]++] = sym;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create and fill in decoding tables. In this loop, the table being
|
||||||
|
filled is at next and has curr index bits. The code being used is huff
|
||||||
|
with length len. That code is converted to an index by dropping drop
|
||||||
|
bits off of the bottom. For codes where len is less than drop + curr,
|
||||||
|
those top drop + curr - len bits are incremented through all values to
|
||||||
|
fill the table with replicated entries.
|
||||||
|
|
||||||
|
root is the number of index bits for the root table. When len exceeds
|
||||||
|
root, sub-tables are created pointed to by the root entry with an index
|
||||||
|
of the low root bits of huff. This is saved in low to check for when a
|
||||||
|
new sub-table should be started. drop is zero when the root table is
|
||||||
|
being filled, and drop is root when sub-tables are being filled.
|
||||||
|
|
||||||
|
When a new sub-table is needed, it is necessary to look ahead in the
|
||||||
|
code lengths to determine what size sub-table is needed. The length
|
||||||
|
counts are used for this, and so count[] is decremented as codes are
|
||||||
|
entered in the tables.
|
||||||
|
|
||||||
|
used keeps track of how many table entries have been allocated from the
|
||||||
|
provided *table space. It is checked for LENS and DIST tables against
|
||||||
|
the constants ENOUGH_LENS and ENOUGH_DISTS to guard against changes in
|
||||||
|
the initial root table size constants. See the comments in inftrees.h
|
||||||
|
for more information.
|
||||||
|
|
||||||
|
sym increments through all symbols, and the loop terminates when
|
||||||
|
all codes of length max, i.e. all codes, have been processed. This
|
||||||
|
routine permits incomplete codes, so another loop after this one fills
|
||||||
|
in the rest of the decoding tables with invalid code markers.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* set up for code type */
|
||||||
|
// poor man optimization - use if-else instead of switch,
|
||||||
|
// to avoid deopts in old v8
|
||||||
|
if (type === CODES) {
|
||||||
|
base = extra = work; /* dummy value--not used */
|
||||||
|
end = 19;
|
||||||
|
|
||||||
|
} else if (type === LENS) {
|
||||||
|
base = lbase;
|
||||||
|
base_index -= 257;
|
||||||
|
extra = lext;
|
||||||
|
extra_index -= 257;
|
||||||
|
end = 256;
|
||||||
|
|
||||||
|
} else { /* DISTS */
|
||||||
|
base = dbase;
|
||||||
|
extra = dext;
|
||||||
|
end = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* initialize opts for loop */
|
||||||
|
huff = 0; /* starting code */
|
||||||
|
sym = 0; /* starting code symbol */
|
||||||
|
len = min; /* starting code length */
|
||||||
|
next = table_index; /* current table to fill in */
|
||||||
|
curr = root; /* current table index bits */
|
||||||
|
drop = 0; /* current bits to drop from code for index */
|
||||||
|
low = -1; /* trigger new sub-table when len > root */
|
||||||
|
used = 1 << root; /* use root table entries */
|
||||||
|
mask = used - 1; /* mask for comparing low */
|
||||||
|
|
||||||
|
/* check available table space */
|
||||||
|
if ((type === LENS && used > ENOUGH_LENS) ||
|
||||||
|
(type === DISTS && used > ENOUGH_DISTS)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* process all codes and make table entries */
|
||||||
|
for (;;) {
|
||||||
|
/* create table entry */
|
||||||
|
here_bits = len - drop;
|
||||||
|
if (work[sym] < end) {
|
||||||
|
here_op = 0;
|
||||||
|
here_val = work[sym];
|
||||||
|
}
|
||||||
|
else if (work[sym] > end) {
|
||||||
|
here_op = extra[extra_index + work[sym]];
|
||||||
|
here_val = base[base_index + work[sym]];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
here_op = 32 + 64; /* end of block */
|
||||||
|
here_val = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* replicate for those indices with low len bits equal to huff */
|
||||||
|
incr = 1 << (len - drop);
|
||||||
|
fill = 1 << curr;
|
||||||
|
min = fill; /* save offset to next table */
|
||||||
|
do {
|
||||||
|
fill -= incr;
|
||||||
|
table[next + (huff >> drop) + fill] = (here_bits << 24) | (here_op << 16) | here_val |0;
|
||||||
|
} while (fill !== 0);
|
||||||
|
|
||||||
|
/* backwards increment the len-bit code huff */
|
||||||
|
incr = 1 << (len - 1);
|
||||||
|
while (huff & incr) {
|
||||||
|
incr >>= 1;
|
||||||
|
}
|
||||||
|
if (incr !== 0) {
|
||||||
|
huff &= incr - 1;
|
||||||
|
huff += incr;
|
||||||
|
} else {
|
||||||
|
huff = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* go to next symbol, update count, len */
|
||||||
|
sym++;
|
||||||
|
if (--count[len] === 0) {
|
||||||
|
if (len === max) { break; }
|
||||||
|
len = lens[lens_index + work[sym]];
|
||||||
|
}
|
||||||
|
|
||||||
|
/* create new sub-table if needed */
|
||||||
|
if (len > root && (huff & mask) !== low) {
|
||||||
|
/* if first time, transition to sub-tables */
|
||||||
|
if (drop === 0) {
|
||||||
|
drop = root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* increment past last table */
|
||||||
|
next += min; /* here min is 1 << curr */
|
||||||
|
|
||||||
|
/* determine length of next table */
|
||||||
|
curr = len - drop;
|
||||||
|
left = 1 << curr;
|
||||||
|
while (curr + drop < max) {
|
||||||
|
left -= count[curr + drop];
|
||||||
|
if (left <= 0) { break; }
|
||||||
|
curr++;
|
||||||
|
left <<= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* check for enough space */
|
||||||
|
used += 1 << curr;
|
||||||
|
if ((type === LENS && used > ENOUGH_LENS) ||
|
||||||
|
(type === DISTS && used > ENOUGH_DISTS)) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* point entry in root table to sub-table */
|
||||||
|
low = huff & mask;
|
||||||
|
/*table.op[low] = curr;
|
||||||
|
table.bits[low] = root;
|
||||||
|
table.val[low] = next - opts.table_index;*/
|
||||||
|
table[low] = (root << 24) | (curr << 16) | (next - table_index) |0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fill in remaining table entry if code is incomplete (guaranteed to have
|
||||||
|
at most one remaining entry, since if the code is incomplete, the
|
||||||
|
maximum code length that was allowed to get this far is one bit) */
|
||||||
|
if (huff !== 0) {
|
||||||
|
//table.op[next + huff] = 64; /* invalid code marker */
|
||||||
|
//table.bits[next + huff] = len - drop;
|
||||||
|
//table.val[next + huff] = 0;
|
||||||
|
table[next + huff] = ((len - drop) << 24) | (64 << 16) |0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* set return parameters */
|
||||||
|
//opts.table_index += used;
|
||||||
|
opts.bits = root;
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
Vendored
+11
@@ -0,0 +1,11 @@
|
|||||||
|
export default {
|
||||||
|
2: 'need dictionary', /* Z_NEED_DICT 2 */
|
||||||
|
1: 'stream end', /* Z_STREAM_END 1 */
|
||||||
|
0: '', /* Z_OK 0 */
|
||||||
|
'-1': 'file error', /* Z_ERRNO (-1) */
|
||||||
|
'-2': 'stream error', /* Z_STREAM_ERROR (-2) */
|
||||||
|
'-3': 'data error', /* Z_DATA_ERROR (-3) */
|
||||||
|
'-4': 'insufficient memory', /* Z_MEM_ERROR (-4) */
|
||||||
|
'-5': 'buffer error', /* Z_BUF_ERROR (-5) */
|
||||||
|
'-6': 'incompatible version' /* Z_VERSION_ERROR (-6) */
|
||||||
|
};
|
||||||
Vendored
+1195
File diff suppressed because it is too large.
Load diff
Loaded 100 of 106 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user