diff --git a/internal/websecure/store.go b/internal/websecure/store.go index 7da2deea0..b70dc3b31 100644 --- a/internal/websecure/store.go +++ b/internal/websecure/store.go @@ -144,6 +144,51 @@ func (s *CertStore) ValidateAndSaveCertificate(hostname string, cert string, key return nil, nil } +// GetCertificate returns the certificate for the given hostname +// returns nil if the certificate is not found +func (s *CertStore) GetCertificate(hostname string) *tls.Certificate { + s.certLock.Lock() + defer s.certLock.Unlock() + + return s.certificates[hostname] +} + +// ValidateAndSaveCertificate validates the certificate and saves it to the store +// returns are: +// - error: if the certificate is invalid or if there's any error during saving the certificate +// - error: if there's any warning or error during saving the certificate +func (s *CertStore) ValidateAndSaveCertificate(hostname string, cert string, key string, ignoreWarning bool) (error, error) { + tlsCert, err := tls.X509KeyPair([]byte(cert), []byte(key)) + if err != nil { + return fmt.Errorf("Failed to parse certificate: %w", err), nil + } + + // this can be skipped as current implementation supports one custom certificate only + if tlsCert.Leaf != nil { + // add recover to avoid panic + defer func() { + if r := recover(); r != nil { + s.log.Errorf("Failed to verify hostname: %v", r) + } + }() + + if err = tlsCert.Leaf.VerifyHostname(hostname); err != nil { + if !ignoreWarning { + return nil, fmt.Errorf("Certificate does not match hostname: %w", err) + } + s.log.Warnf("Certificate does not match hostname: %v", err) + } + } + + s.certLock.Lock() + s.certificates[hostname] = &tlsCert + s.certLock.Unlock() + + s.saveCertificate(hostname) + + return nil, nil +} + func (s *CertStore) saveCertificate(hostname string) { // check if certificate already exists tlsCert := s.certificates[hostname] diff --git a/ui/src/components/WebRTCVideo.tsx b/ui/src/components/WebRTCVideo.tsx index be6989988..2cb028dbb 100644 --- a/ui/src/components/WebRTCVideo.tsx +++ b/ui/src/components/WebRTCVideo.tsx @@ -6,6 +6,7 @@ import { useMouseStore, useRTCStore, useSettingsStore, + useUiStore, useVideoStore, } from "@/hooks/stores"; import { keys, modifiers } from "@/keyboardMappings"; @@ -17,6 +18,7 @@ import MacroBar from "@/components/MacroBar"; import InfoBar from "@components/InfoBar"; import useKeyboard from "@/hooks/useKeyboard"; import { useJsonRpc } from "@/hooks/useJsonRpc"; +import notifications from "../notifications"; import { HDMIErrorOverlay, @@ -61,6 +63,7 @@ export default function WebRTCVideo() { // Misc states and hooks const [blockWheelEvent, setBlockWheelEvent] = useState(false); + const disableVideoFocusTrap = useUiStore(state => state.disableVideoFocusTrap); const [send] = useJsonRpc(); // Video-related @@ -97,6 +100,40 @@ export default function WebRTCVideo() { [setVideoClientSize, updateVideoSizeStore, setVideoSize], ); + const checkNavigatorPermissions = async (permissionName: string) => { + const name = permissionName as PermissionName; + const { state } = await navigator.permissions.query({ name }); + return state === "granted"; + } + + const requestPointerLock = useCallback(async () => { + if (document.pointerLockElement) return; + + const isPointerLockGranted = await checkNavigatorPermissions("pointer-lock"); + if (isPointerLockGranted && settings.mouseMode === "relative") { + notifications.success("Pointer lock enabled, to exit it, press the escape key for a few seconds"); + videoElm.current?.requestPointerLock(); + } + }, [settings.mouseMode]); + + const requestFullscreen = useCallback(async () => { + videoElm.current?.requestFullscreen({ + navigationUI: "show", + }); + + // we do not care about pointer lock if it's for fullscreen + await requestPointerLock(); + + const isKeyboardLockGranted = await checkNavigatorPermissions("keyboard-lock"); + if (isKeyboardLockGranted) { + if ('keyboard' in navigator) { + // @ts-ignore + await navigator.keyboard.lock(); + } + } + + }, [disableVideoFocusTrap]); + // Mouse-related const calcDelta = (pos: number) => (Math.abs(pos) < 10 ? pos * 2 : pos); const sendRelMouseMovement = useCallback( @@ -525,6 +562,13 @@ export default function WebRTCVideo() { const containerElm = containerRef.current; if (!containerElm) return; + + const videoElmRefValue = videoElm.current; + if (videoElmRefValue) containerElm.addEventListener("click", () => { + if (disableVideoFocusTrap) return; + requestPointerLock(); + }, { signal }); + containerElm.addEventListener("mousemove", relMouseMoveHandler, { signal }); containerElm.addEventListener("pointerdown", relMouseMoveHandler, { signal }); containerElm.addEventListener("pointerup", relMouseMoveHandler, { signal }); @@ -541,7 +585,7 @@ export default function WebRTCVideo() { abortController.abort(); }; }, - [settings.mouseMode, relMouseMoveHandler, mouseWheelHandler], + [settings.mouseMode, relMouseMoveHandler, mouseWheelHandler, requestPointerLock, disableVideoFocusTrap] ); const hasNoAutoPlayPermissions = useMemo(() => { @@ -554,19 +598,12 @@ export default function WebRTCVideo() { return (