SEBWIN-475: Implemented basic proctoring with Zoom and SEB server.
This commit is contained in:
parent
cad8f21ff3
commit
7ad1d6ae5d
12 changed files with 234 additions and 58 deletions
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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)}`)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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; }
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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" />
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Reference in a new issue