SEBWIN-475: Implemented basic proctoring with Zoom and SEB server.

This commit is contained in:
Damian Büchel 2021-06-16 15:38:55 +02:00
parent cad8f21ff3
commit 7ad1d6ae5d
12 changed files with 234 additions and 58 deletions

View file

@ -77,9 +77,24 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
case Keys.Proctoring.WindowVisibility:
MapWindowVisibility(settings, value);
break;
case Keys.Proctoring.Zoom.ApiKey:
MapZoomApiKey(settings, value);
break;
case Keys.Proctoring.Zoom.ApiSecret:
MapZoomApiSecret(settings, value);
break;
case Keys.Proctoring.Zoom.Enabled:
MapZoomEnabled(settings, value);
break;
case Keys.Proctoring.Zoom.MeetingNumber:
MapZoomMeetingNumber(settings, value);
break;
case Keys.Proctoring.Zoom.Signature:
MapZoomSignature(settings, value);
break;
case Keys.Proctoring.Zoom.UserName:
MapZoomUserName(settings, value);
break;
}
}
@ -262,6 +277,22 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
}
}
private void MapZoomApiKey(AppSettings settings, object value)
{
if (value is string key)
{
settings.Proctoring.Zoom.ApiKey = key;
}
}
private void MapZoomApiSecret(AppSettings settings, object value)
{
if (value is string secret)
{
settings.Proctoring.Zoom.ApiSecret = secret;
}
}
private void MapZoomEnabled(AppSettings settings, object value)
{
if (value is bool enabled)
@ -269,5 +300,29 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
settings.Proctoring.Zoom.Enabled = enabled;
}
}
private void MapZoomMeetingNumber(AppSettings settings, object value)
{
if (value is string number)
{
settings.Proctoring.Zoom.MeetingNumber = number;
}
}
private void MapZoomSignature(AppSettings settings, object value)
{
if (value is string signature)
{
settings.Proctoring.Zoom.Signature = signature;
}
}
private void MapZoomUserName(AppSettings settings, object value)
{
if (value is string name)
{
settings.Proctoring.Zoom.UserName = name;
}
}
}
}

View file

@ -250,7 +250,12 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
internal static class Zoom
{
internal const string ApiKey = "zoomApiKey";
internal const string ApiSecret = "zoomApiSecret";
internal const string Enabled = "zoomEnable";
internal const string MeetingNumber = "zoomRoom";
internal const string Signature = "zoomToken";
internal const string UserName = "zoomUserInfoDisplayName";
}
}

View file

@ -16,12 +16,12 @@ namespace SafeExamBrowser.Proctoring.Contracts
public interface IProctoringController
{
/// <summary>
///
/// Initializes the given settings and starts the proctoring if the settings are valid.
/// </summary>
void Initialize(ProctoringSettings settings);
/// <summary>
///
/// Stops the proctoring functionality.
/// </summary>
void Terminate();
}

View file

