diff --git a/SafeExamBrowser.Client/ClientContext.cs b/SafeExamBrowser.Client/ClientContext.cs index a8cd77c6..5ce989a4 100644 --- a/SafeExamBrowser.Client/ClientContext.cs +++ b/SafeExamBrowser.Client/ClientContext.cs @@ -12,7 +12,6 @@ using SafeExamBrowser.Applications.Contracts; using SafeExamBrowser.Browser.Contracts; using SafeExamBrowser.Communication.Contracts.Hosts; using SafeExamBrowser.Configuration.Contracts; -using SafeExamBrowser.Proctoring.Contracts; using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Settings; using SafeExamBrowser.UserInterface.Contracts.Shell; @@ -49,11 +48,6 @@ namespace SafeExamBrowser.Client /// internal IClientHost ClientHost { get; set; } - /// - /// The proctoring controller to be used if the current session runs with remote proctoring. - /// - internal IProctoringController ProctoringController { get; set; } - /// /// The server proxy to be used if the current session mode is . /// diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index 3da0622a..d9790ccd 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -249,11 +249,9 @@ namespace SafeExamBrowser.Client private IOperation BuildProctoringOperation() { - var controller = new ProctoringController(context.AppConfig, new FileSystem(), ModuleLogger(nameof(ProctoringController)), text, uiFactory); + var controller = new ProctoringController(context.AppConfig, new FileSystem(), ModuleLogger(nameof(ProctoringController)), context.Server, text, uiFactory); var operation = new ProctoringOperation(actionCenter, context, controller, logger, controller, taskbar, uiFactory); - context.ProctoringController = controller; - return operation; } diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs index f5de851c..e35a10f7 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs @@ -17,21 +17,60 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping { switch (key) { + case Keys.Proctoring.JitsiMeet.AllowChat: + MapJitsiMeetAllowChat(settings, value); + break; + case Keys.Proctoring.JitsiMeet.AllowCloseCaptions: + MapJitsiMeetAllowCloseCaptions(settings, value); + break; + case Keys.Proctoring.JitsiMeet.AllowRaiseHand: + MapJitsiMeetAllowRaiseHands(settings, value); + break; + case Keys.Proctoring.JitsiMeet.AllowRecording: + MapJitsiMeetAllowRecording(settings, value); + break; + case Keys.Proctoring.JitsiMeet.AllowTileView: + MapJitsiMeetAllowTileView(settings, value); + break; + case Keys.Proctoring.JitsiMeet.AudioMuted: + MapJitsiMeetAudioMuted(settings, value); + break; + case Keys.Proctoring.JitsiMeet.AudioOnly: + MapJitsiMeetAudioOnly(settings, value); + break; case Keys.Proctoring.JitsiMeet.Enabled: MapJitsiMeetEnabled(settings, value); break; + case Keys.Proctoring.JitsiMeet.ReceiveAudio: + MapJitsiMeetReceiveAudio(settings, value); + break; + case Keys.Proctoring.JitsiMeet.ReceiveVideo: + MapJitsiMeetReceiveVideo(settings, value); + break; case Keys.Proctoring.JitsiMeet.RoomName: MapJitsiMeetRoomName(settings, value); break; + case Keys.Proctoring.JitsiMeet.SendAudio: + MapJitsiMeetSendAudio(settings, value); + break; + case Keys.Proctoring.JitsiMeet.SendVideo: + MapJitsiMeetSendVideo(settings, value); + break; case Keys.Proctoring.JitsiMeet.ServerUrl: MapJitsiMeetServerUrl(settings, value); break; + case Keys.Proctoring.JitsiMeet.ShowMeetingName: + MapJitsiMeetShowMeetingName(settings, value); + break; case Keys.Proctoring.JitsiMeet.Subject: MapJitsiMeetSubject(settings, value); break; case Keys.Proctoring.JitsiMeet.Token: MapJitsiMeetToken(settings, value); break; + case Keys.Proctoring.JitsiMeet.VideoMuted: + MapJitsiMeetVideoMuted(settings, value); + break; case Keys.Proctoring.ShowTaskbarNotification: MapShowTaskbarNotification(settings, value); break; @@ -44,6 +83,62 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping } } + private void MapJitsiMeetAllowChat(AppSettings settings, object value) + { + if (value is bool allow) + { + settings.Proctoring.JitsiMeet.AllowChat = allow; + } + } + + private void MapJitsiMeetAllowCloseCaptions(AppSettings settings, object value) + { + if (value is bool allow) + { + settings.Proctoring.JitsiMeet.AllowCloseCaptions = allow; + } + } + + private void MapJitsiMeetAllowRaiseHands(AppSettings settings, object value) + { + if (value is bool allow) + { + settings.Proctoring.JitsiMeet.AllowRaiseHand = allow; + } + } + + private void MapJitsiMeetAllowRecording(AppSettings settings, object value) + { + if (value is bool allow) + { + settings.Proctoring.JitsiMeet.AllowRecording = allow; + } + } + + private void MapJitsiMeetAllowTileView(AppSettings settings, object value) + { + if (value is bool allow) + { + settings.Proctoring.JitsiMeet.AllowTileView = allow; + } + } + + private void MapJitsiMeetAudioMuted(AppSettings settings, object value) + { + if (value is bool audioMuted) + { + settings.Proctoring.JitsiMeet.AudioMuted = audioMuted; + } + } + + private void MapJitsiMeetAudioOnly(AppSettings settings, object value) + { + if (value is bool audioOnly) + { + settings.Proctoring.JitsiMeet.AudioOnly = audioOnly; + } + } + private void MapJitsiMeetEnabled(AppSettings settings, object value) { if (value is bool enabled) @@ -52,6 +147,22 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping } } + private void MapJitsiMeetReceiveAudio(AppSettings settings, object value) + { + if (value is bool receive) + { + settings.Proctoring.JitsiMeet.ReceiveAudio = receive; + } + } + + private void MapJitsiMeetReceiveVideo(AppSettings settings, object value) + { + if (value is bool receive) + { + settings.Proctoring.JitsiMeet.ReceiveVideo = receive; + } + } + private void MapJitsiMeetRoomName(AppSettings settings, object value) { if (value is string name) @@ -60,6 +171,22 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping } } + private void MapJitsiMeetSendAudio(AppSettings settings, object value) + { + if (value is bool send) + { + settings.Proctoring.JitsiMeet.SendAudio = send; + } + } + + private void MapJitsiMeetSendVideo(AppSettings settings, object value) + { + if (value is bool send) + { + settings.Proctoring.JitsiMeet.SendVideo = send; + } + } + private void MapJitsiMeetServerUrl(AppSettings settings, object value) { if (value is string url) @@ -68,6 +195,14 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping } } + private void MapJitsiMeetShowMeetingName(AppSettings settings, object value) + { + if (value is bool show) + { + settings.Proctoring.JitsiMeet.ShowMeetingName = show; + } + } + private void MapJitsiMeetSubject(AppSettings settings, object value) { if (value is string subject) @@ -84,6 +219,14 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping } } + private void MapJitsiMeetVideoMuted(AppSettings settings, object value) + { + if (value is bool muted) + { + settings.Proctoring.JitsiMeet.VideoMuted = muted; + } + } + private void MapShowTaskbarNotification(AppSettings settings, object value) { if (value is bool show) diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs index ffb84b28..ce720ab9 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataProcessor.cs @@ -12,6 +12,7 @@ using System.IO; using System.Security.Cryptography; using SafeExamBrowser.Settings; using SafeExamBrowser.Settings.Applications; +using SafeExamBrowser.Settings.Proctoring; namespace SafeExamBrowser.Configuration.ConfigurationData { @@ -68,6 +69,11 @@ namespace SafeExamBrowser.Configuration.ConfigurationData private void InitializeProctoringSettings(AppSettings settings) { settings.Proctoring.Enabled = settings.Proctoring.JitsiMeet.Enabled || settings.Proctoring.Zoom.Enabled; + + if (!settings.Proctoring.JitsiMeet.ReceiveVideo) + { + settings.Proctoring.WindowVisibility = WindowVisibility.Hidden; + } } private void RemoveLegacyBrowsers(AppSettings settings) diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs index 72ac759f..ac35332a 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataValues.cs @@ -176,8 +176,23 @@ namespace SafeExamBrowser.Configuration.ConfigurationData settings.Mouse.AllowRightButton = true; settings.Proctoring.Enabled = false; + settings.Proctoring.JitsiMeet.AllowChat = false; + settings.Proctoring.JitsiMeet.AllowCloseCaptions = false; + settings.Proctoring.JitsiMeet.AllowRaiseHand = false; + settings.Proctoring.JitsiMeet.AllowRecording = false; + settings.Proctoring.JitsiMeet.AllowTileView = false; + settings.Proctoring.JitsiMeet.AudioMuted = true; + settings.Proctoring.JitsiMeet.AudioOnly = false; + settings.Proctoring.JitsiMeet.Enabled = false; + settings.Proctoring.JitsiMeet.ReceiveAudio = false; + settings.Proctoring.JitsiMeet.ReceiveVideo = false; + settings.Proctoring.JitsiMeet.SendAudio = true; + settings.Proctoring.JitsiMeet.SendVideo = true; + settings.Proctoring.JitsiMeet.ShowMeetingName = false; + settings.Proctoring.JitsiMeet.VideoMuted = false; settings.Proctoring.ShowTaskbarNotification = true; settings.Proctoring.WindowVisibility = WindowVisibility.Hidden; + settings.Proctoring.Zoom.Enabled = false; settings.Security.AllowApplicationLogAccess = false; settings.Security.AllowTermination = true; diff --git a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs index 1a64fcb5..ee843585 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs @@ -221,11 +221,24 @@ namespace SafeExamBrowser.Configuration.ConfigurationData internal static class JitsiMeet { + internal const string AllowChat = "jitsiMeetFeatureFlagChat"; + internal const string AllowCloseCaptions = "jitsiMeetFeatureFlagCloseCaptions"; + internal const string AllowRaiseHand = "jitsiMeetFeatureFlagRaiseHand"; + internal const string AllowRecording = "jitsiMeetFeatureFlagRecording"; + internal const string AllowTileView = "jitsiMeetFeatureFlagTileView"; + internal const string AudioMuted = "jitsiMeetAudioMuted"; + internal const string AudioOnly = "jitsiMeetAudioOnly"; internal const string Enabled = "jitsiMeetEnable"; + internal const string ReceiveAudio = "jitsiMeetReceiveAudio"; + internal const string ReceiveVideo = "jitsiMeetReceiveVideo"; internal const string RoomName = "jitsiMeetRoom"; + internal const string SendAudio = "jitsiMeetSendAudio"; + internal const string SendVideo = "jitsiMeetSendVideo"; internal const string ServerUrl = "jitsiMeetServerURL"; + internal const string ShowMeetingName = "jitsiMeetFeatureFlagDisplayMeetingName"; internal const string Subject = "jitsiMeetSubject"; internal const string Token = "jitsiMeetToken"; + internal const string VideoMuted = "jitsiMeetVideoMuted"; } internal static class Zoom diff --git a/SafeExamBrowser.Proctoring/JitsiMeet/index.html b/SafeExamBrowser.Proctoring/JitsiMeet/index.html index fa6faae1..94cef1e8 100644 --- a/SafeExamBrowser.Proctoring/JitsiMeet/index.html +++ b/SafeExamBrowser.Proctoring/JitsiMeet/index.html @@ -6,32 +6,33 @@
diff --git a/SafeExamBrowser.Proctoring/ProctoringController.cs b/SafeExamBrowser.Proctoring/ProctoringController.cs index 177ac351..00163424 100644 --- a/SafeExamBrowser.Proctoring/ProctoringController.cs +++ b/SafeExamBrowser.Proctoring/ProctoringController.cs @@ -9,6 +9,7 @@ using System; using System.IO; using System.Reflection; +using System.Windows; using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Core.Contracts.Notifications; using SafeExamBrowser.Core.Contracts.Notifications.Events; @@ -16,6 +17,7 @@ using SafeExamBrowser.Core.Contracts.Resources.Icons; using SafeExamBrowser.I18n.Contracts; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Proctoring.Contracts; +using SafeExamBrowser.Server.Contracts; using SafeExamBrowser.Settings.Proctoring; using SafeExamBrowser.SystemComponents.Contracts; using SafeExamBrowser.UserInterface.Contracts; @@ -28,23 +30,32 @@ namespace SafeExamBrowser.Proctoring private readonly AppConfig appConfig; private readonly IFileSystem fileSystem; private readonly IModuleLogger logger; + private readonly IServerProxy server; private readonly IText text; private readonly IUserInterfaceFactory uiFactory; private string filePath; - private IProctoringWindow window; + private ProctoringControl control; private ProctoringSettings settings; + private IProctoringWindow window; - public string Tooltip { get; } public IconResource IconResource { get; set; } + public string Tooltip { get; } public event NotificationChangedEventHandler NotificationChanged; - public ProctoringController(AppConfig appConfig, IFileSystem fileSystem, IModuleLogger logger, IText text, IUserInterfaceFactory uiFactory) + public ProctoringController( + AppConfig appConfig, + IFileSystem fileSystem, + IModuleLogger logger, + IServerProxy server, + IText text, + IUserInterfaceFactory uiFactory) { this.appConfig = appConfig; this.fileSystem = fileSystem; this.logger = logger; + this.server = server; this.text = text; this.uiFactory = uiFactory; @@ -60,56 +71,122 @@ namespace SafeExamBrowser.Proctoring } else if (settings.WindowVisibility == WindowVisibility.AllowToHide || settings.WindowVisibility == WindowVisibility.AllowToShow) { - window.Toggle(); + window?.Toggle(); } } public void Initialize(ProctoringSettings settings) { + var start = false; + this.settings = settings; - if (settings.JitsiMeet.Enabled || settings.Zoom.Enabled) + server.ProctoringConfigurationReceived += Server_ProctoringConfigurationReceived; + server.ProctoringInstructionReceived += Server_ProctoringInstructionReceived; + + if (settings.JitsiMeet.Enabled) { - var content = LoadContent(settings); - var control = new ProctoringControl(logger.CloneFor(nameof(ProctoringControl))); + start = !string.IsNullOrWhiteSpace(settings.JitsiMeet.RoomName); + start &= !string.IsNullOrWhiteSpace(settings.JitsiMeet.ServerUrl); + start &= !string.IsNullOrWhiteSpace(settings.JitsiMeet.Token); + } + else if (settings.Zoom.Enabled) + { + start = !string.IsNullOrWhiteSpace(settings.Zoom.ApiKey); + start &= !string.IsNullOrWhiteSpace(settings.Zoom.ApiSecret); + start &= settings.Zoom.MeetingNumber != default(int); + start &= !string.IsNullOrWhiteSpace(settings.Zoom.UserName); + } - filePath = Path.Combine(appConfig.TemporaryDirectory, $"{Path.GetRandomFileName()}_index.html"); - fileSystem.Save(content, filePath); + if (start) + { + StartProctoring(); + } + } - control.EnsureCoreWebView2Async().ContinueWith(_ => + public void Terminate() + { + StopProctoring(); + } + + private void Server_ProctoringInstructionReceived(string roomName, string serverUrl, string token) + { + logger.Info("Proctoring instruction received."); + + settings.JitsiMeet.RoomName = roomName; + settings.JitsiMeet.ServerUrl = serverUrl.Replace(Uri.UriSchemeHttps, "").Replace(Uri.UriSchemeHttp, "").Replace(Uri.SchemeDelimiter, ""); + settings.JitsiMeet.Token = token; + + if (window != default(IProctoringWindow)) + { + StopProctoring(); + } + + StartProctoring(); + } + + private void Server_ProctoringConfigurationReceived(bool enableChat, bool receiveAudio, bool receiveVideo) + { + logger.Info("Proctoring configuration received."); + + // TODO: How to set these things dynamically?!? + + control.ExecuteScriptAsync("api.executeCommand('toggleChat');"); + } + + private void StartProctoring() + { + Application.Current.Dispatcher.Invoke(() => + { + try { - control.Dispatcher.Invoke(() => control.CoreWebView2.Navigate(filePath)); - }); + var content = LoadContent(settings); - window = uiFactory.CreateProctoringWindow(control); + filePath = Path.Combine(appConfig.TemporaryDirectory, $"{Path.GetRandomFileName()}_index.html"); + fileSystem.Save(content, filePath); - if (settings.WindowVisibility == WindowVisibility.AllowToHide || settings.WindowVisibility == WindowVisibility.Visible) - { + control = new ProctoringControl(logger.CloneFor(nameof(ProctoringControl))); + control.EnsureCoreWebView2Async().ContinueWith(_ => + { + control.Dispatcher.Invoke(() => + { + control.CoreWebView2.Navigate(filePath); + }); + }); + + window = uiFactory.CreateProctoringWindow(control); window.SetTitle(settings.JitsiMeet.Enabled ? settings.JitsiMeet.Subject : settings.Zoom.UserName); window.Show(); + + if (settings.WindowVisibility == WindowVisibility.AllowToShow || settings.WindowVisibility == WindowVisibility.Hidden) + { + window.Hide(); + } + + IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Active.xaml") }; + NotificationChanged?.Invoke(); + + logger.Info($"Started proctoring with {(settings.JitsiMeet.Enabled ? "Jitsi Meet" : "Zoom")}."); } + catch (Exception e) + { + logger.Error($"Failed to start proctoring! Reason: {e.Message}", e); + } + }); + } - logger.Info($"Initialized proctoring with {(settings.JitsiMeet.Enabled ? "Jitsi Meet" : "Zoom")}."); - - IconResource = new XamlIconResource { Uri = new Uri("pack://application:,,,/SafeExamBrowser.UserInterface.Desktop;component/Images/ProctoringNotification_Active.xaml") }; - NotificationChanged?.Invoke(); - } - else + private void StopProctoring() + { + if (window != default(IProctoringWindow)) { - logger.Warn("Failed to initialize remote proctoring because no provider is enabled in the active configuration."); + window.Close(); + window = default(IProctoringWindow); + fileSystem.Delete(filePath); + + logger.Info("Stopped proctoring."); } } - void INotification.Terminate() - { - window?.Close(); - } - - void IProctoringController.Terminate() - { - fileSystem.Delete(filePath); - } - private string LoadContent(ProctoringSettings settings) { var provider = settings.JitsiMeet.Enabled ? "JitsiMeet" : "Zoom"; @@ -123,10 +200,18 @@ namespace SafeExamBrowser.Proctoring if (settings.JitsiMeet.Enabled) { + html = html.Replace("%%_ALLOW_CHAT_%%", settings.JitsiMeet.AllowChat ? "chat" : ""); + html = html.Replace("%%_ALLOW_CLOSED_CAPTIONS_%%", settings.JitsiMeet.AllowCloseCaptions ? "closedcaptions" : ""); + html = html.Replace("%%_ALLOW_RAISE_HAND_%%", settings.JitsiMeet.AllowRaiseHand ? "raisehand" : ""); + html = html.Replace("%%_ALLOW_RECORDING_%%", settings.JitsiMeet.AllowRecording ? "recording" : ""); + html = html.Replace("%%_ALLOW_TILE_VIEW", settings.JitsiMeet.AllowTileView ? "tileview" : ""); + html = html.Replace("'%_AUDIO_MUTED_%'", settings.JitsiMeet.AudioMuted && settings.WindowVisibility != WindowVisibility.Hidden ? "true" : "false"); + html = html.Replace("'%_AUDIO_ONLY_%'", settings.JitsiMeet.AudioOnly ? "true" : "false"); + html = html.Replace("%%_SUBJECT_%%", settings.JitsiMeet.ShowMeetingName ? settings.JitsiMeet.Subject : " "); html = html.Replace("%%_DOMAIN_%%", settings.JitsiMeet.ServerUrl); html = html.Replace("%%_ROOM_NAME_%%", settings.JitsiMeet.RoomName); - html = html.Replace("%%_SUBJECT_%%", settings.JitsiMeet.Subject); html = html.Replace("%%_TOKEN_%%", settings.JitsiMeet.Token); + html = html.Replace("'%_VIDEO_MUTED_%'", settings.JitsiMeet.VideoMuted && settings.WindowVisibility != WindowVisibility.Hidden ? "true" : "false"); } else if (settings.Zoom.Enabled) { diff --git a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj index 1954bffa..0cc620bf 100644 --- a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj +++ b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj @@ -94,6 +94,10 @@ {8e52bd1c-0540-4f16-b181-6665d43f7a7b} SafeExamBrowser.Proctoring.Contracts + + {db701e6f-bddc-4cec-b662-335a9dc11809} + SafeExamBrowser.Server.Contracts + {30b2d907-5861-4f39-abad-c4abf1b3470e} SafeExamBrowser.Settings diff --git a/SafeExamBrowser.Server.Contracts/Events/ProctoringConfigurationReceivedEventHandler.cs b/SafeExamBrowser.Server.Contracts/Events/ProctoringConfigurationReceivedEventHandler.cs new file mode 100644 index 00000000..55e26ee0 --- /dev/null +++ b/SafeExamBrowser.Server.Contracts/Events/ProctoringConfigurationReceivedEventHandler.cs @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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/. + */ + +namespace SafeExamBrowser.Server.Contracts.Events +{ + /// + /// Event handler used to indicate that proctoring configuration data has been received. + /// + public delegate void ProctoringConfigurationReceivedEventHandler(bool enableChat, bool receiveAudio, bool receiveVideo); +} diff --git a/SafeExamBrowser.Server.Contracts/Events/ProctoringInstructionReceivedEventHandler.cs b/SafeExamBrowser.Server.Contracts/Events/ProctoringInstructionReceivedEventHandler.cs new file mode 100644 index 00000000..8fd8e996 --- /dev/null +++ b/SafeExamBrowser.Server.Contracts/Events/ProctoringInstructionReceivedEventHandler.cs @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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/. + */ + +namespace SafeExamBrowser.Server.Contracts.Events +{ + /// + /// Event handler used to indicate that a proctoring instruction has been detected. + /// + public delegate void ProctoringInstructionReceivedEventHandler(string roomName, string serverUrl, string token); +} diff --git a/SafeExamBrowser.Server.Contracts/IServerProxy.cs b/SafeExamBrowser.Server.Contracts/IServerProxy.cs index 88f6d87c..5c4d8456 100644 --- a/SafeExamBrowser.Server.Contracts/IServerProxy.cs +++ b/SafeExamBrowser.Server.Contracts/IServerProxy.cs @@ -19,6 +19,16 @@ namespace SafeExamBrowser.Server.Contracts /// public interface IServerProxy { + /// + /// Event fired when the server receives new proctoring configuration values. + /// + event ProctoringConfigurationReceivedEventHandler ProctoringConfigurationReceived; + + /// + /// Event fired when the server receives a proctoring instruction. + /// + event ProctoringInstructionReceivedEventHandler ProctoringInstructionReceived; + /// /// Event fired when the server detects an instruction to terminate SEB. /// diff --git a/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj b/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj index 4bc8ec6b..475bf680 100644 --- a/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj +++ b/SafeExamBrowser.Server.Contracts/SafeExamBrowser.Server.Contracts.csproj @@ -56,6 +56,8 @@ + + diff --git a/SafeExamBrowser.Server/Data/Attributes.cs b/SafeExamBrowser.Server/Data/Attributes.cs new file mode 100644 index 00000000..9e735cb0 --- /dev/null +++ b/SafeExamBrowser.Server/Data/Attributes.cs @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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/. + */ + +namespace SafeExamBrowser.Server.Data +{ + internal class Attributes + { + public bool EnableChat { get; set; } + public bool ReceiveAudio { get; set; } + public bool ReceiveVideo { get; set; } + public string RoomName { get; set; } + public string ServerUrl { get; set; } + public string Token { get; set; } + } +} diff --git a/SafeExamBrowser.Server/Data/Instructions.cs b/SafeExamBrowser.Server/Data/Instructions.cs new file mode 100644 index 00000000..e135c5dc --- /dev/null +++ b/SafeExamBrowser.Server/Data/Instructions.cs @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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/. + */ + +namespace SafeExamBrowser.Server.Data +{ + internal sealed class Instructions + { + internal const string PROCTORING = "SEB_PROCTORING"; + internal const string PROCTORING_RECONFIGURATION = "SEB_RECONFIGURE_SETTINGS"; + internal const string QUIT = "SEB_QUIT"; + } +} diff --git a/SafeExamBrowser.Server/Extensions.cs b/SafeExamBrowser.Server/Extensions.cs new file mode 100644 index 00000000..7ad868ba --- /dev/null +++ b/SafeExamBrowser.Server/Extensions.cs @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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.Net.Http; +using SafeExamBrowser.Settings.Logging; + +namespace SafeExamBrowser.Server +{ + internal static class Extensions + { + internal static string ToLogType(this LogLevel severity) + { + switch (severity) + { + case LogLevel.Debug: + return "DEBUG_LOG"; + case LogLevel.Error: + return "ERROR_LOG"; + case LogLevel.Info: + return "INFO_LOG"; + case LogLevel.Warning: + return "WARN_LOG"; + } + + return "UNKNOWN"; + } + + internal static string ToLogString(this HttpResponseMessage response) + { + return $"{(int?) response?.StatusCode} {response?.StatusCode} {response?.ReasonPhrase}"; + } + + internal static long ToUnixTimestamp(this DateTime date) + { + return new DateTimeOffset(date).ToUnixTimeMilliseconds(); + } + } +} diff --git a/SafeExamBrowser.Server/FileSystem.cs b/SafeExamBrowser.Server/FileSystem.cs new file mode 100644 index 00000000..b8d97cef --- /dev/null +++ b/SafeExamBrowser.Server/FileSystem.cs @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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.IO; +using System.Net.Http; +using System.Threading.Tasks; +using SafeExamBrowser.Configuration.Contracts; +using SafeExamBrowser.Logging.Contracts; + +namespace SafeExamBrowser.Server +{ + internal class FileSystem + { + private readonly AppConfig appConfig; + private readonly ILogger logger; + + internal FileSystem(AppConfig appConfig, ILogger logger) + { + this.appConfig = appConfig; + this.logger = logger; + } + + internal bool TrySaveFile(HttpContent content, out Uri uri) + { + uri = new Uri(Path.Combine(appConfig.TemporaryDirectory, $"ServerExam{appConfig.ConfigurationFileExtension}")); + + try + { + var task = Task.Run(async () => + { + return await content.ReadAsStreamAsync(); + }); + + using (var data = task.GetAwaiter().GetResult()) + using (var file = new FileStream(uri.LocalPath, FileMode.Create)) + { + data.Seek(0, SeekOrigin.Begin); + data.CopyTo(file); + data.Flush(); + file.Flush(); + } + + return true; + } + catch (Exception e) + { + logger.Error($"Failed to save file '{uri.LocalPath}'!", e); + } + + return false; + } + } +} diff --git a/SafeExamBrowser.Server/Parser.cs b/SafeExamBrowser.Server/Parser.cs new file mode 100644 index 00000000..fea161c2 --- /dev/null +++ b/SafeExamBrowser.Server/Parser.cs @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET) + * + * 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.Generic; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using SafeExamBrowser.Logging.Contracts; +using SafeExamBrowser.Server.Contracts.Data; +using SafeExamBrowser.Server.Data; + +namespace SafeExamBrowser.Server +{ + internal class Parser + { + private readonly ILogger logger; + + internal Parser(ILogger logger) + { + this.logger = logger; + } + + internal bool TryParseApi(HttpContent content, out ApiVersion1 api) + { + var success = false; + + api = new ApiVersion1(); + + try + { + var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; + var apisJson = json["api-versions"]; + + foreach (var apiJson in apisJson.AsJEnumerable()) + { + if (apiJson["name"].Value().Equals("v1")) + { + foreach (var endpoint in apiJson["endpoints"].AsJEnumerable()) + { + var name = endpoint["name"].Value(); + var location = endpoint["location"].Value(); + + switch (name) + { + case "access-token-endpoint": + api.AccessTokenEndpoint = location; + break; + case "seb-configuration-endpoint": + api.ConfigurationEndpoint = location; + break; + case "seb-handshake-endpoint": + api.HandshakeEndpoint = location; + break; + case "seb-log-endpoint": + api.LogEndpoint = location; + break; + case "seb-ping-endpoint": + api.PingEndpoint = location; + break; + } + } + + success = true; + } + + if (!success) + { + logger.Error("The selected SEB server instance does not support the required API version!"); + } + } + } + catch (Exception e) + { + logger.Error("Failed to parse server API!", e); + } + + return success; + } + + internal bool TryParseConnectionToken(HttpResponseMessage response, out string connectionToken) + { + connectionToken = default(string); + + try + { + var hasHeader = response.Headers.TryGetValues("SEBConnectionToken", out var values); + + if (hasHeader) + { + connectionToken = values.First(); + } + else + { + logger.Error("Failed to retrieve connection token!"); + } + } + catch (Exception e) + { + logger.Error("Failed to parse connection token!", e); + } + + return connectionToken != default(string); + } + + internal bool TryParseExams(HttpContent content, out IList exams) + { + exams = new List(); + + try + { + var json = JsonConvert.DeserializeObject(Extract(content)) as JArray; + + foreach (var exam in json.AsJEnumerable()) + { + exams.Add(new Exam + { + Id = exam["examId"].Value(), + LmsName = exam["lmsType"].Value(), + Name = exam["name"].Value(), + Url = exam["url"].Value() + }); + } + } + catch (Exception e) + { + logger.Error("Failed to parse exams!", e); + } + + return exams.Any(); + } + + internal bool TryParseInstruction(HttpContent content, out Attributes attributes, out string instruction, out string instructionConfirmation) + { + attributes = new Attributes(); + instruction = default(string); + instructionConfirmation = default(string); + + try + { + var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; + + if (json != default(JObject)) + { + instruction = json["instruction"].Value(); + + if (json.ContainsKey("attributes")) + { + var attributesJson = json["attributes"] as JObject; + + if (attributesJson.ContainsKey("instruction-confirm")) + { + instructionConfirmation = attributesJson["instruction-confirm"].Value(); + } + + switch (instruction) + { + case Instructions.PROCTORING: + attributes.RoomName = attributesJson["jitsiMeetRoom"].Value(); + attributes.ServerUrl = attributesJson["jitsiMeetServerURL"].Value(); + attributes.Token = attributesJson["jitsiMeetToken"].Value(); + break; + case Instructions.PROCTORING_RECONFIGURATION: + attributes.EnableChat = attributesJson["jitsiMeetFeatureFlagChat"].Value(); + attributes.ReceiveAudio = attributesJson["jitsiMeetReceiveAudio"].Value(); + attributes.ReceiveVideo = attributesJson["jitsiMeetReceiveVideo"].Value(); + break; + } + } + } + } + catch (Exception e) + { + logger.Error("Failed to parse instruction!", e); + } + + return instruction != default(string); + } + + internal bool TryParseOauth2Token(HttpContent content, out string oauth2Token) + { + oauth2Token = default(string); + + try + { + var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; + + oauth2Token = json["access_token"].Value(); + } + catch (Exception e) + { + logger.Error("Failed to parse Oauth2 token!", e); + } + + return oauth2Token != default(string); + } + + private string Extract(HttpContent content) + { + var task = Task.Run(async () => + { + return await content.ReadAsStreamAsync(); + }); + var stream = task.GetAwaiter().GetResult(); + var reader = new StreamReader(stream); + + return reader.ReadToEnd(); + } + } +} diff --git a/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj b/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj index 00c2b6a8..dce919b0 100644 --- a/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj +++ b/SafeExamBrowser.Server/SafeExamBrowser.Server.csproj @@ -59,6 +59,11 @@ + + + + + diff --git a/SafeExamBrowser.Server/ServerProxy.cs b/SafeExamBrowser.Server/ServerProxy.cs index 407dcc82..cdf3fd55 100644 --- a/SafeExamBrowser.Server/ServerProxy.cs +++ b/SafeExamBrowser.Server/ServerProxy.cs @@ -9,7 +9,6 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net.Http; using System.Net.Http.Headers; @@ -38,13 +37,16 @@ namespace SafeExamBrowser.Server private ApiVersion1 api; private AppConfig appConfig; private CancellationTokenSource cancellationTokenSource; + private FileSystem fileSystem; private string connectionToken; private int currentPowerSupplyValue; private int currentWlanValue; private string examId; private HttpClient httpClient; + private ConcurrentQueue instructionConfirmations; private ILogger logger; private ConcurrentQueue logContent; + private Parser parser; private string oauth2Token; private int pingNumber; private IPowerSupply powerSupply; @@ -53,6 +55,8 @@ namespace SafeExamBrowser.Server private Timer timer; private IWirelessAdapter wirelessAdapter; + public event ProctoringConfigurationReceivedEventHandler ProctoringConfigurationReceived; + public event ProctoringInstructionReceivedEventHandler ProctoringInstructionReceived; public event TerminationRequestedEventHandler TerminationRequested; public ServerProxy( @@ -64,9 +68,12 @@ namespace SafeExamBrowser.Server this.api = new ApiVersion1(); this.appConfig = appConfig; this.cancellationTokenSource = new CancellationTokenSource(); + this.fileSystem = new FileSystem(appConfig, logger); this.httpClient = new HttpClient(); - this.logContent = new ConcurrentQueue(); + this.instructionConfirmations = new ConcurrentQueue(); this.logger = logger; + this.logContent = new ConcurrentQueue(); + this.parser = new Parser(logger); this.powerSupply = powerSupply; this.timer = new Timer(); this.wirelessAdapter = wirelessAdapter; @@ -75,9 +82,9 @@ namespace SafeExamBrowser.Server public ServerResponse Connect() { var success = TryExecute(HttpMethod.Get, settings.ApiUrl, out var response); - var message = ToString(response); + var message = response.ToLogString(); - if (success && TryParseApi(response.Content)) + if (success && parser.TryParseApi(response.Content, out api)) { logger.Info("Successfully loaded server API."); @@ -87,9 +94,9 @@ namespace SafeExamBrowser.Server var contentType = "application/x-www-form-urlencoded"; success = TryExecute(HttpMethod.Post, api.AccessTokenEndpoint, out response, content, contentType, authorization); - message = ToString(response); + message = response.ToLogString(); - if (success && TryParseOauth2Token(response.Content)) + if (success && parser.TryParseOauth2Token(response.Content, out oauth2Token)) { logger.Info("Successfully retrieved OAuth2 token."); } @@ -114,7 +121,7 @@ namespace SafeExamBrowser.Server var token = ("SEBConnectionToken", connectionToken); var success = TryExecute(HttpMethod.Delete, api.HandshakeEndpoint, out var response, content, contentType, authorization, token); - var message = ToString(response); + var message = response.ToLogString(); if (success) { @@ -136,12 +143,12 @@ namespace SafeExamBrowser.Server var exams = default(IList); var success = TryExecute(HttpMethod.Post, api.HandshakeEndpoint, out var response, content, contentType, authorization); - var message = ToString(response); + var message = response.ToLogString(); if (success) { - var hasExams = TryParseExams(response.Content, out exams); - var hasToken = TryParseConnectionToken(response); + var hasExams = parser.TryParseExams(response.Content, out exams); + var hasToken = parser.TryParseConnectionToken(response, out connectionToken); success = hasExams && hasToken; @@ -173,13 +180,13 @@ namespace SafeExamBrowser.Server var uri = default(Uri); var success = TryExecute(HttpMethod.Get, $"{api.ConfigurationEndpoint}?examId={exam.Id}", out var response, default(string), default(string), authorization, token); - var message = ToString(response); + var message = response.ToLogString(); if (success) { logger.Info("Successfully retrieved exam configuration."); - success = TrySaveFile(response.Content, out uri); + success = fileSystem.TrySaveFile(response.Content, out uri); if (success) { @@ -242,7 +249,7 @@ namespace SafeExamBrowser.Server var token = ("SEBConnectionToken", connectionToken); var success = TryExecute(HttpMethod.Put, api.HandshakeEndpoint, out var response, content, contentType, authorization, token); - var message = ToString(response); + var message = response.ToLogString(); if (success) { @@ -315,8 +322,8 @@ namespace SafeExamBrowser.Server { var json = new JObject { - ["type"] = ToLogType(message.Severity), - ["timestamp"] = ToUnixTimestamp(message.DateTime), + ["type"] = message.Severity.ToLogType(), + ["timestamp"] = message.DateTime.ToUnixTimestamp(), ["text"] = message.Message }; var content = json.ToString(); @@ -346,8 +353,8 @@ namespace SafeExamBrowser.Server var token = ("SEBConnectionToken", connectionToken); var json = new JObject { - ["type"] = ToLogType(LogLevel.Info), - ["timestamp"] = ToUnixTimestamp(DateTime.Now), + ["type"] = LogLevel.Info.ToLogType(), + ["timestamp"] = DateTime.Now.ToUnixTimestamp(), ["text"] = $" {chargeInfo}, {status.BatteryTimeRemaining} remaining, {gridInfo}", ["numericValue"] = value }; @@ -368,26 +375,43 @@ namespace SafeExamBrowser.Server try { var authorization = ("Authorization", $"Bearer {oauth2Token}"); - var content = $"timestamp={ToUnixTimestamp(DateTime.Now)}&ping-number={++pingNumber}"; + var content = $"timestamp={DateTime.Now.ToUnixTimestamp()}&ping-number={++pingNumber}"; var contentType = "application/x-www-form-urlencoded"; var token = ("SEBConnectionToken", connectionToken); + + if (instructionConfirmations.TryDequeue(out var confirmation)) + { + content = $"{content}&instruction-confirm={confirmation}"; + } + var success = TryExecute(HttpMethod.Post, api.PingEndpoint, out var response, content, contentType, authorization, token); if (success) { - if (TryParseInstruction(response.Content, out var instruction)) + if (parser.TryParseInstruction(response.Content, out var attributes, out var instruction, out var instructionConfirmation)) { switch (instruction) { - case "SEB_QUIT": + case Instructions.PROCTORING: + Task.Run(() => ProctoringInstructionReceived?.Invoke(attributes.RoomName, attributes.ServerUrl, attributes.Token)); + break; + case Instructions.PROCTORING_RECONFIGURATION: + Task.Run(() => ProctoringConfigurationReceived?.Invoke(attributes.EnableChat, attributes.ReceiveAudio, attributes.ReceiveVideo)); + break; + case Instructions.QUIT: Task.Run(() => TerminationRequested?.Invoke()); break; } + + if (instructionConfirmation != default(string)) + { + instructionConfirmations.Enqueue(instructionConfirmation); + } } } else { - logger.Error($"Failed to send ping: {ToString(response)}"); + logger.Error($"Failed to send ping: {response.ToLogString()}"); } } catch (Exception e) @@ -411,7 +435,7 @@ namespace SafeExamBrowser.Server var authorization = ("Authorization", $"Bearer {oauth2Token}"); var contentType = "application/json;charset=UTF-8"; var token = ("SEBConnectionToken", connectionToken); - var json = new JObject { ["type"] = ToLogType(LogLevel.Info), ["timestamp"] = ToUnixTimestamp(DateTime.Now) }; + var json = new JObject { ["type"] = LogLevel.Info.ToLogType(), ["timestamp"] = DateTime.Now.ToUnixTimestamp() }; if (network != default(IWirelessNetwork)) { @@ -434,148 +458,6 @@ namespace SafeExamBrowser.Server } } - private bool TryParseApi(HttpContent content) - { - var success = false; - - try - { - var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; - var apis = json["api-versions"]; - - foreach (var api in apis.AsJEnumerable()) - { - if (api["name"].Value().Equals("v1")) - { - foreach (var endpoint in api["endpoints"].AsJEnumerable()) - { - var name = endpoint["name"].Value(); - var location = endpoint["location"].Value(); - - switch (name) - { - case "access-token-endpoint": - this.api.AccessTokenEndpoint = location; - break; - case "seb-configuration-endpoint": - this.api.ConfigurationEndpoint = location; - break; - case "seb-handshake-endpoint": - this.api.HandshakeEndpoint = location; - break; - case "seb-log-endpoint": - this.api.LogEndpoint = location; - break; - case "seb-ping-endpoint": - this.api.PingEndpoint = location; - break; - } - } - - success = true; - } - - if (!success) - { - logger.Error("The selected SEB server instance does not support the required API version!"); - } - } - } - catch (Exception e) - { - logger.Error("Failed to parse server API!", e); - } - - return success; - } - - private bool TryParseConnectionToken(HttpResponseMessage response) - { - try - { - var hasHeader = response.Headers.TryGetValues("SEBConnectionToken", out var values); - - if (hasHeader) - { - connectionToken = values.First(); - } - else - { - logger.Error("Failed to retrieve connection token!"); - } - } - catch (Exception e) - { - logger.Error("Failed to parse connection token!", e); - } - - return connectionToken != default(string); - } - - private bool TryParseExams(HttpContent content, out IList exams) - { - exams = new List(); - - try - { - var json = JsonConvert.DeserializeObject(Extract(content)) as JArray; - - foreach (var exam in json.AsJEnumerable()) - { - exams.Add(new Exam - { - Id = exam["examId"].Value(), - LmsName = exam["lmsType"].Value(), - Name = exam["name"].Value(), - Url = exam["url"].Value() - }); - } - } - catch (Exception e) - { - logger.Error("Failed to parse exams!", e); - } - - return exams.Any(); - } - - private bool TryParseInstruction(HttpContent content, out string instruction) - { - instruction = default(string); - - try - { - var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; - - if (json != default(JObject)) - { - instruction = json["instruction"].Value(); - } - } - catch (Exception e) - { - logger.Error("Failed to parse instruction!", e); - } - - return instruction != default(string); - } - - private bool TryParseOauth2Token(HttpContent content) - { - try - { - var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; - - oauth2Token = json["access_token"].Value(); - } - catch (Exception e) - { - logger.Error("Failed to parse Oauth2 token!", e); - } - - return oauth2Token != default(string); - } - private bool TryExecute( HttpMethod method, string url, @@ -611,7 +493,7 @@ namespace SafeExamBrowser.Server if (request.RequestUri.AbsolutePath != api.LogEndpoint && request.RequestUri.AbsolutePath != api.PingEndpoint) { - logger.Debug($"Completed request: {request.Method} '{request.RequestUri}' -> {ToString(response)}"); + logger.Debug($"Completed request: {request.Method} '{request.RequestUri}' -> {response.ToLogString()}"); } } catch (TaskCanceledException) @@ -627,74 +509,5 @@ namespace SafeExamBrowser.Server return response != default(HttpResponseMessage) && response.IsSuccessStatusCode; } - - private bool TrySaveFile(HttpContent content, out Uri uri) - { - uri = new Uri(Path.Combine(appConfig.TemporaryDirectory, $"ServerExam{appConfig.ConfigurationFileExtension}")); - - try - { - var task = Task.Run(async () => - { - return await content.ReadAsStreamAsync(); - }); - - using (var data = task.GetAwaiter().GetResult()) - using (var file = new FileStream(uri.LocalPath, FileMode.Create)) - { - data.Seek(0, SeekOrigin.Begin); - data.CopyTo(file); - data.Flush(); - file.Flush(); - } - - return true; - } - catch (Exception e) - { - logger.Error($"Failed to save file '{uri.LocalPath}'!", e); - } - - return false; - } - - private string Extract(HttpContent content) - { - var task = Task.Run(async () => - { - return await content.ReadAsStreamAsync(); - }); - var stream = task.GetAwaiter().GetResult(); - var reader = new StreamReader(stream); - - return reader.ReadToEnd(); - } - - private string ToLogType(LogLevel severity) - { - switch (severity) - { - case LogLevel.Debug: - return "DEBUG_LOG"; - case LogLevel.Error: - return "ERROR_LOG"; - case LogLevel.Info: - return "INFO_LOG"; - case LogLevel.Warning: - return "WARN_LOG"; - } - - return "UNKNOWN"; - } - - private string ToString(HttpResponseMessage response) - { - return $"{(int?) response?.StatusCode} {response?.StatusCode} {response?.ReasonPhrase}"; - } - - private long ToUnixTimestamp(DateTime date) - { - return new DateTimeOffset(date).ToUnixTimeMilliseconds(); - } } } diff --git a/SafeExamBrowser.Settings/Proctoring/JitsiMeetSettings.cs b/SafeExamBrowser.Settings/Proctoring/JitsiMeetSettings.cs index ff8bb029..ac438744 100644 --- a/SafeExamBrowser.Settings/Proctoring/JitsiMeetSettings.cs +++ b/SafeExamBrowser.Settings/Proctoring/JitsiMeetSettings.cs @@ -16,21 +16,81 @@ namespace SafeExamBrowser.Settings.Proctoring [Serializable] public class JitsiMeetSettings { + /// + /// Determines whether the user can use the chat. + /// + public bool AllowChat { get; set; } + + /// + /// Determines whether the user can use close captions. + /// + public bool AllowCloseCaptions { get; set; } + + /// + /// Determines whether the user can use the raise hand feature. + /// + public bool AllowRaiseHand { get; set; } + + /// + /// Determines whether the user can record the meeting. + /// + public bool AllowRecording { get; set; } + + /// + /// Determines whether the user may use the tile view. + /// + public bool AllowTileView { get; set; } + + /// + /// Determines whether the audio starts muted. + /// + public bool AudioMuted { get; set; } + + /// + /// Determines whether the meeting runs in an audio-only mode. + /// + public bool AudioOnly { get; set; } + /// /// Determines whether proctoring with Jitsi Meet is enabled. /// public bool Enabled { get; set; } + /// + /// Determines whether the user may receive the video stream of other meeting participants. + /// + public bool ReceiveAudio { get; set; } + + /// + /// Determines whether the user may receive the audio stream of other meeting participants. + /// + public bool ReceiveVideo { get; set; } + /// /// The name of the meeting room. /// public string RoomName { get; set; } + /// + /// Determines whether the audio stream of the user will be sent to the server. + /// + public bool SendAudio { get; set; } + + /// + /// Determines whether the video stream of the user will be sent to the server. + /// + public bool SendVideo { get; set; } + /// /// The URL of the Jitsi Meet server. /// public string ServerUrl { get; set; } + /// + /// Determines whether the subject will be shown as meeting name. + /// + public bool ShowMeetingName { get; set; } + /// /// The subject of the meeting. /// @@ -40,5 +100,10 @@ namespace SafeExamBrowser.Settings.Proctoring /// The authentication token for the meeting. /// public string Token { get; set; } + + /// + /// Determines whether the video starts muted. + /// + public bool VideoMuted { get; set; } } }