mirror of https://github.com/jetkvm/kvm.git
Compare commits
4 Commits
7e64a529f8
...
1f7c5c94d8
Author | SHA1 | Date |
---|---|---|
|
1f7c5c94d8 | |
|
55d7f22c47 | |
|
a28676cd94 | |
|
2ec061b3a8 |
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue