This repository has been archived on 2024-05-31. You can view files and clone it, but cannot push or open issues or pull requests.
authentik/web/src/enterprise/rac/index.ts

325 lines
10 KiB
TypeScript
Raw Normal View History

enterprise/providers: Add RAC [AUTH-15] (#7291) * add basic guacamole Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make everything mostly work Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add rac build to CI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix resize, fix web lint, sendSize correctly Signed-off-by: Jens Langhammer <jens@goauthentik.io> * pre-send connection from client, format Signed-off-by: Jens Langhammer <jens@goauthentik.io> * improve throughput Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rework TokenOutpostConsumer into middleware Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix some layout issues Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add outpost controllers Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start testing audio things Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix a bunch of things Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add deps Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix to work with outpost group Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add simple loadbalancing Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add simple reconnect Signed-off-by: Jens Langhammer <jens@goauthentik.io> * show reconnecting text Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix error when checking ports Signed-off-by: Jens Langhammer <jens@goauthentik.io> * move to providers Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add flow check to interface Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix go lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix rac app label Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix audio Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add logging Signed-off-by: Jens Langhammer <jens@goauthentik.io> * cleanup Signed-off-by: Jens Langhammer <jens@goauthentik.io> * allow overriding all settings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix duplicate keyboard, debug high DPI Signed-off-by: Jens Langhammer <jens@goauthentik.io> * re-add deps Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix missing __init__.py breaking model loading I love python Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * bump successful ws connection to info Signed-off-by: Jens Langhammer <jens@goauthentik.io> * hide cursor since guac draws that Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add clipboard support (bidirectional) Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make codespell not want to break the code Signed-off-by: Jens Langhammer <jens@goauthentik.io> * run pr comment in separate task Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start endpoint and property mapping stuff Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more endpoint things Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated: fix event model_pk filtering with ints Signed-off-by: Jens Langhammer <jens@goauthentik.io> * unrelated: improve event display for changelog Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rebuild endpoint stuff again Signed-off-by: Jens Langhammer <jens@goauthentik.io> * idk special url Signed-off-by: Jens Langhammer <jens@goauthentik.io> * more stuff, connect token with session Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add disconnect Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rework disconnect cleanly disconnect from guacd instead of just letting the connection timeout Signed-off-by: Jens Langhammer <jens@goauthentik.io> * clear cache when creating outpost Signed-off-by: Jens Langhammer <jens@goauthentik.io> * support host:port and fix protocol Signed-off-by: Jens Langhammer <jens@goauthentik.io> * center smaller viewport Signed-off-by: Jens Langhammer <jens@goauthentik.io> * rework connection to wait more and stop after some time Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add policy control to endpoints Signed-off-by: Jens Langhammer <jens@goauthentik.io> * remove provider protocol Signed-off-by: Jens Langhammer <jens@goauthentik.io> * don't switch to different outpost connection when already chosen Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start using property mappings, add static settings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add some RAC mapping settings Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix lint Signed-off-by: Jens Langhammer <jens@goauthentik.io> * start adding tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add tests for event changes Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add tests and fix issues found by said tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add preview banner, move endpoints to main page Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add locale Signed-off-by: Jens Langhammer <jens@goauthentik.io> * auto-select endpoint if only one is available Signed-off-by: Jens Langhammer <jens@goauthentik.io> * backport https://github.com/goauthentik/authentik/pull/7831 to rac Signed-off-by: Jens Langhammer <jens@goauthentik.io> * dont select property mappings on endpoints Signed-off-by: Jens Langhammer <jens@goauthentik.io> * make table modal only load when opened Signed-off-by: Jens Langhammer <jens@goauthentik.io> * only auto-redirect when open Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix web deps Signed-off-by: Jens Langhammer <jens@goauthentik.io> * check for token expiry and terminate session Signed-off-by: Jens Langhammer <jens@goauthentik.io> * re-add endpoint name to title Signed-off-by: Jens Langhammer <jens@goauthentik.io> * disconnect connection when token is manually deleted Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add initial RAC docs Signed-off-by: Jens Langhammer <jens@goauthentik.io> * add connection expiry setting to provider Signed-off-by: Jens Langhammer <jens@goauthentik.io> * fix flaky tests Signed-off-by: Jens Langhammer <jens@goauthentik.io> --------- Signed-off-by: Jens Langhammer <jens@goauthentik.io>
2023-12-30 20:33:14 +00:00
import { TITLE_DEFAULT } from "@goauthentik/app/common/constants";
import { Interface } from "@goauthentik/elements/Base";
import "@goauthentik/elements/LoadingOverlay";
import Guacamole from "guacamole-common-js";
import { msg, str } from "@lit/localize";
import { CSSResult, TemplateResult, css, html } from "lit";
import { customElement, property, state } from "lit/decorators.js";
import AKGlobal from "@goauthentik/common/styles/authentik.css";
import PFContent from "@patternfly/patternfly/components/Content/content.css";
import PFPage from "@patternfly/patternfly/components/Page/page.css";
import PFBase from "@patternfly/patternfly/patternfly-base.css";
enum GuacClientState {
IDLE = 0,
CONNECTING = 1,
WAITING = 2,
CONNECTED = 3,
DISCONNECTING = 4,
DISCONNECTED = 5,
}
const AUDIO_INPUT_MIMETYPE = "audio/L16;rate=44100,channels=2";
const RECONNECT_ATTEMPTS_INITIAL = 5;
const RECONNECT_ATTEMPTS = 5;
@customElement("ak-rac")
export class RacInterface extends Interface {
static get styles(): CSSResult[] {
return [
PFBase,
PFPage,
PFContent,
AKGlobal,
css`
:host {
cursor: none;
}
canvas {
z-index: unset !important;
}
.container {
overflow: hidden;
height: 100vh;
background-color: black;
display: flex;
justify-content: center;
align-items: center;
}
ak-loading-overlay {
z-index: 5;
}
`,
];
}
client?: Guacamole.Client;
tunnel?: Guacamole.Tunnel;
@state()
container?: HTMLElement;
@state()
clientState?: GuacClientState;
@state()
reconnectingMessage = "";
@property()
token?: string;
@property()
endpointName?: string;
@state()
clipboardWatcherTimer = 0;
_previousClipboardValue: unknown;
// Set to `true` if we've successfully connected once
hasConnected = false;
// Keep track of current connection attempt
connectionAttempt = 0;
static domSize(): DOMRect {
return document.body.getBoundingClientRect();
}
constructor() {
super();
this.initKeyboard();
this.checkClipboard();
this.clipboardWatcherTimer = setInterval(
this.checkClipboard.bind(this),
500,
) as unknown as number;
}
connectedCallback(): void {
super.connectedCallback();
window.addEventListener(
"focus",
() => {
this.checkClipboard();
},
{
capture: false,
},
);
window.addEventListener("resize", () => {
this.client?.sendSize(
Math.floor(RacInterface.domSize().width),
Math.floor(RacInterface.domSize().height),
);
});
}
disconnectedCallback(): void {
super.disconnectedCallback();
clearInterval(this.clipboardWatcherTimer);
}
async firstUpdated(): Promise<void> {
this.updateTitle();
const wsUrl = `${window.location.protocol.replace("http", "ws")}//${
window.location.host
}/ws/rac/${this.token}/`;
this.tunnel = new Guacamole.WebSocketTunnel(wsUrl);
this.tunnel.receiveTimeout = 10 * 1000; // 10 seconds
this.tunnel.onerror = (status) => {
console.debug("authentik/rac: tunnel error: ", status);
this.reconnect();
};
this.client = new Guacamole.Client(this.tunnel);
this.client.onerror = (err) => {
console.debug("authentik/rac: error: ", err);
this.reconnect();
};
this.client.onstatechange = (state) => {
this.clientState = state;
if (state === GuacClientState.CONNECTED) {
this.onConnected();
}
};
this.client.onclipboard = (stream, mimetype) => {
// If the received data is text, read it as a simple string
if (/^text\//.exec(mimetype)) {
const reader = new Guacamole.StringReader(stream);
let data = "";
reader.ontext = (text) => {
data += text;
};
reader.onend = () => {
this._previousClipboardValue = data;
navigator.clipboard.writeText(data);
};
} else {
const reader = new Guacamole.BlobReader(stream, mimetype);
reader.onend = () => {
const blob = reader.getBlob();
navigator.clipboard.write([
new ClipboardItem({
[blob.type]: blob,
}),
]);
};
}
console.debug("authentik/rac: updated clipboard from remote");
};
const params = new URLSearchParams();
params.set("screen_width", Math.floor(RacInterface.domSize().width).toString());
params.set("screen_height", Math.floor(RacInterface.domSize().height).toString());
params.set("screen_dpi", (window.devicePixelRatio * 96).toString());
this.client.connect(params.toString());
}
reconnect(): void {
this.clientState = undefined;
this.connectionAttempt += 1;
if (!this.hasConnected) {
// Check connection attempts if we haven't had a successful connection
if (this.connectionAttempt >= RECONNECT_ATTEMPTS_INITIAL) {
this.hasConnected = true;
this.reconnectingMessage = msg(
str`Connection failed after ${this.connectionAttempt} attempts.`,
);
return;
}
} else {
if (this.connectionAttempt >= RECONNECT_ATTEMPTS) {
this.reconnectingMessage = msg(
str`Connection failed after ${this.connectionAttempt} attempts.`,
);
return;
}
}
const delay = 500 * this.connectionAttempt;
this.reconnectingMessage = msg(
str`Re-connecting in ${Math.max(1, delay / 1000)} second(s).`,
);
setTimeout(() => {
this.firstUpdated();
}, delay);
}
updateTitle(): void {
let title = this.tenant?.brandingTitle || TITLE_DEFAULT;
if (this.endpointName) {
title = `${this.endpointName} - ${title}`;
}
document.title = `${title}`;
}
onConnected(): void {
console.debug("authentik/rac: connected");
if (!this.client) {
return;
}
this.hasConnected = true;
this.container = this.client.getDisplay().getElement();
this.initMouse(this.container);
this.client?.sendSize(
Math.floor(RacInterface.domSize().width),
Math.floor(RacInterface.domSize().height),
);
}
initMouse(container: HTMLElement): void {
const mouse = new Guacamole.Mouse(container);
const handler = (mouseState: Guacamole.Mouse.State, scaleMouse = false) => {
if (!this.client) return;
if (scaleMouse) {
mouseState.y = mouseState.y / this.client.getDisplay().getScale();
mouseState.x = mouseState.x / this.client.getDisplay().getScale();
}
this.client.sendMouseState(mouseState);
};
mouse.onmouseup = mouse.onmousedown = (mouseState) => {
this.container?.focus();
handler(mouseState);
};
mouse.onmousemove = (mouseState) => {
handler(mouseState, true);
};
}
initAudioInput(): void {
const stream = this.client?.createAudioStream(AUDIO_INPUT_MIMETYPE);
if (!stream) return;
// Guacamole.AudioPlayer
const recorder = Guacamole.AudioRecorder.getInstance(stream, AUDIO_INPUT_MIMETYPE);
// If creation of the AudioRecorder failed, simply end the stream
if (!recorder) {
stream.sendEnd();
return;
}
// Otherwise, ensure that another audio stream is created after this
// audio stream is closed
recorder.onclose = this.initAudioInput.bind(this);
}
initKeyboard(): void {
const keyboard = new Guacamole.Keyboard(document);
keyboard.onkeydown = (keysym) => {
this.client?.sendKeyEvent(1, keysym);
};
keyboard.onkeyup = (keysym) => {
this.client?.sendKeyEvent(0, keysym);
};
}
async checkClipboard(): Promise<void> {
try {
if (!this._previousClipboardValue) {
this._previousClipboardValue = await navigator.clipboard.readText();
return;
}
const newValue = await navigator.clipboard.readText();
if (newValue !== this._previousClipboardValue) {
console.debug(`authentik/rac: new clipboard value: ${newValue}`);
this._previousClipboardValue = newValue;
this.writeClipboard(newValue);
}
} catch (ex) {
// The error is most likely caused by the document not being in focus
// in which case we can ignore it and just retry
if (ex instanceof DOMException) {
return;
}
console.warn("authentik/rac: error reading clipboard", ex);
}
}
private writeClipboard(value: string) {
if (!this.client) {
return;
}
const stream = this.client.createClipboardStream("text/plain", "clipboard");
const writer = new Guacamole.StringWriter(stream);
writer.sendText(value);
writer.sendEnd();
console.debug("authentik/rac: Sent clipboard");
}
render(): TemplateResult {
return html`
${this.clientState !== GuacClientState.CONNECTED
? html`
<ak-loading-overlay>
<span slot="body">
${this.hasConnected
? html`${this.reconnectingMessage}`
: html`${msg("Connecting...")}`}
</span>
</ak-loading-overlay>
`
: html``}
<div class="container">${this.container}</div>
`;
}
}