@ -19,6 +19,7 @@ using SafeExamBrowser.I18n.Contracts;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.Proctoring.Contracts;
using SafeExamBrowser.Server.Contracts;
using SafeExamBrowser.Server.Contracts.Events;
using SafeExamBrowser.Settings.Proctoring;
using SafeExamBrowser.SystemComponents.Contracts;
using SafeExamBrowser.UserInterface.Contracts;
@ -97,7 +98,7 @@ namespace SafeExamBrowser.Proctoring
else if (settings.Zoom.Enabled)
{
start = !string.IsNullOrWhiteSpace(settings.Zoom.ApiKey);
start &= !string.IsNullOrWhiteSpace(settings.Zoom.ApiSecret);
start &= !string.IsNullOrWhiteSpace(settings.Zoom.ApiSecret) || !string.IsNullOrWhiteSpace(settings.Zoom.Signature);
start &= !string.IsNullOrWhiteSpace(settings.Zoom.MeetingNumber);
start &= !string.IsNullOrWhiteSpace(settings.Zoom.UserName);
}
@ -113,13 +114,19 @@ namespace SafeExamBrowser.Proctoring
StopProctoring();
}
private void Server_ProctoringInstructionReceived(string roomName, string serverUrl, string token)
private void Server_ProctoringInstructionReceived(ProctoringInstructionEventArgs args)
{
logger.Info("Proctoring instruction received.");
settings.JitsiMeet.RoomName = roomName;
settings.JitsiMeet.ServerUrl = Sanitize(serverUrl);
settings.JitsiMeet.Token = token;
settings.JitsiMeet.RoomName = args.JitsiMeetRoomName;
settings.JitsiMeet.ServerUrl = args.JitsiMeetServerUrl;
settings.JitsiMeet.Token = args.JitsiMeetToken;
settings.Zoom.ApiKey = args.ZoomApiKey;
settings.Zoom.MeetingNumber = args.ZoomMeetingNumber;
settings.Zoom.Password = args.ZoomPassword;
settings.Zoom.Signature = args.ZoomSignature;
settings.Zoom.UserName = args.ZoomUserName;
StopProctoring();
StartProctoring();
@ -142,6 +149,7 @@ namespace SafeExamBrowser.Proctoring
settings.WindowVisibility = windowVisibility;
}
// TODO: This is apparently not necessary for Zoom, there we can enable / disable the options via API call!
StopProctoring();
StartProctoring();
}
@ -237,6 +245,8 @@ namespace SafeExamBrowser.Proctoring
html = html.Replace("%%_API_KEY_%%", settings.Zoom.ApiKey);
html = html.Replace("%%_API_SECRET_%%", settings.Zoom.ApiSecret);
html = html.Replace("%%_MEETING_NUMBER_%%", settings.Zoom.MeetingNumber);
html = html.Replace("%%_PASSWORD_%%", settings.Zoom.Password);
html = html.Replace("%%_SIGNATURE_%%", settings.Zoom.Signature);
html = html.Replace("%%_USER_NAME_%%", settings.Zoom.UserName);
}

View file

