From c13b2d2ac7c53a8249d9308896a191f26648e0fe Mon Sep 17 00:00:00 2001 From: anhefti Date: Wed, 31 Aug 2022 14:11:19 +0200 Subject: [PATCH] SEBWIN-477 implementation --- SafeExamBrowser.Client/ClientController.cs | 47 +++++++++++- .../Events/LockScreenRequestedEventHandler.cs | 14 ++++ .../IServerProxy.cs | 20 ++++++ .../SafeExamBrowser.Server.Contracts.csproj | 1 + SafeExamBrowser.Server/Data/Attributes.cs | 2 + SafeExamBrowser.Server/Data/Instructions.cs | 1 + SafeExamBrowser.Server/Parser.cs | 15 +++- SafeExamBrowser.Server/ServerProxy.cs | 72 +++++++++++++++++-- .../Windows/Data/LockScreenResult.cs | 6 ++ .../Windows/ILockScreen.cs | 4 ++ .../Windows/LockScreen.xaml.cs | 8 +++ .../Windows/LockScreen.xaml.cs | 8 +++ 12 files changed, 190 insertions(+), 8 deletions(-) create mode 100644 SafeExamBrowser.Server.Contracts/Events/LockScreenRequestedEventHandler.cs diff --git a/SafeExamBrowser.Client/ClientController.cs b/SafeExamBrowser.Client/ClientController.cs index 0a614582..473f5e21 100644 --- a/SafeExamBrowser.Client/ClientController.cs +++ b/SafeExamBrowser.Client/ClientController.cs @@ -68,6 +68,7 @@ namespace SafeExamBrowser.Client private IServerProxy Server => context.Server; private AppSettings Settings => context.Settings; + private ILockScreen lockScreen; private bool sessionLocked; internal ClientController( @@ -215,10 +216,25 @@ namespace SafeExamBrowser.Client if (Server != null) { + Server.LockScreenConfirmed += Server_LockScreenConfirmed; Server.TerminationRequested += Server_TerminationRequested; + Server.LockScreenRequested += Server_LockScreenRequested; } } + private void Server_LockScreenRequested(string message) + { + logger.Info("Received lock screen event from SEB Server."); + var title = text.Get(TextKey.LockScreen_Title); + ShowLockScreen(message, title, Enumerable.Empty()); + } + + private void Server_LockScreenConfirmed() + { + logger.Info("Closing lock screen as requested by the server..."); + lockScreen?.Cancel(); + } + private void Taskbar_LoseFocusRequested(bool forward) { Browser.Focus(forward); @@ -260,7 +276,9 @@ namespace SafeExamBrowser.Client if (Server != null) { + Server.LockScreenConfirmed -= Server_LockScreenConfirmed; Server.TerminationRequested -= Server_TerminationRequested; + Server.LockScreenRequested -= Server_LockScreenRequested; } foreach (var activator in context.Activators.OfType()) @@ -769,18 +787,33 @@ namespace SafeExamBrowser.Client private LockScreenResult ShowLockScreen(string message, string title, IEnumerable options) { var hasQuitPassword = !string.IsNullOrEmpty(Settings.Security.QuitPasswordHash); - var lockScreen = uiFactory.CreateLockScreen(message, title, options); + lockScreen = uiFactory.CreateLockScreen(message, title, options); var result = default(LockScreenResult); logger.Info("Showing lock screen..."); PauseActivators(); lockScreen.Show(); + if (Settings.SessionMode == SessionMode.Server) + { + var response = Server.LockScreen(message); + + if (!response.Success) + { + logger.Error($"Failed to send lock screen notification to server! Message: {response.Message}."); + } + } + for (var unlocked = false; !unlocked;) { result = lockScreen.WaitForResult(); - if (hasQuitPassword) + if (result.Canceled) + { + logger.Info("The lock screen has been automaticaly canceled."); + unlocked = true; + } + else if (hasQuitPassword) { var passwordHash = hashAlgorithm.GenerateHashFor(result.Password); var isCorrect = Settings.Security.QuitPasswordHash.Equals(passwordHash, StringComparison.OrdinalIgnoreCase); @@ -807,6 +840,16 @@ namespace SafeExamBrowser.Client ResumeActivators(); logger.Info("Closed lock screen."); + if (Settings.SessionMode == SessionMode.Server) + { + var response = Server.ConfirmLockScreen(); + + if (!response.Success) + { + logger.Error($"Failed to send lock screen confirm notification to server! Message: {response.Message}."); + } + } + return result; } diff --git a/SafeExamBrowser.Server.Contracts/Events/LockScreenRequestedEventHandler.cs b/SafeExamBrowser.Server.Contracts/Events/LockScreenRequestedEventHandler.cs new file mode 100644 index 00000000..f8c4f3fd --- /dev/null +++ b/SafeExamBrowser.Server.Contracts/Events/LockScreenRequestedEventHandler.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace SafeExamBrowser.Server.Contracts.Events +{ + /// + /// Event handler used to handle a requested lock screen from SEB Server + /// + public delegate void LockScreenRequestedEventHandler(String message); + +} diff --git a/SafeExamBrowser.Server.Contracts/IServerProxy.cs b/SafeExamBrowser.Server.Contracts/IServerProxy.cs index 675a8c4b..a74807f7 100644 --- a/SafeExamBrowser.Server.Contracts/IServerProxy.cs +++ b/SafeExamBrowser.Server.Contracts/IServerProxy.cs @@ -24,6 +24,16 @@ namespace SafeExamBrowser.Server.Contracts /// event ServerEventHandler HandConfirmed; + /// + /// Event fired when the proxy receives a confirmation for a lock screen notification. + /// + event ServerEventHandler LockScreenConfirmed; + + /// + /// Event fired when proxy receives a lock screen instruction. + /// + event LockScreenRequestedEventHandler LockScreenRequested; + /// /// Event fired when the proxy receives new proctoring configuration values. /// @@ -79,6 +89,11 @@ namespace SafeExamBrowser.Server.Contracts /// ServerResponse LowerHand(); + /// + /// Sends a lock screen confirm notification to the server. + /// + ServerResponse ConfirmLockScreen(); + /// /// Sends the given user session identifier of a LMS and thus establishes a connection with the server. /// @@ -98,5 +113,10 @@ namespace SafeExamBrowser.Server.Contracts /// Sends a raise hand notification to the server. /// ServerResponse RaiseHand(string message = default(string)); + + /// + /// Sends a lock screen notification to the server. + /// + ServerResponse LockScreen(string message = default(string)); } } diff --git a/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj b/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj index 68bca326..c9cfdbd1 100644 --- a/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj +++ b/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj @@ -56,6 +56,7 @@ + diff --git a/SafeExamBrowser.Server/Data/Attributes.cs b/SafeExamBrowser.Server/Data/Attributes.cs index d7767be8..92f3d325 100644 --- a/SafeExamBrowser.Server/Data/Attributes.cs +++ b/SafeExamBrowser.Server/Data/Attributes.cs @@ -19,6 +19,8 @@ namespace SafeExamBrowser.Server.Data internal bool ReceiveVideo { get; set; } internal string Type { get; set; } + internal string Message { get; set; } + internal Attributes() { Instruction = new ProctoringInstructionEventArgs(); diff --git a/SafeExamBrowser.Server/Data/Instructions.cs b/SafeExamBrowser.Server/Data/Instructions.cs index e7087407..06d47804 100644 --- a/SafeExamBrowser.Server/Data/Instructions.cs +++ b/SafeExamBrowser.Server/Data/Instructions.cs @@ -14,5 +14,6 @@ namespace SafeExamBrowser.Server.Data internal const string PROCTORING = "SEB_PROCTORING"; internal const string PROCTORING_RECONFIGURATION = "SEB_RECONFIGURE_SETTINGS"; internal const string QUIT = "SEB_QUIT"; + internal const string LOCK_SCREEN = "SEB_FORCE_LOCK_SCREEN"; } } diff --git a/SafeExamBrowser.Server/Parser.cs b/SafeExamBrowser.Server/Parser.cs index 62e6cf0d..0db21942 100644 --- a/SafeExamBrowser.Server/Parser.cs +++ b/SafeExamBrowser.Server/Parser.cs @@ -180,7 +180,7 @@ namespace SafeExamBrowser.Server instructionConfirmation = attributesJson["instruction-confirm"].Value(); } - attributes = ParseProctoringAttributes(attributesJson, instruction); + attributes = ParseAttributes(attributesJson, instruction); } } } @@ -210,7 +210,7 @@ namespace SafeExamBrowser.Server return oauth2Token != default; } - private Attributes ParseProctoringAttributes(JObject attributesJson, string instruction) + private Attributes ParseAttributes(JObject attributesJson, string instruction) { var attributes = new Attributes(); @@ -225,11 +225,22 @@ namespace SafeExamBrowser.Server case Instructions.PROCTORING_RECONFIGURATION: ParseReconfigurationInstruction(attributes, attributesJson); break; + case Instructions.LOCK_SCREEN: + ParseLockScreenInstruction(attributes, attributesJson); + break; } return attributes; } + private void ParseLockScreenInstruction(Attributes attributes, JObject attributesJson) + { + if (attributesJson.ContainsKey("message")) + { + attributes.Message = attributesJson["message"].Value(); + } + } + private void ParseNotificationConfirmation(Attributes attributes, JObject attributesJson) { if (attributesJson.ContainsKey("id")) diff --git a/SafeExamBrowser.Server/ServerProxy.cs b/SafeExamBrowser.Server/ServerProxy.cs index 5eba19f4..b02116f5 100644 --- a/SafeExamBrowser.Server/ServerProxy.cs +++ b/SafeExamBrowser.Server/ServerProxy.cs @@ -54,16 +54,21 @@ namespace SafeExamBrowser.Server private bool connectedToPowergrid; private int currentWlanValue; private string examId; - private int handNotificationId; + private int notificationId; + private int currentRaisHandId; + private int currentLockScreenId; + private HttpClient httpClient; private string oauth2Token; private int pingNumber; private ServerSettings settings; public event ServerEventHandler HandConfirmed; + public event ServerEventHandler LockScreenConfirmed; public event ProctoringConfigurationReceivedEventHandler ProctoringConfigurationReceived; public event ProctoringInstructionReceivedEventHandler ProctoringInstructionReceived; public event TerminationRequestedEventHandler TerminationRequested; + public event LockScreenRequestedEventHandler LockScreenRequested; public ServerProxy( AppConfig appConfig, @@ -242,7 +247,7 @@ namespace SafeExamBrowser.Server { ["type"] = "NOTIFICATION_CONFIRMED", ["timestamp"] = DateTime.Now.ToUnixTimestamp(), - ["numericValue"] = handNotificationId, + ["numericValue"] = currentRaisHandId, }; var content = json.ToString(); var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token); @@ -259,6 +264,32 @@ namespace SafeExamBrowser.Server return new ServerResponse(success, response.ToLogString()); } + public ServerResponse ConfirmLockScreen() + { + var authorization = ("Authorization", $"Bearer {oauth2Token}"); + var contentType = "application/json;charset=UTF-8"; + var token = ("SEBConnectionToken", connectionToken); + var json = new JObject + { + ["type"] = "NOTIFICATION_CONFIRMED", + ["timestamp"] = DateTime.Now.ToUnixTimestamp(), + ["numericValue"] = currentLockScreenId, + }; + var content = json.ToString(); + var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token); + + if (success) + { + logger.Info("Successfully sent lock screen confirm notification."); + } + else + { + logger.Error("Failed to send lock screen confirm notification!"); + } + + return new ServerResponse(success, response.ToLogString()); + } + public void Notify(ILogContent content) { logContent.Enqueue(content); @@ -273,7 +304,7 @@ namespace SafeExamBrowser.Server { ["type"] = "NOTIFICATION", ["timestamp"] = DateTime.Now.ToUnixTimestamp(), - ["numericValue"] = ++handNotificationId, + ["numericValue"] = currentRaisHandId = ++notificationId, ["text"] = $" {message}" }; var content = json.ToString(); @@ -291,6 +322,33 @@ namespace SafeExamBrowser.Server return new ServerResponse(success, response.ToLogString()); } + public ServerResponse LockScreen(string message = null) + { + var authorization = ("Authorization", $"Bearer {oauth2Token}"); + var contentType = "application/json;charset=UTF-8"; + var token = ("SEBConnectionToken", connectionToken); + var json = new JObject + { + ["type"] = "NOTIFICATION", + ["timestamp"] = DateTime.Now.ToUnixTimestamp(), + ["numericValue"] = currentLockScreenId = ++notificationId, + ["text"] = $" {message}" + }; + var content = json.ToString(); + var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token); + + if (success) + { + logger.Info("Successfully sent lock screen notification."); + } + else + { + logger.Error("Failed to send lock screen notification!"); + } + + return new ServerResponse(success, response.ToLogString()); + } + public ServerResponse SendSessionIdentifier(string identifier) { var authorization = ("Authorization", $"Bearer {oauth2Token}"); @@ -414,9 +472,12 @@ namespace SafeExamBrowser.Server { switch (instruction) { - case Instructions.NOTIFICATION_CONFIRM when attributes.Type == "raisehand" && attributes.Id == handNotificationId: + case Instructions.NOTIFICATION_CONFIRM when attributes.Type == "raisehand": Task.Run(() => HandConfirmed?.Invoke()); break; + case Instructions.NOTIFICATION_CONFIRM when attributes.Type == "lockscreen": + Task.Run(() => LockScreenConfirmed?.Invoke()); + break; case Instructions.PROCTORING: Task.Run(() => ProctoringInstructionReceived?.Invoke(attributes.Instruction)); break; @@ -426,6 +487,9 @@ namespace SafeExamBrowser.Server case Instructions.QUIT: Task.Run(() => TerminationRequested?.Invoke()); break; + case Instructions.LOCK_SCREEN: + Task.Run(() => LockScreenRequested?.Invoke(attributes.Message)); + break; } if (instructionConfirmation != default) diff --git a/SafeExamBrowser.UserInterface.Contracts/Windows/Data/LockScreenResult.cs b/SafeExamBrowser.UserInterface.Contracts/Windows/Data/LockScreenResult.cs index 406d0d8e..867f282d 100644 --- a/SafeExamBrowser.UserInterface.Contracts/Windows/Data/LockScreenResult.cs +++ b/SafeExamBrowser.UserInterface.Contracts/Windows/Data/LockScreenResult.cs @@ -15,6 +15,12 @@ namespace SafeExamBrowser.UserInterface.Contracts.Windows.Data /// public class LockScreenResult { + + /// + /// This is been set if the lock screen was canceled from another process (E.g.: from SEB Server instruction) + /// + public bool Canceled { get; set; } + /// /// The identifier of the option selected by the user, if available. /// diff --git a/SafeExamBrowser.UserInterface.Contracts/Windows/ILockScreen.cs b/SafeExamBrowser.UserInterface.Contracts/Windows/ILockScreen.cs index 68f10b93..9d34fe55 100644 --- a/SafeExamBrowser.UserInterface.Contracts/Windows/ILockScreen.cs +++ b/SafeExamBrowser.UserInterface.Contracts/Windows/ILockScreen.cs @@ -19,5 +19,9 @@ namespace SafeExamBrowser.UserInterface.Contracts.Windows /// Waits for the user to provide the required input to unlock the application. /// LockScreenResult WaitForResult(); + /// + /// This cancel a waiting thread for LockScreenResult and force the lock screen to close + /// + void Cancel(); } } diff --git a/SafeExamBrowser.UserInterface.Desktop/Windows/LockScreen.xaml.cs b/SafeExamBrowser.UserInterface.Desktop/Windows/LockScreen.xaml.cs index 0e2f6b88..a7d2a8fd 100644 --- a/SafeExamBrowser.UserInterface.Desktop/Windows/LockScreen.xaml.cs +++ b/SafeExamBrowser.UserInterface.Desktop/Windows/LockScreen.xaml.cs @@ -27,6 +27,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows { private readonly AutoResetEvent autoResetEvent; private readonly IText text; + private bool canceled = false; private IList windows; @@ -85,6 +86,7 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows } }); + result.Canceled = canceled; return result; } @@ -172,5 +174,11 @@ namespace SafeExamBrowser.UserInterface.Desktop.Windows autoResetEvent.Set(); } } + + public void Cancel() + { + canceled = true; + autoResetEvent.Set(); + } } } diff --git a/SafeExamBrowser.UserInterface.Mobile/Windows/LockScreen.xaml.cs b/SafeExamBrowser.UserInterface.Mobile/Windows/LockScreen.xaml.cs index 728487e5..50c29de0 100644 --- a/SafeExamBrowser.UserInterface.Mobile/Windows/LockScreen.xaml.cs +++ b/SafeExamBrowser.UserInterface.Mobile/Windows/LockScreen.xaml.cs @@ -27,6 +27,7 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows { private readonly AutoResetEvent autoResetEvent; private readonly IText text; + private bool canceled = false; private IList windows; @@ -85,6 +86,7 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows } }); + result.Canceled = canceled; return result; } @@ -172,5 +174,11 @@ namespace SafeExamBrowser.UserInterface.Mobile.Windows autoResetEvent.Set(); } } + + public void Cancel() + { + canceled = true; + autoResetEvent.Set(); + } } }