/* * Copyright (c) 2024 ETH Zürich, IT Services * * This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Threading; using SafeExamBrowser.WindowsApi.Constants; using SafeExamBrowser.WindowsApi.Contracts; using SafeExamBrowser.WindowsApi.Contracts.Events; using SafeExamBrowser.WindowsApi.Hooks; using SafeExamBrowser.WindowsApi.Types; namespace SafeExamBrowser.WindowsApi { public class NativeMethods : INativeMethods { private readonly ConcurrentDictionary KeyboardHooks = new ConcurrentDictionary(); private readonly ConcurrentDictionary MouseHooks = new ConcurrentDictionary(); private readonly ConcurrentDictionary SystemHooks = new ConcurrentDictionary(); /// /// Upon finalization, unregister all active system events and hooks... /// ~NativeMethods() { foreach (var hook in SystemHooks.Values) { hook.Detach(); } foreach (var hook in KeyboardHooks.Values) { hook.Detach(); } foreach (var hook in MouseHooks.Values) { hook.Detach(); } } public void ActivateWindow(IntPtr handle) { var placement = new WINDOWPLACEMENT(); User32.BringWindowToTop(handle); User32.SetForegroundWindow(handle); placement.length = Marshal.SizeOf(placement); User32.GetWindowPlacement(handle, ref placement); if (placement.showCmd == (int) ShowWindowCommand.ShowMinimized) { User32.ShowWindowAsync(handle, (int) ShowWindowCommand.Restore); } } public void DeregisterKeyboardHook(Guid hookId) { var hook = KeyboardHooks.Values.FirstOrDefault(h => h.Id == hookId); if (hook != default) { var success = hook.Detach(); if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } KeyboardHooks.TryRemove(hookId, out _); } } public void DeregisterMouseHook(Guid hookId) { var hook = MouseHooks.Values.FirstOrDefault(h => h.Id == hookId); if (hook != default) { var success = hook.Detach(); if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } MouseHooks.TryRemove(hookId, out _); } } public void DeregisterSystemEventHook(Guid hookId) { var hook = SystemHooks.Values.FirstOrDefault(h => h.Id == hookId); if (hook != default) { var success = hook.Detach(); if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } SystemHooks.TryRemove(hookId, out _); } } public bool DisableStickyKeys() { var success = TryGetStickyKeys(out var state); if (success) { (state as StickyKeysState).Flags &= ~StickyKeysFlags.AVAILABLE; (state as StickyKeysState).Flags &= ~StickyKeysFlags.HOTKEYACTIVE; (state as StickyKeysState).Flags &= ~StickyKeysFlags.ON; success = TrySetStickyKeys(state); } return success; } public void EmptyClipboard() { var success = true; success &= User32.OpenClipboard(IntPtr.Zero); success &= User32.EmptyClipboard(); success &= User32.CloseClipboard(); if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } public bool EnableStickyKeys() { var success = TryGetStickyKeys(out var state); if (success) { (state as StickyKeysState).Flags |= StickyKeysFlags.AVAILABLE; (state as StickyKeysState).Flags |= StickyKeysFlags.HOTKEYACTIVE; (state as StickyKeysState).Flags |= StickyKeysFlags.ON; success = TrySetStickyKeys(state); } return success; } public (int x, int y) GetCursorPosition() { var position = new POINT(); User32.GetCursorPos(ref position); return (position.X, position.Y); } public IEnumerable GetOpenWindows() { var windows = new List(); bool EnumWindows(IntPtr hWnd, IntPtr lParam) { if (hWnd != GetShellWindowHandle() && User32.IsWindowVisible(hWnd) && User32.GetWindowTextLength(hWnd) > 0) { windows.Add(hWnd); } return true; } var success = User32.EnumWindows(EnumWindows, IntPtr.Zero); if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } return windows; } public uint GetProcessIdFor(IntPtr window) { User32.GetWindowThreadProcessId(window, out var processId); return processId; } public IntPtr GetShellWindowHandle() { return User32.FindWindow("Shell_TrayWnd", default); } public uint GetShellProcessId() { var handle = GetShellWindowHandle(); var threadId = User32.GetWindowThreadProcessId(handle, out var processId); return processId; } public string GetWallpaperPath() { const int MAX_PATH = 260; var buffer = new String('\0', MAX_PATH); var success = User32.SystemParametersInfo(SPI.GETDESKWALLPAPER, buffer.Length, buffer, SPIF.NONE); if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } var path = buffer.Substring(0, buffer.IndexOf('\0')); return path; } public IntPtr GetWindowIcon(IntPtr window) { var icon = User32.SendMessage(window, Constant.WM_GETICON, new IntPtr(Constant.ICON_BIG), IntPtr.Zero); if (icon == IntPtr.Zero) { icon = User32.SendMessage(window, Constant.WM_GETICON, new IntPtr(Constant.ICON_SMALL), IntPtr.Zero); } if (icon == IntPtr.Zero) { icon = User32.SendMessage(window, Constant.WM_GETICON, new IntPtr(Constant.ICON_SMALL2), IntPtr.Zero); } return icon; } public string GetWindowTitle(IntPtr window) { var length = User32.GetWindowTextLength(window); if (length > 0) { var builder = new StringBuilder(length); User32.GetWindowText(window, builder, length + 1); return builder.ToString(); } return string.Empty; } public IBounds GetWorkingArea() { var workingArea = new RECT(); var success = User32.SystemParametersInfo(SPI.GETWORKAREA, 0, ref workingArea, SPIF.NONE); if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } return workingArea.ToBounds(); } public bool HasInternetConnection() { return WinInet.InternetGetConnectedState(out _, 0); } public bool HideWindow(IntPtr window) { return User32.ShowWindow(window, (int) ShowWindowCommand.Hide); } public void MinimizeAllOpenWindows() { var handle = GetShellWindowHandle(); User32.SendMessage(handle, Constant.WM_COMMAND, (IntPtr) Constant.MIN_ALL, IntPtr.Zero); } public void PostCloseMessageToShell() { // NOTE: The close message 0x5B4 posted to the shell is undocumented and not officially supported: // https://stackoverflow.com/questions/5689904/gracefully-exit-explorer-programmatically/5705965#5705965 var handle = GetShellWindowHandle(); var success = User32.PostMessage(handle, 0x5B4, IntPtr.Zero, IntPtr.Zero); if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } public Guid RegisterKeyboardHook(KeyboardHookCallback callback) { var hookId = default(Guid); var hookReadyEvent = new AutoResetEvent(false); var hookThread = new Thread(() => { var hook = new KeyboardHook(callback); var sleepEvent = new AutoResetEvent(false); hook.Attach(); hookId = hook.Id; KeyboardHooks[hookId] = hook; hookReadyEvent.Set(); while (true) { sleepEvent.WaitOne(); } }); hookThread.SetApartmentState(ApartmentState.STA); hookThread.IsBackground = true; hookThread.Start(); hookReadyEvent.WaitOne(); return hookId; } public Guid RegisterMouseHook(MouseHookCallback callback) { var hookId = default(Guid); var hookReadyEvent = new AutoResetEvent(false); var hookThread = new Thread(() => { var hook = new MouseHook(callback); var sleepEvent = new AutoResetEvent(false); hook.Attach(); hookId = hook.Id; MouseHooks[hookId] = hook; hookReadyEvent.Set(); while (true) { sleepEvent.WaitOne(); } }); hookThread.SetApartmentState(ApartmentState.STA); hookThread.IsBackground = true; hookThread.Start(); hookReadyEvent.WaitOne(); return hookId; } public Guid RegisterSystemCaptureStartEvent(SystemEventCallback callback) { return RegisterSystemEvent(callback, Constant.EVENT_SYSTEM_CAPTURESTART); } public Guid RegisterSystemForegroundEvent(SystemEventCallback callback) { return RegisterSystemEvent(callback, Constant.EVENT_SYSTEM_FOREGROUND); } internal Guid RegisterSystemEvent(SystemEventCallback callback, uint eventId) { var hookId = default(Guid); var hookReadyEvent = new AutoResetEvent(false); var hookThread = new Thread(() => { var hook = new SystemHook(callback, eventId); hook.Attach(); hookId = hook.Id; SystemHooks[hookId] = hook; hookReadyEvent.Set(); hook.AwaitDetach(); }); hookThread.SetApartmentState(ApartmentState.STA); hookThread.IsBackground = true; hookThread.Start(); hookReadyEvent.WaitOne(); return hookId; } public void RemoveWallpaper() { SetWallpaper(string.Empty); } public void RestoreWindow(IntPtr window) { User32.ShowWindow(window, (int) ShowWindowCommand.Restore); } public bool ResumeThread(int threadId) { const int FAILURE = -1; var handle = Kernel32.OpenThread(ThreadAccess.SUSPEND_RESUME, false, (uint) threadId); if (handle == IntPtr.Zero) { return false; } try { var result = Kernel32.ResumeThread(handle); var success = result != FAILURE; return success; } finally { Kernel32.CloseHandle(handle); } } public void SendCloseMessageTo(IntPtr window) { User32.SendMessage(window, Constant.WM_SYSCOMMAND, (IntPtr) SystemCommand.CLOSE, IntPtr.Zero); } public void SetAlwaysOnState(bool display = true, bool system = true) { if (display || system) { var state = EXECUTION_STATE.CONTINUOUS; state |= display ? EXECUTION_STATE.DISPLAY_REQUIRED : 0x0; state |= system ? EXECUTION_STATE.SYSTEM_REQUIRED : 0x0; Kernel32.SetThreadExecutionState(state); } } public void SetWallpaper(string filePath) { var success = User32.SystemParametersInfo(SPI.SETDESKWALLPAPER, 0, filePath, SPIF.NONE); if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } public void SetWorkingArea(IBounds bounds) { var workingArea = new RECT { Left = bounds.Left, Top = bounds.Top, Right = bounds.Right, Bottom = bounds.Bottom }; var success = User32.SystemParametersInfo(SPI.SETWORKAREA, 0, ref workingArea, SPIF.NONE); if (!success) { throw new Win32Exception(Marshal.GetLastWin32Error()); } } public bool SuspendThread(int threadId) { const int FAILURE = -1; var handle = Kernel32.OpenThread(ThreadAccess.SUSPEND_RESUME, false, (uint) threadId); if (handle == IntPtr.Zero) { return false; } try { var result = Kernel32.SuspendThread(handle); var success = result != FAILURE; return success; } finally { Kernel32.CloseHandle(handle); } } public bool TryGetStickyKeys(out IStickyKeysState state) { var stickyKeys = new STICKYKEYS(); state = default; stickyKeys.cbSize = Marshal.SizeOf(typeof(STICKYKEYS)); var success = User32.SystemParametersInfo(SPI.GETSTICKYKEYS, stickyKeys.cbSize, ref stickyKeys, SPIF.NONE); if (success) { state = new StickyKeysState { Flags = stickyKeys.dwFlags, IsAvailable = stickyKeys.dwFlags.HasFlag(StickyKeysFlags.AVAILABLE), IsEnabled = stickyKeys.dwFlags.HasFlag(StickyKeysFlags.ON), IsHotkeyActive = stickyKeys.dwFlags.HasFlag(StickyKeysFlags.HOTKEYACTIVE) }; } return success; } public bool TrySetStickyKeys(IStickyKeysState state) { var success = false; var stickyKeys = new STICKYKEYS(); if (state is StickyKeysState stateWithFlags) { stickyKeys.cbSize = Marshal.SizeOf(typeof(STICKYKEYS)); stickyKeys.dwFlags = stateWithFlags.Flags; success = User32.SystemParametersInfo(SPI.SETSTICKYKEYS, stickyKeys.cbSize, ref stickyKeys, SPIF.NONE); } else { success = state.IsEnabled ? EnableStickyKeys() : DisableStickyKeys(); } return success; } } }