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; }
}
}