SEBWIN-450: Implemented static settings for proctoring with Jitsi Meet.
This commit is contained in:
parent
d309de2050
commit
55603f3221
21 changed files with 837 additions and 295 deletions
|
@ -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
|
|||
/// </summary>
|
||||
internal IClientHost ClientHost { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The proctoring controller to be used if the current session runs with remote proctoring.
|
||||
/// </summary>
|
||||
internal IProctoringController ProctoringController { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The server proxy to be used if the current session mode is <see cref="SessionMode.Server"/>.
|
||||
/// </summary>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -6,32 +6,33 @@
|
|||
<div id="placeholder" />
|
||||
<script src='https://meet.jit.si/external_api.js'></script>
|
||||
<script type="text/javascript">
|
||||
var domain = "%%_DOMAIN_%%";
|
||||
var configOverwrite = {
|
||||
disableProfile: true,
|
||||
startAudioOnly: '%_AUDIO_ONLY_%',
|
||||
startWithAudioMuted: '%_AUDIO_MUTED_%',
|
||||
startWithVideoMuted: '%_VIDEO_MUTED_%'
|
||||
};
|
||||
var interfaceOverwrite = {
|
||||
JITSI_WATERMARK_LINK: '',
|
||||
SHOW_JITSI_WATERMARK: false,
|
||||
TOOLBAR_BUTTONS: [
|
||||
'microphone', 'camera', '%%_ALLOW_CLOSED_CAPTIONS_%%', /*'desktop',*/ 'embedmeeting', 'fullscreen',
|
||||
'fodeviceselection', 'hangup', 'profile', '%%_ALLOW_CHAT_%%', '%%_ALLOW_RECORDING_%%',
|
||||
'livestreaming', 'etherpad', /*'sharedvideo',*/ 'settings', '%%_ALLOW_RAISE_HAND_%%',
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
'%%_ALLOW_TILE_VIEW_%%', 'select-background', 'download', 'help', /*'mute-everyone',*/ 'mute-video-everyone', 'security'
|
||||
]
|
||||
};
|
||||
var options = {
|
||||
configOverwrite: {
|
||||
disable1On1Mode: true,
|
||||
startAudioOnly: false,
|
||||
startWithAudioMuted: true,
|
||||
startWithVideoMuted: false
|
||||
},
|
||||
configOverwrite: configOverwrite,
|
||||
height: "100%",
|
||||
interfaceConfigOverwrite: {
|
||||
JITSI_WATERMARK_LINK: '',
|
||||
SHOW_JITSI_WATERMARK: false,
|
||||
TOOLBAR_BUTTONS: [
|
||||
'microphone', 'camera', 'closedcaptions', 'desktop', 'embedmeeting', 'fullscreen',
|
||||
'fodeviceselection', 'hangup', 'profile', 'chat', 'recording',
|
||||
'livestreaming', 'etherpad', /*'sharedvideo',*/ 'settings', 'raisehand',
|
||||
'videoquality', 'filmstrip', 'invite', 'feedback', 'stats', 'shortcuts',
|
||||
'tileview', 'select-background', 'download', 'help', 'mute-everyone', 'mute-video-everyone', 'security'
|
||||
]
|
||||
},
|
||||
interfaceConfigOverwrite: interfaceOverwrite,
|
||||
jwt: "%%_TOKEN_%%",
|
||||
parentNode: document.querySelector('#placeholder'),
|
||||
roomName: "%%_ROOM_NAME_%%",
|
||||
width: "100%"
|
||||
};
|
||||
var api = new JitsiMeetExternalAPI(domain, options);
|
||||
var api = new JitsiMeetExternalAPI("%%_DOMAIN_%%", options);
|
||||
|
||||
api.executeCommand("subject", "%%_SUBJECT_%%");
|
||||
</script>
|
||||
|
|
|
@ -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)
|
||||
{
|
||||
|
|
|
@ -94,6 +94,10 @@
|
|||
<Project>{8e52bd1c-0540-4f16-b181-6665d43f7a7b}</Project>
|
||||
<Name>SafeExamBrowser.Proctoring.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Server.Contracts\SafeExamBrowser.Server.Contracts.csproj">
|
||||
<Project>{db701e6f-bddc-4cec-b662-335a9dc11809}</Project>
|
||||
<Name>SafeExamBrowser.Server.Contracts</Name>
|
||||
</ProjectReference>
|
||||
<ProjectReference Include="..\SafeExamBrowser.Settings\SafeExamBrowser.Settings.csproj">
|
||||
<Project>{30b2d907-5861-4f39-abad-c4abf1b3470e}</Project>
|
||||
<Name>SafeExamBrowser.Settings</Name>
|
||||
|
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Event handler used to indicate that proctoring configuration data has been received.
|
||||
/// </summary>
|
||||
public delegate void ProctoringConfigurationReceivedEventHandler(bool enableChat, bool receiveAudio, bool receiveVideo);
|
||||
}
|
|
@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Event handler used to indicate that a proctoring instruction has been detected.
|
||||
/// </summary>
|
||||
public delegate void ProctoringInstructionReceivedEventHandler(string roomName, string serverUrl, string token);
|
||||
}
|
|
@ -19,6 +19,16 @@ namespace SafeExamBrowser.Server.Contracts
|
|||
/// </summary>
|
||||
public interface IServerProxy
|
||||
{
|
||||
/// <summary>
|
||||
/// Event fired when the server receives new proctoring configuration values.
|
||||
/// </summary>
|
||||
event ProctoringConfigurationReceivedEventHandler ProctoringConfigurationReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when the server receives a proctoring instruction.
|
||||
/// </summary>
|
||||
event ProctoringInstructionReceivedEventHandler ProctoringInstructionReceived;
|
||||
|
||||
/// <summary>
|
||||
/// Event fired when the server detects an instruction to terminate SEB.
|
||||
/// </summary>
|
||||
|
|
|
@ -56,6 +56,8 @@
|
|||
<ItemGroup>
|
||||
<Compile Include="Data\ConnectionInfo.cs" />
|
||||
<Compile Include="Data\Exam.cs" />
|
||||
<Compile Include="Events\ProctoringConfigurationReceivedEventHandler.cs" />
|
||||
<Compile Include="Events\ProctoringInstructionReceivedEventHandler.cs" />
|
||||
<Compile Include="Events\TerminationRequestedEventHandler.cs" />
|
||||
<Compile Include="IServerProxy.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
|
|
20
SafeExamBrowser.Server/Data/Attributes.cs
Normal file
20
SafeExamBrowser.Server/Data/Attributes.cs
Normal file
|
@ -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; }
|
||||
}
|
||||
}
|
17
SafeExamBrowser.Server/Data/Instructions.cs
Normal file
17
SafeExamBrowser.Server/Data/Instructions.cs
Normal file
|
@ -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";
|
||||
}
|
||||
}
|
44
SafeExamBrowser.Server/Extensions.cs
Normal file
44
SafeExamBrowser.Server/Extensions.cs
Normal file
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
59
SafeExamBrowser.Server/FileSystem.cs
Normal file
59
SafeExamBrowser.Server/FileSystem.cs
Normal file
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
218
SafeExamBrowser.Server/Parser.cs
Normal file
218
SafeExamBrowser.Server/Parser.cs
Normal file
|
@ -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<string>().Equals("v1"))
|
||||
{
|
||||
foreach (var endpoint in apiJson["endpoints"].AsJEnumerable())
|
||||
{
|
||||
var name = endpoint["name"].Value<string>();
|
||||
var location = endpoint["location"].Value<string>();
|
||||
|
||||
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<Exam> exams)
|
||||
{
|
||||
exams = new List<Exam>();
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.DeserializeObject(Extract(content)) as JArray;
|
||||
|
||||
foreach (var exam in json.AsJEnumerable())
|
||||
{
|
||||
exams.Add(new Exam
|
||||
{
|
||||
Id = exam["examId"].Value<string>(),
|
||||
LmsName = exam["lmsType"].Value<string>(),
|
||||
Name = exam["name"].Value<string>(),
|
||||
Url = exam["url"].Value<string>()
|
||||
});
|
||||
}
|
||||
}
|
||||
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<string>();
|
||||
|
||||
if (json.ContainsKey("attributes"))
|
||||
{
|
||||
var attributesJson = json["attributes"] as JObject;
|
||||
|
||||
if (attributesJson.ContainsKey("instruction-confirm"))
|
||||
{
|
||||
instructionConfirmation = attributesJson["instruction-confirm"].Value<string>();
|
||||
}
|
||||
|
||||
switch (instruction)
|
||||
{
|
||||
case Instructions.PROCTORING:
|
||||
attributes.RoomName = attributesJson["jitsiMeetRoom"].Value<string>();
|
||||
attributes.ServerUrl = attributesJson["jitsiMeetServerURL"].Value<string>();
|
||||
attributes.Token = attributesJson["jitsiMeetToken"].Value<string>();
|
||||
break;
|
||||
case Instructions.PROCTORING_RECONFIGURATION:
|
||||
attributes.EnableChat = attributesJson["jitsiMeetFeatureFlagChat"].Value<bool>();
|
||||
attributes.ReceiveAudio = attributesJson["jitsiMeetReceiveAudio"].Value<bool>();
|
||||
attributes.ReceiveVideo = attributesJson["jitsiMeetReceiveVideo"].Value<bool>();
|
||||
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<string>();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -59,6 +59,11 @@
|
|||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Data\ApiVersion1.cs" />
|
||||
<Compile Include="Data\Attributes.cs" />
|
||||
<Compile Include="Data\Instructions.cs" />
|
||||
<Compile Include="Extensions.cs" />
|
||||
<Compile Include="FileSystem.cs" />
|
||||
<Compile Include="Parser.cs" />
|
||||
<Compile Include="Properties\AssemblyInfo.cs" />
|
||||
<Compile Include="ServerProxy.cs" />
|
||||
</ItemGroup>
|
||||
|
|
|
@ -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<string> instructionConfirmations;
|
||||
private ILogger logger;
|
||||
private ConcurrentQueue<ILogContent> 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<ILogContent>();
|
||||
this.instructionConfirmations = new ConcurrentQueue<string>();
|
||||
this.logger = logger;
|
||||
this.logContent = new ConcurrentQueue<ILogContent>();
|
||||
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<Exam>);
|
||||
|
||||
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"] = $"<battery> {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<string>().Equals("v1"))
|
||||
{
|
||||
foreach (var endpoint in api["endpoints"].AsJEnumerable())
|
||||
{
|
||||
var name = endpoint["name"].Value<string>();
|
||||
var location = endpoint["location"].Value<string>();
|
||||
|
||||
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<Exam> exams)
|
||||
{
|
||||
exams = new List<Exam>();
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.DeserializeObject(Extract(content)) as JArray;
|
||||
|
||||
foreach (var exam in json.AsJEnumerable())
|
||||
{
|
||||
exams.Add(new Exam
|
||||
{
|
||||
Id = exam["examId"].Value<string>(),
|
||||
LmsName = exam["lmsType"].Value<string>(),
|
||||
Name = exam["name"].Value<string>(),
|
||||
Url = exam["url"].Value<string>()
|
||||
});
|
||||
}
|
||||
}
|
||||
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<string>();
|
||||
}
|
||||
}
|
||||
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<string>();
|
||||
}
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,21 +16,81 @@ namespace SafeExamBrowser.Settings.Proctoring
|
|||
[Serializable]
|
||||
public class JitsiMeetSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Determines whether the user can use the chat.
|
||||
/// </summary>
|
||||
public bool AllowChat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the user can use close captions.
|
||||
/// </summary>
|
||||
public bool AllowCloseCaptions { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the user can use the raise hand feature.
|
||||
/// </summary>
|
||||
public bool AllowRaiseHand { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the user can record the meeting.
|
||||
/// </summary>
|
||||
public bool AllowRecording { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the user may use the tile view.
|
||||
/// </summary>
|
||||
public bool AllowTileView { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the audio starts muted.
|
||||
/// </summary>
|
||||
public bool AudioMuted { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the meeting runs in an audio-only mode.
|
||||
/// </summary>
|
||||
public bool AudioOnly { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether proctoring with Jitsi Meet is enabled.
|
||||
/// </summary>
|
||||
public bool Enabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the user may receive the video stream of other meeting participants.
|
||||
/// </summary>
|
||||
public bool ReceiveAudio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the user may receive the audio stream of other meeting participants.
|
||||
/// </summary>
|
||||
public bool ReceiveVideo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The name of the meeting room.
|
||||
/// </summary>
|
||||
public string RoomName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the audio stream of the user will be sent to the server.
|
||||
/// </summary>
|
||||
public bool SendAudio { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the video stream of the user will be sent to the server.
|
||||
/// </summary>
|
||||
public bool SendVideo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The URL of the Jitsi Meet server.
|
||||
/// </summary>
|
||||
public string ServerUrl { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the subject will be shown as meeting name.
|
||||
/// </summary>
|
||||
public bool ShowMeetingName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The subject of the meeting.
|
||||
/// </summary>
|
||||
|
@ -40,5 +100,10 @@ namespace SafeExamBrowser.Settings.Proctoring
|
|||
/// The authentication token for the meeting.
|
||||
/// </summary>
|
||||
public string Token { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the video starts muted.
|
||||
/// </summary>
|
||||
public bool VideoMuted { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue