Compare commits

..

4 Commits

Author SHA1 Message Date
Marc Brooks 1f7c5c94d8
feat(ui): Add Ctrl+Alt+Del to the action bar (#498)
Since this is the sort of thing we do all the time, make it one-click away
2025-05-25 14:19:42 +02:00
Marc Brooks 55d7f22c47
chore(ui): Removed unused DeviceSettingState (#496)
Now that we don't do any mouse/trackpad sensitivity settings, this whole interface is unused.
2025-05-25 14:19:31 +02:00
Aveline a28676cd94
feat(websecure): add support for ed25519 certificates (#513) 2025-05-25 11:09:58 +02:00
ariedel87 2ec061b3a8
feat(Keyboard): Hide Pressed Keys (#518) 2025-05-25 11:09:48 +02:00
8 changed files with 187 additions and 28 deletions

View File

@ -0,0 +1,55 @@
package websecure
import (
"os"
"testing"
)
var (
fixtureEd25519Certificate = `-----BEGIN CERTIFICATE-----
MIIBQDCB86ADAgECAhQdB4qB6dV0/u1lwhJofQgkmjjV1zAFBgMrZXAwLzELMAkG
A1UEBhMCREUxIDAeBgNVBAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMB4XDTI1
MDUyMzEyNTkyN1oXDTI3MDQyMzEyNTkyN1owLzELMAkGA1UEBhMCREUxIDAeBgNV
BAMMF2VkMjU1MTktdGVzdC5qZXRrdm0uY29tMCowBQYDK2VwAyEA9tLyoulJn7Ev
bf8kuD1ZGdA092773pCRjFEDKpXHonyjITAfMB0GA1UdDgQWBBRkmrVMfsLY57iy
r/0POP0S4QxCADAFBgMrZXADQQBfTRvqavLHDYQiKQTgbGod+Yn+fIq2lE584+1U
C4wh9peIJDFocLBEAYTQpEMKxa4s0AIRxD+a7aCS5oz0e/0I
-----END CERTIFICATE-----`
fixtureEd25519PrivateKey = `-----BEGIN PRIVATE KEY-----
MC4CAQAwBQYDK2VwBCIEIKV08xUsLRHBfMXqZwxVRzIbViOp8G7aQGjPvoRFjujB
-----END PRIVATE KEY-----`
certStore *CertStore
certSigner *SelfSigner
)
func TestMain(m *testing.M) {
tlsStorePath, err := os.MkdirTemp("", "jktls.*")
if err != nil {
defaultLogger.Fatal().Err(err).Msg("failed to create temp directory")
}
certStore = NewCertStore(tlsStorePath, nil)
certStore.LoadCertificates()
certSigner = NewSelfSigner(
certStore,
nil,
"ci.jetkvm.com",
"JetKVM",
"JetKVM",
"JetKVM",
)
m.Run()
os.RemoveAll(tlsStorePath)
}
func TestSaveEd25519Certificate(t *testing.T) {
err, _ := certStore.ValidateAndSaveCertificate("ed25519-test.jetkvm.com", fixtureEd25519Certificate, fixtureEd25519PrivateKey, true)
if err != nil {
t.Fatalf("failed to save certificate: %v", err)
}
}

View File

@ -2,6 +2,7 @@ package websecure
import (
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/tls"
@ -37,11 +38,15 @@ func keyToFile(cert *tls.Certificate, filename string) error {
if e != nil {
return fmt.Errorf("failed to marshal EC private key: %v", e)
}
keyBlock = pem.Block{
Type: "EC PRIVATE KEY",
Bytes: b,
}
case ed25519.PrivateKey:
keyBlock = pem.Block{
Type: "ED25519 PRIVATE KEY",
Bytes: k,
}
default:
return fmt.Errorf("unknown private key type: %T", k)
}

View File

@ -1,6 +1,6 @@
import { MdOutlineContentPasteGo } from "react-icons/md";
import { LuCable, LuHardDrive, LuMaximize, LuSettings, LuSignal } from "react-icons/lu";
import { FaKeyboard } from "react-icons/fa6";
import { FaKeyboard, FaLock} from "react-icons/fa6";
import { Popover, PopoverButton, PopoverPanel } from "@headlessui/react";
import { Fragment, useCallback, useRef } from "react";
import { CommandLineIcon } from "@heroicons/react/20/solid";
@ -19,6 +19,8 @@ import WakeOnLanModal from "@/components/popovers/WakeOnLan/Index";
import MountPopopover from "@/components/popovers/MountPopover";
import ExtensionPopover from "@/components/popovers/ExtensionPopover";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import useKeyboard from "@/hooks/useKeyboard";
import { keys, modifiers } from "@/keyboardMappings";
export default function Actionbar({
requestFullscreen,
@ -56,6 +58,8 @@ export default function Actionbar({
[setDisableFocusTrap],
);
const { sendKeyboardEvent, resetKeyboardState } = useKeyboard();
return (
<Container className="border-b border-b-slate-800/20 bg-white dark:border-b-slate-300/20 dark:bg-slate-900">
<div
@ -262,7 +266,23 @@ export default function Actionbar({
}}
/>
</div>
{useSettingsStore().actionBarCtrlAltDel && (
<div className="hidden lg:block">
<Button
size="XS"
theme="light"
text="Ctrl + Alt + Del"
LeadingIcon={FaLock}
onClick={() => {
sendKeyboardEvent(
[keys["Delete"]],
[modifiers["ControlLeft"], modifiers["AltLeft"]],
);
setTimeout(resetKeyboardState, 100);
}}
/>
</div>
)}
<div>
<Button
size="XS"

View File

@ -28,6 +28,7 @@ export default function InfoBar() {
const rpcDataChannel = useRTCStore(state => state.rpcDataChannel);
const settings = useSettingsStore();
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
useEffect(() => {
if (!rpcDataChannel) return;
@ -97,6 +98,7 @@ export default function InfoBar() {
</div>
)}
{showPressedKeys && (
<div className="flex items-center gap-x-1">
<span className="text-xs font-semibold">Keys:</span>
<h2 className="text-xs">
@ -110,6 +112,7 @@ export default function InfoBar() {
].join(", ")}
</h2>
</div>
)}
</div>
</div>
<div className="flex items-center divide-x first:divide-l divide-slate-800/20 dark:divide-slate-300/20">

View File

@ -308,8 +308,14 @@ interface SettingsState {
keyboardLayout: string;
setKeyboardLayout: (layout: string) => void;
actionBarCtrlAltDel: boolean;
setActionBarCtrlAltDel: (enabled: boolean) => void;
keyboardLedSync: KeyboardLedSync;
setKeyboardLedSync: (sync: KeyboardLedSync) => void;
showPressedKeys: boolean;
setShowPressedKeys: (show: boolean) => void;
}
export const useSettingsStore = create(
@ -342,8 +348,14 @@ export const useSettingsStore = create(
keyboardLayout: "en-US",
setKeyboardLayout: layout => set({ keyboardLayout: layout }),
actionBarCtrlAltDel: false,
setActionBarCtrlAltDel: enabled => set({ actionBarCtrlAltDel: enabled }),
keyboardLedSync: "auto",
setKeyboardLedSync: sync => set({ keyboardLedSync: sync }),
showPressedKeys: true,
setShowPressedKeys: show => set({ showPressedKeys: show }),
}),
{
name: "settings",
@ -352,17 +364,6 @@ export const useSettingsStore = create(
),
);
export interface DeviceSettingsState {
trackpadSensitivity: number;
mouseSensitivity: number;
clampMin: number;
clampMax: number;
blockDelay: number;
trackpadThreshold: number;
scrollSensitivity: "low" | "default" | "high";
setScrollSensitivity: (sensitivity: DeviceSettingsState["scrollSensitivity"]) => void;
}
export interface RemoteVirtualMediaState {
source: "WebRTC" | "HTTP" | "Storage" | null;
mode: "CDROM" | "Disk" | null;

View File

@ -0,0 +1,28 @@
import { SettingsItem } from "./devices.$id.settings";
import { Checkbox } from "@/components/Checkbox";
import { SettingsPageHeader } from "@/components/SettingsPageheader";
import { useSettingsStore } from "@/hooks/stores";
export default function SettingsCtrlAltDelRoute() {
const enableCtrlAltDel = useSettingsStore(state => state.actionBarCtrlAltDel);
const setEnableCtrlAltDel = useSettingsStore(state => state.setActionBarCtrlAltDel);
return (
<div className="space-y-4">
<SettingsPageHeader
title="Action Bar"
description="Customize the action bar of your JetKVM interface"
/>
<div className="space-y-4">
<SettingsItem title="Enable Ctrl-Alt-Del" description="Enable the Ctrl-Alt-Del key on the virtual keyboard">
<Checkbox
checked={enableCtrlAltDel}
onChange={e => setEnableCtrlAltDel(e.target.checked)}
/>
</SettingsItem>
</div>
</div>
);
}

View File

@ -1,9 +1,10 @@
import { useEffect } from "react";
import { useCallback, useEffect, useState } from "react";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { SettingsItem } from "@routes/devices.$id.settings";
import { BacklightSettings, useSettingsStore } from "@/hooks/stores";
import { useJsonRpc } from "@/hooks/useJsonRpc";
import Checkbox from "@components/Checkbox";
import { SelectMenuBasic } from "@components/SelectMenuBasic";
import { UsbDeviceSetting } from "@components/UsbDeviceSetting";
@ -11,6 +12,14 @@ import notifications from "../notifications";
import { UsbInfoSetting } from "../components/UsbInfoSetting";
import { FeatureFlag } from "../components/FeatureFlag";
export interface ActionBarConfig {
ctrlAltDel: boolean;
}
const defaultActionBarConfig: ActionBarConfig = {
ctrlAltDel: false,
};
export default function SettingsHardwareRoute() {
const [send] = useJsonRpc();
const settings = useSettingsStore();
@ -71,6 +80,18 @@ export default function SettingsHardwareRoute() {
});
}, [send, setBacklightSettings]);
const [actionBarConfig, setActionBarConfig] = useState<ActionBarConfig>(defaultActionBarConfig);
const onActionBarItemChange = useCallback(
(key: keyof ActionBarConfig) => (e: React.ChangeEvent<HTMLInputElement>) => {
setActionBarConfig(prev => ({
...prev,
[key]: e.target.checked,
}));
},
[],
);
return (
<div className="space-y-4">
<SettingsPageHeader
@ -116,6 +137,15 @@ export default function SettingsHardwareRoute() {
}}
/>
</SettingsItem>
<SettingsItem
title="Enable Ctrl+Alt+Del Action Bar"
description="Enable or disable the action bar action for sending a Ctrl+Alt+Del to the host"
>
<Checkbox
checked={actionBarConfig.ctrlAltDel}
onChange={onActionBarItemChange("ctrlAltDel")}
/>
</SettingsItem>
{settings.backlightSettings.max_brightness != 0 && (
<>
<SettingsItem

View File

@ -5,6 +5,7 @@ import { useJsonRpc } from "@/hooks/useJsonRpc";
import notifications from "@/notifications";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { layouts } from "@/keyboardLayouts";
import { Checkbox } from "@/components/Checkbox";
import { SelectMenuBasic } from "../components/SelectMenuBasic";
@ -13,12 +14,16 @@ import { SettingsItem } from "./devices.$id.settings";
export default function SettingsKeyboardRoute() {
const keyboardLayout = useSettingsStore(state => state.keyboardLayout);
const keyboardLedSync = useSettingsStore(state => state.keyboardLedSync);
const showPressedKeys = useSettingsStore(state => state.showPressedKeys);
const setKeyboardLayout = useSettingsStore(
state => state.setKeyboardLayout,
);
const setKeyboardLedSync = useSettingsStore(
state => state.setKeyboardLedSync,
);
const setShowPressedKeys = useSettingsStore(
state => state.setShowPressedKeys,
);
// this ensures we always get the original en-US if it hasn't been set yet
const safeKeyboardLayout = useMemo(() => {
@ -102,6 +107,18 @@ export default function SettingsKeyboardRoute() {
/>
</SettingsItem>
</div>
<div className="space-y-4">
<SettingsItem
title="Show Pressed Keys"
description="Display currently pressed keys in the status bar"
>
<Checkbox
checked={showPressedKeys}
onChange={e => setShowPressedKeys(e.target.checked)}
/>
</SettingsItem>
</div>
</div>
);
}