@ -14,32 +14,41 @@
<script src="https://source.zoom.us/zoom-meeting-1.9.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9/crypto-js.min.js"></script>
<script type="text/javascript">
const API_KEY = "%%_API_KEY_%%";
const API_SECRET = "%%_API_SECRET_%%";
const API_KEY = '%%_API_KEY_%%';
const API_SECRET = '%%_API_SECRET_%%';
const ATTENDEE = 0;
var configuration = {
leaveUrl: 'doesnotexist',
meetingNumber: '%%_MEETING_NUMBER_%%',
passWord: '%%_PASSWORD_%%',
role: ATTENDEE,
userName: '%%_USER_NAME_%%'
};
var signature = '%%_SIGNATURE_%%';
if (!ZoomMtg.checkSystemRequirements()) {
alert('This system does not meet the necessary requirements for Zoom!');
}
ZoomMtg.setZoomJSLib('https://source.zoom.us/1.9.1/lib', '/av');
ZoomMtg.preLoadWasm();
ZoomMtg.prepareJssdk();
const config = {
meetingNumber: "%%_MEETING_NUMBER_%%",
leaveUrl: "doesnotexist",
userName: '%%_USER_NAME_%%',
role: 0
};
const signature = ZoomMtg.generateSignature({
meetingNumber: config.meetingNumber,
apiKey: API_KEY,
apiSecret: API_SECRET,
role: config.role,
error: function (res) {
alert(`Failed to generate signature: ${res}`)
}
});
if (!signature) {
signature = ZoomMtg.generateSignature({
meetingNumber: configuration.meetingNumber,
apiKey: API_KEY,
apiSecret: API_SECRET,
role: configuration.role,
error: function (res) {
alert(`Failed to generate signature: ${JSON.stringify(res)}`)
}
});
}
ZoomMtg.init({
leaveUrl: config.leaveUrl,
leaveUrl: configuration.leaveUrl,
showMeetingHeader: true,
disableInvite: false,
disableCallOut: false,
@ -48,7 +57,7 @@
audioPanelAlwaysOpen: true,
showPureSharingContent: false,
isSupportAV: true,
isSupportChat: false,
isSupportChat: true,
isSupportQA: true,
isSupportCC: true,
screenShare: true,
@ -59,10 +68,6 @@
isSupportNonverbal: true,
isShowJoiningErrorDialog: true,
inviteUrlFormat: '',
loginWindow: {
width: 400,
height: 380
},
meetingInfo: [
'topic',
'host',
@ -76,16 +81,17 @@
disableVoIP: false,
disableReport: false,
error: function (res) {
alert(`Failed to initialize meeting: ${res}`)
alert(`Failed to initialize meeting: ${JSON.stringify(res)}`)
},
success: function () {
ZoomMtg.join({
signature: signature,
apiKey: API_KEY,
meetingNumber: config.meetingNumber,
userName: config.userName,
meetingNumber: configuration.meetingNumber,
passWord: configuration.passWord,
signature: signature,
userName: configuration.userName,
error: function (res) {
alert(`Failed to join meeting: ${res}`)
alert(`Failed to join meeting: ${JSON.stringify(res)}`)
}
})
}

View file

@ -0,0 +1,25 @@
/*
* 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>
/// Defines all parameters for a proctoring instruction received by the <see cref="IServerProxy"/>.
/// </summary>
public class ProctoringInstructionEventArgs
{
public string JitsiMeetRoomName { get; set; }
public string JitsiMeetServerUrl { get; set; }
public string JitsiMeetToken { get; set; }
public string ZoomApiKey { get; set; }
public string ZoomMeetingNumber { get; set; }
public string ZoomPassword { get; set; }
public string ZoomSignature { get; set; }
public string ZoomUserName { get; set; }
}
}

View file

@ -9,7 +9,7 @@
namespace SafeExamBrowser.Server.Contracts.Events
{
/// <summary>
/// Event handler used to indicate that a proctoring instruction has been detected.
/// Event handler used to indicate that a proctoring instruction has been received.
/// </summary>
public delegate void ProctoringInstructionReceivedEventHandler(string roomName, string serverUrl, string token);
public delegate void ProctoringInstructionReceivedEventHandler(ProctoringInstructionEventArgs args);
}

View file

@ -57,6 +57,7 @@
<Compile Include="Data\ConnectionInfo.cs" />
<Compile Include="Data\Exam.cs" />
<Compile Include="Events\ProctoringConfigurationReceivedEventHandler.cs" />
<Compile Include="Events\ProctoringInstructionEventArgs.cs" />
<Compile Include="Events\ProctoringInstructionReceivedEventHandler.cs" />
<Compile Include="Events\TerminationRequestedEventHandler.cs" />
<Compile Include="IServerProxy.cs" />

View file

@ -6,15 +6,20 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
using SafeExamBrowser.Server.Contracts.Events;
namespace SafeExamBrowser.Server.Data
{
internal class Attributes
{
public bool AllowChat { 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; }
internal bool AllowChat { get; set; }
internal ProctoringInstructionEventArgs Instruction { get; set; }
internal bool ReceiveAudio { get; set; }
internal bool ReceiveVideo { get; set; }
internal Attributes()
{
Instruction = new ProctoringInstructionEventArgs();
}
}
}

View file

@ -161,19 +161,7 @@ namespace SafeExamBrowser.Server
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.AllowChat = attributesJson["jitsiMeetFeatureFlagChat"].Value<bool>();
attributes.ReceiveAudio = attributesJson["jitsiMeetReceiveAudio"].Value<bool>();
attributes.ReceiveVideo = attributesJson["jitsiMeetReceiveVideo"].Value<bool>();
break;
}
attributes = ParseProctoringAttributes(attributesJson, instruction);
}
}
}
@ -185,6 +173,77 @@ namespace SafeExamBrowser.Server
return instruction != default(string);
}
private Attributes ParseProctoringAttributes(JObject attributesJson, string instruction)
{
var attributes = new Attributes();
switch (instruction)
{
case Instructions.PROCTORING:
ParseProctoringInstruction(attributes, attributesJson);
break;
case Instructions.PROCTORING_RECONFIGURATION:
ParseReconfigurationInstruction(attributes, attributesJson);
break;
}
return attributes;
}
private void ParseProctoringInstruction(Attributes attributes, JObject attributesJson)
{
var provider = attributesJson["service-type"].Value<string>();
switch (provider)
{
case "JITSI_MEET":
attributes.Instruction.JitsiMeetRoomName = attributesJson["jitsiMeetRoom"].Value<string>();
attributes.Instruction.JitsiMeetServerUrl = attributesJson["jitsiMeetServerURL"].Value<string>();
attributes.Instruction.JitsiMeetToken = attributesJson["jitsiMeetToken"].Value<string>();
break;
case "ZOOM":
attributes.Instruction.ZoomApiKey = attributesJson["zoomAPIKey"].Value<string>();
attributes.Instruction.ZoomMeetingNumber = attributesJson["zoomRoom"].Value<string>();
attributes.Instruction.ZoomPassword = attributesJson["zoomMeetingKey"].Value<string>();
attributes.Instruction.ZoomSignature = attributesJson["zoomToken"].Value<string>();
attributes.Instruction.ZoomUserName = attributesJson["zoomUserName"].Value<string>();
break;
}
}
private void ParseReconfigurationInstruction(Attributes attributes, JObject attributesJson)
{
if (attributesJson.ContainsKey("jitsiMeetFeatureFlagChat"))
{
attributes.AllowChat = attributesJson["jitsiMeetFeatureFlagChat"].Value<bool>();
}
if (attributesJson.ContainsKey("zoomFeatureFlagChat"))
{
attributes.AllowChat = attributesJson["zoomFeatureFlagChat"].Value<bool>();
}
if (attributesJson.ContainsKey("jitsiMeetReceiveAudio"))
{
attributes.ReceiveAudio = attributesJson["jitsiMeetReceiveAudio"].Value<bool>();
}
if (attributesJson.ContainsKey("zoomReceiveAudio"))
{
attributes.ReceiveAudio = attributesJson["zoomReceiveAudio"].Value<bool>();
}
if (attributesJson.ContainsKey("jitsiMeetReceiveVideo"))
{
attributes.ReceiveVideo = attributesJson["jitsiMeetReceiveVideo"].Value<bool>();
}
if (attributesJson.ContainsKey("zoomReceiveVideo"))
{
attributes.ReceiveVideo = attributesJson["zoomReceiveVideo"].Value<bool>();
}
}
internal bool TryParseOauth2Token(HttpContent content, out string oauth2Token)
{
oauth2Token = default(string);

View file

@ -393,7 +393,7 @@ namespace SafeExamBrowser.Server
switch (instruction)
{
case Instructions.PROCTORING:
Task.Run(() => ProctoringInstructionReceived?.Invoke(attributes.RoomName, attributes.ServerUrl, attributes.Token));
Task.Run(() => ProctoringInstructionReceived?.Invoke(attributes.Instruction));
break;
case Instructions.PROCTORING_RECONFIGURATION:
Task.Run(() => ProctoringConfigurationReceived?.Invoke(attributes.AllowChat, attributes.ReceiveAudio, attributes.ReceiveVideo));

View file

@ -36,6 +36,16 @@ namespace SafeExamBrowser.Settings.Proctoring
/// </summary>
public string MeetingNumber { get; set; }
/// <summary>
/// The password of the meeting.
/// </summary>
public string Password { get; set; }
/// <summary>
/// The signature to be used for authentication.
/// </summary>
public string Signature { get; set; }
/// <summary>
/// The user name to be used for the meeting.
/// </summary>