Ephemeral keys for xterm.js. Initial rework of audit logging. All endpoints now return a 401 regardless of presence if not logged in.
This commit is contained in:
@@ -1,38 +1,39 @@
|
||||
{% extends "base.html" %}
|
||||
{% load static %}
|
||||
|
||||
{% block title %}Shell • {{ server.display_name }} • Keywarden{% endblock %}
|
||||
|
||||
{% block extra_head %}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.4.0/css/xterm.css">
|
||||
<link rel="stylesheet" href="{% static 'vendor/xterm/xterm.css' %}">
|
||||
{% if is_popout %}
|
||||
<style>
|
||||
body.popout-shell main {
|
||||
max-width: none !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
</style>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-8">
|
||||
{% if not is_popout %}
|
||||
{% include "servers/_header.html" %}
|
||||
{% else %}
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-xl font-semibold text-gray-900">Shell • {{ server.display_name }}</h1>
|
||||
<p class="text-sm text-gray-600">
|
||||
{{ server.hostname|default:server.ipv4|default:server.ipv6|default:"Unassigned" }}
|
||||
</p>
|
||||
</div>
|
||||
<button type="button" class="text-sm font-semibold text-gray-500 hover:text-gray-700" onclick="window.close()">
|
||||
Close
|
||||
</button>
|
||||
{% if is_popout %}
|
||||
<div class="w-screen">
|
||||
<div id="shell-popout-shell" class="w-full border border-gray-200 bg-gray-900 shadow-sm">
|
||||
<div id="shell-terminal" class="h-full w-full p-2"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="space-y-8">
|
||||
{% include "servers/_header.html" %}
|
||||
|
||||
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Shell access</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Connect with your private key and the signed certificate for this server.
|
||||
</p>
|
||||
</div>
|
||||
{% if not is_popout %}
|
||||
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Shell access</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Connect with your private key and the signed certificate for this server.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md border border-gray-200 bg-white px-3 py-1.5 text-xs font-semibold text-gray-700 hover:bg-gray-50"
|
||||
@@ -41,101 +42,110 @@
|
||||
>
|
||||
Pop out terminal
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-4 text-sm text-gray-600">
|
||||
<dl class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<dt>Account name</dt>
|
||||
<dd class="font-medium text-gray-900">
|
||||
{% if system_username %}
|
||||
{{ system_username }}
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<dt>Host</dt>
|
||||
<dd class="font-medium text-gray-900">
|
||||
{% if shell_target %}
|
||||
{{ shell_target }}
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
{% if shell_command %}
|
||||
<div class="rounded-xl border border-dashed border-gray-200 bg-gray-50 p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class="text-xs font-semibold text-gray-500">SSH command</span>
|
||||
<button
|
||||
type="button"
|
||||
class="text-xs font-semibold text-purple-700 hover:text-purple-800"
|
||||
data-copy-target="shell-command"
|
||||
>
|
||||
Copy command
|
||||
</button>
|
||||
<div class="mt-4 space-y-4 text-sm text-gray-600">
|
||||
<dl class="space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<dt>Account name</dt>
|
||||
<dd class="font-medium text-gray-900">
|
||||
{% if system_username %}
|
||||
{{ system_username }}
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<code class="mt-2 block break-all text-xs text-gray-800" id="shell-command">{{ shell_command }}</code>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{% if certificate_key_id %}
|
||||
<div class="inline-flex overflow-hidden rounded-md shadow-sm">
|
||||
<div class="flex items-center justify-between">
|
||||
<dt>Host</dt>
|
||||
<dd class="font-medium text-gray-900">
|
||||
{% if shell_target %}
|
||||
{{ shell_target }}
|
||||
{% else %}
|
||||
Unknown
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
{% if shell_command %}
|
||||
<div class="rounded-xl border border-dashed border-gray-200 bg-gray-50 p-4">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<span class="text-xs font-semibold text-gray-500">SSH command</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-l-md bg-purple-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-purple-700"
|
||||
data-download-url="/api/v1/keys/{{ certificate_key_id }}/certificate"
|
||||
class="text-xs font-semibold text-purple-700 hover:text-purple-800"
|
||||
data-copy-target="shell-command"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-r-md border border-l-0 border-gray-200 bg-gray-100 px-3 py-1.5 text-xs font-semibold text-gray-700 hover:bg-gray-200"
|
||||
data-download-url="/api/v1/keys/{{ certificate_key_id }}/certificate.sha256"
|
||||
>
|
||||
Hash
|
||||
Copy command
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-700 js-regenerate-cert"
|
||||
data-key-id="{{ certificate_key_id }}"
|
||||
>
|
||||
Regenerate
|
||||
</button>
|
||||
{% endif %}
|
||||
<span class="text-xs text-gray-500">Use the command above for local SSH.</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-600">Upload a key to enable downloads and a local SSH command.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Browser terminal</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Launch a proxied terminal session to the target host in your browser.
|
||||
</p>
|
||||
<code class="mt-2 block break-all text-xs text-gray-800" id="shell-command">{{ shell_command }}</code>
|
||||
</div>
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
{% if certificate_key_id %}
|
||||
<div class="inline-flex overflow-hidden rounded-md shadow-sm">
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-l-md bg-purple-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-purple-700"
|
||||
data-download-url="/api/v1/keys/{{ certificate_key_id }}/certificate"
|
||||
>
|
||||
Download
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-r-md border border-l-0 border-gray-200 bg-gray-100 px-3 py-1.5 text-xs font-semibold text-gray-700 hover:bg-gray-200"
|
||||
data-download-url="/api/v1/keys/{{ certificate_key_id }}/certificate.sha256"
|
||||
>
|
||||
Hash
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md bg-red-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-red-700 js-regenerate-cert"
|
||||
data-key-id="{{ certificate_key_id }}"
|
||||
>
|
||||
Regenerate
|
||||
</button>
|
||||
{% endif %}
|
||||
<span class="text-xs text-gray-500">Use the command above for local SSH.</span>
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-sm text-gray-600">Upload a key to enable downloads and a local SSH command.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<span class="text-xs font-semibold text-gray-500">Beta</span>
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl border border-gray-200 bg-gray-900 p-2">
|
||||
<div id="shell-terminal" class="h-96"></div>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-gray-500">
|
||||
Sessions are proxied through Keywarden and end when this page closes.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/xterm@5.4.0/lib/xterm.js"></script>
|
||||
<section class="rounded-2xl border border-gray-200 bg-white p-5 shadow-sm">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h2 class="text-lg font-semibold text-gray-900">Browser terminal</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">
|
||||
Launch a proxied terminal session to the target host in your browser.
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-xs font-semibold text-gray-500">Beta</span>
|
||||
<button
|
||||
type="button"
|
||||
class="inline-flex items-center rounded-md bg-purple-600 px-3 py-1.5 text-xs font-semibold text-white hover:bg-purple-700"
|
||||
id="shell-start"
|
||||
>
|
||||
Start terminal
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 rounded-xl border border-gray-200 bg-gray-900 p-2">
|
||||
<div id="shell-terminal" class="h-96"></div>
|
||||
</div>
|
||||
<p class="mt-3 text-xs text-gray-500">
|
||||
Sessions are proxied through Keywarden and end when this page closes.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<script src="{% static 'vendor/xterm/xterm.js' %}"></script>
|
||||
<script>
|
||||
(function () {
|
||||
function getCookie(name) {
|
||||
@@ -227,7 +237,77 @@
|
||||
}
|
||||
|
||||
var termContainer = document.getElementById("shell-terminal");
|
||||
if (termContainer && window.Terminal) {
|
||||
var startButton = document.getElementById("shell-start");
|
||||
var activeSocket = null;
|
||||
var activeTerm = null;
|
||||
var popoutShell = document.getElementById("shell-popout-shell");
|
||||
var isPopout = {{ is_popout|yesno:"true,false" }};
|
||||
|
||||
function sizePopoutTerminal() {
|
||||
if (!isPopout || !popoutShell || !termContainer) {
|
||||
return;
|
||||
}
|
||||
var padding = 24;
|
||||
var height = Math.max(320, window.innerHeight - padding);
|
||||
popoutShell.style.height = height + "px";
|
||||
termContainer.style.height = (height - 8) + "px";
|
||||
}
|
||||
|
||||
function fitTerminal(term) {
|
||||
if (!termContainer || !term || !term._core || !term._core._renderService) {
|
||||
return;
|
||||
}
|
||||
var dims = term._core._renderService.dimensions;
|
||||
if (!dims || !dims.css || !dims.css.cell) {
|
||||
return;
|
||||
}
|
||||
var cellWidth = dims.css.cell.width || 9;
|
||||
var cellHeight = dims.css.cell.height || 18;
|
||||
if (!cellWidth || !cellHeight) {
|
||||
return;
|
||||
}
|
||||
var cols = Math.max(20, Math.floor(termContainer.clientWidth / cellWidth));
|
||||
var rows = Math.max(10, Math.floor(termContainer.clientHeight / cellHeight));
|
||||
term.resize(cols, rows);
|
||||
}
|
||||
|
||||
function setButtonState(isRunning) {
|
||||
if (!startButton) {
|
||||
return;
|
||||
}
|
||||
startButton.disabled = false;
|
||||
startButton.textContent = isRunning ? "Stop terminal" : "Start terminal";
|
||||
startButton.classList.toggle("bg-red-600", isRunning);
|
||||
startButton.classList.toggle("hover:bg-red-700", isRunning);
|
||||
startButton.classList.toggle("bg-purple-600", !isRunning);
|
||||
startButton.classList.toggle("hover:bg-purple-700", !isRunning);
|
||||
}
|
||||
|
||||
function stopTerminal() {
|
||||
if (activeSocket) {
|
||||
try {
|
||||
activeSocket.close();
|
||||
} catch (err) {
|
||||
// noop
|
||||
}
|
||||
}
|
||||
if (termContainer) {
|
||||
termContainer.dataset.started = "0";
|
||||
}
|
||||
activeSocket = null;
|
||||
activeTerm = null;
|
||||
setButtonState(false);
|
||||
}
|
||||
|
||||
function startTerminal() {
|
||||
if (!termContainer || !window.Terminal || termContainer.dataset.started === "1") {
|
||||
return;
|
||||
}
|
||||
termContainer.dataset.started = "1";
|
||||
if (startButton) {
|
||||
startButton.disabled = true;
|
||||
startButton.textContent = "Starting...";
|
||||
}
|
||||
var term = new window.Terminal({
|
||||
cursorBlink: true,
|
||||
fontFamily: "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace",
|
||||
@@ -239,12 +319,16 @@
|
||||
}
|
||||
});
|
||||
term.open(termContainer);
|
||||
term.write("Connecting...\r\n");
|
||||
setTimeout(function () {
|
||||
fitTerminal(term);
|
||||
}, 0);
|
||||
|
||||
var protocol = window.location.protocol === "https:" ? "wss" : "ws";
|
||||
var socketUrl = protocol + "://" + window.location.host + "/ws/servers/{{ server.id }}/shell/";
|
||||
var socket = new WebSocket(socketUrl);
|
||||
socket.binaryType = "arraybuffer";
|
||||
activeSocket = socket;
|
||||
activeTerm = term;
|
||||
|
||||
socket.onmessage = function (event) {
|
||||
if (typeof event.data === "string") {
|
||||
@@ -258,6 +342,9 @@
|
||||
|
||||
socket.onclose = function () {
|
||||
term.write("\r\nSession closed.\r\n");
|
||||
if (activeSocket === socket) {
|
||||
stopTerminal();
|
||||
}
|
||||
};
|
||||
|
||||
term.onData(function (data) {
|
||||
@@ -265,6 +352,37 @@
|
||||
socket.send(data);
|
||||
}
|
||||
});
|
||||
|
||||
setButtonState(true);
|
||||
|
||||
if (isPopout) {
|
||||
var onResize = function () {
|
||||
sizePopoutTerminal();
|
||||
fitTerminal(term);
|
||||
};
|
||||
window.addEventListener("resize", onResize);
|
||||
}
|
||||
}
|
||||
|
||||
if (termContainer && window.Terminal) {
|
||||
if (isPopout) {
|
||||
document.body.classList.add("popout-shell");
|
||||
sizePopoutTerminal();
|
||||
window.addEventListener("resize", sizePopoutTerminal);
|
||||
}
|
||||
if (startButton) {
|
||||
startButton.addEventListener("click", function () {
|
||||
if (termContainer.dataset.started === "1") {
|
||||
stopTerminal();
|
||||
return;
|
||||
}
|
||||
startTerminal();
|
||||
});
|
||||
} else {
|
||||
startTerminal();
|
||||
}
|
||||
} else if (termContainer) {
|
||||
termContainer.textContent = "Terminal assets failed to load.";
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
Reference in New Issue
Block a user