diff --git a/SafeExamBrowser.Client/CompositionRoot.cs b/SafeExamBrowser.Client/CompositionRoot.cs index 2ff52fbf..3c0b045e 100644 --- a/SafeExamBrowser.Client/CompositionRoot.cs +++ b/SafeExamBrowser.Client/CompositionRoot.cs @@ -249,7 +249,7 @@ namespace SafeExamBrowser.Client private IOperation BuildProctoringOperation() { - var controller = new ProctoringController(uiFactory); + var controller = new ProctoringController(ModuleLogger(nameof(ProctoringController)), uiFactory); var operation = new ProctoringOperation(context, logger, controller); context.ProctoringController = controller; diff --git a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs index 23097435..0627fea0 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/DataMapping/ProctoringDataMapper.cs @@ -17,7 +17,50 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping { switch (key) { - // TODO + case Keys.Proctoring.JitsiMeet.RoomName: + MapJitsiMeetRoomName(settings, value); + break; + case Keys.Proctoring.JitsiMeet.ServerUrl: + MapJitsiMeetServerUrl(settings, value); + break; + case Keys.Proctoring.JitsiMeet.Subject: + MapJitsiMeetSubject(settings, value); + break; + case Keys.Proctoring.JitsiMeet.Token: + MapJitsiMeetToken(settings, value); + break; + } + } + + private void MapJitsiMeetRoomName(AppSettings settings, object value) + { + if (value is string name) + { + settings.Proctoring.JitsiMeet.RoomName = name; + } + } + + private void MapJitsiMeetServerUrl(AppSettings settings, object value) + { + if (value is string url) + { + settings.Proctoring.JitsiMeet.ServerUrl = url; + } + } + + private void MapJitsiMeetSubject(AppSettings settings, object value) + { + if (value is string subject) + { + settings.Proctoring.JitsiMeet.Subject = subject; + } + } + + private void MapJitsiMeetToken(AppSettings settings, object value) + { + if (value is string token) + { + settings.Proctoring.JitsiMeet.Token = token; } } diff --git a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs index c9b9d4de..899f464f 100644 --- a/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs +++ b/SafeExamBrowser.Configuration/ConfigurationData/Keys.cs @@ -219,6 +219,10 @@ namespace SafeExamBrowser.Configuration.ConfigurationData internal static class JitsiMeet { internal const string Enabled = "jitsiMeetEnable"; + internal const string RoomName = "jitsiMeetRoom"; + internal const string ServerUrl = "jitsiMeetServerURL"; + internal const string Subject = "jitsiMeetSubject"; + internal const string Token = "jitsiMeetToken"; } internal static class Zoom diff --git a/SafeExamBrowser.Proctoring/JitsiMeet/index.html b/SafeExamBrowser.Proctoring/JitsiMeet/index.html new file mode 100644 index 00000000..5fc8af18 --- /dev/null +++ b/SafeExamBrowser.Proctoring/JitsiMeet/index.html @@ -0,0 +1,22 @@ +<html> + <head> + <meta charset="utf-8" /> + </head> + <body> + <div id="placeholder" /> + <script src='https://meet.jit.si/external_api.js'></script> + <script type="text/javascript"> + var domain = "%%_DOMAIN_%%"; + var options = { + height: "100%", + jwt: "%%_TOKEN_%%", + parentNode: document.querySelector('#placeholder'), + roomName: "%%_ROOM_NAME_%%", + width: "100%" + }; + var api = new JitsiMeetExternalAPI(domain, options); + + api.executeCommand("subject", "%%_SUBJECT_%%"); + </script> + </body> +</html> \ No newline at end of file diff --git a/SafeExamBrowser.Proctoring/ProctoringControl.cs b/SafeExamBrowser.Proctoring/ProctoringControl.cs index d9600a8d..bc4cfb68 100644 --- a/SafeExamBrowser.Proctoring/ProctoringControl.cs +++ b/SafeExamBrowser.Proctoring/ProctoringControl.cs @@ -6,17 +6,46 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -using System; +using Microsoft.Web.WebView2.Core; using Microsoft.Web.WebView2.Wpf; +using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.UserInterface.Contracts.Proctoring; namespace SafeExamBrowser.Proctoring { internal class ProctoringControl : WebView2, IProctoringControl { - internal ProctoringControl() + private readonly ILogger logger; + + internal ProctoringControl(ILogger logger) { - Source = new Uri("https://www.microsoft.com"); + this.logger = logger; + CoreWebView2InitializationCompleted += ProctoringControl_CoreWebView2InitializationCompleted; + } + + private void CoreWebView2_PermissionRequested(object sender, CoreWebView2PermissionRequestedEventArgs e) + { + if (e.PermissionKind == CoreWebView2PermissionKind.Camera || e.PermissionKind == CoreWebView2PermissionKind.Microphone) + { + logger.Info($"Granted access to {e.PermissionKind}."); + } + else + { + logger.Info($"Denied access to {e.PermissionKind}."); + } + } + + private void ProctoringControl_CoreWebView2InitializationCompleted(object sender, CoreWebView2InitializationCompletedEventArgs e) + { + if (e.IsSuccess) + { + CoreWebView2.PermissionRequested += CoreWebView2_PermissionRequested; + logger.Info("Successfully initialized."); + } + else + { + logger.Error("Failed to initialize!", e.InitializationException); + } } } } diff --git a/SafeExamBrowser.Proctoring/ProctoringController.cs b/SafeExamBrowser.Proctoring/ProctoringController.cs index b98acf87..834e5c90 100644 --- a/SafeExamBrowser.Proctoring/ProctoringController.cs +++ b/SafeExamBrowser.Proctoring/ProctoringController.cs @@ -6,6 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +using System; +using System.IO; +using System.Reflection; +using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Proctoring.Contracts; using SafeExamBrowser.Settings.Proctoring; using SafeExamBrowser.UserInterface.Contracts; @@ -15,31 +19,72 @@ namespace SafeExamBrowser.Proctoring { public class ProctoringController : IProctoringController { + private readonly IModuleLogger logger; private readonly IUserInterfaceFactory uiFactory; private IProctoringWindow window; - public ProctoringController(IUserInterfaceFactory uiFactory) + public ProctoringController(IModuleLogger logger, IUserInterfaceFactory uiFactory) { + this.logger = logger; this.uiFactory = uiFactory; } public void Initialize(ProctoringSettings settings) { - var control = new ProctoringControl(); + if (settings.JitsiMeet.Enabled || settings.Zoom.Enabled) + { + var control = new ProctoringControl(logger.CloneFor(nameof(ProctoringControl))); - window = uiFactory.CreateProctoringWindow(control); - window.Show(); + control.EnsureCoreWebView2Async().ContinueWith(_ => + { + control.Dispatcher.Invoke(() => control.NavigateToString(LoadContent(settings))); + }); - // TODO - //var content = load Zoom page, replace //INDEX_JS//; + window = uiFactory.CreateProctoringWindow(control); + window.Show(); - //control.NavigateToString(content); + logger.Info($"Initialized proctoring with {(settings.JitsiMeet.Enabled ? "Jitsi Meet" : "Zoom")}."); + } + else + { + logger.Warn("Failed to initialize remote proctoring because no provider is enabled in the active configuration."); + } } public void Terminate() { window?.Close(); } + + private string LoadContent(ProctoringSettings settings) + { + var provider = settings.JitsiMeet.Enabled ? "JitsiMeet" : "Zoom"; + var assembly = Assembly.GetAssembly(typeof(ProctoringController)); + var path = $"{typeof(ProctoringController).Namespace}.{provider}.index.html"; + + using (var stream = assembly.GetManifestResourceStream(path)) + using (var reader = new StreamReader(stream)) + { + var html = reader.ReadToEnd(); + + if (settings.JitsiMeet.Enabled) + { + 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); + } + else if (settings.Zoom.Enabled) + { + html = html.Replace("%%_API_KEY_%%", settings.Zoom.ApiKey); + html = html.Replace("%%_API_SECRET_%%", settings.Zoom.ApiSecret); + html = html.Replace("123456789", Convert.ToString(settings.Zoom.MeetingNumber)); + html = html.Replace("%%_USER_NAME_%%", settings.Zoom.UserName); + } + + return html; + } + } } } diff --git a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj index 1c1bf007..c81eeb22 100644 --- a/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj +++ b/SafeExamBrowser.Proctoring/SafeExamBrowser.Proctoring.csproj @@ -95,12 +95,10 @@ <None Include="packages.config" /> </ItemGroup> <ItemGroup> - <Content Include="Zoom\index.html" /> - <Content Include="Zoom\index.js" /> - </ItemGroup> - <ItemGroup> - <Folder Include="Jitsi\" /> + <EmbeddedResource Include="JitsiMeet\index.html" /> + <EmbeddedResource Include="Zoom\index.html" /> </ItemGroup> + <ItemGroup /> <Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <Import Project="..\packages\Microsoft.Web.WebView2.1.0.705.50\build\Microsoft.Web.WebView2.targets" Condition="Exists('..\packages\Microsoft.Web.WebView2.1.0.705.50\build\Microsoft.Web.WebView2.targets')" /> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild"> diff --git a/SafeExamBrowser.Proctoring/Zoom/index.html b/SafeExamBrowser.Proctoring/Zoom/index.html index e6151f97..a29ac0e8 100644 --- a/SafeExamBrowser.Proctoring/Zoom/index.html +++ b/SafeExamBrowser.Proctoring/Zoom/index.html @@ -14,7 +14,100 @@ <script src="https://source.zoom.us/zoom-meeting-1.8.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"> - //INDEX_JS// + const API_KEY = "%%_API_KEY_%%"; + const API_SECRET = "%%_API_SECRET_%%"; + + console.log("Checking system requirements..."); + console.log(JSON.stringify(ZoomMtg.checkSystemRequirements())); + + console.log("Initializing Zoom..."); + ZoomMtg.setZoomJSLib('https://source.zoom.us/1.8.1/lib', '/av'); + ZoomMtg.preLoadWasm(); + ZoomMtg.prepareJssdk(); + + const config = { + meetingNumber: 123456789, + leaveUrl: 'https://google.ch', + userName: '%%_USER_NAME_%%', + /* passWord: 'password', // if required */ + role: 0 // 1 for host; 0 for attendee + }; + + const signature = ZoomMtg.generateSignature({ + meetingNumber: config.meetingNumber, + apiKey: API_KEY, + apiSecret: API_SECRET, + role: config.role, + error: function (res) { + console.error("FAILED TO GENERATE SIGNATURE: " + res) + }, + success: function (res) { + console.log("Successfully generated signature."); + console.log(res.result); + }, + }); + + console.log("Initializing meeting..."); + + // See documentation: https://zoom.github.io/sample-app-web/ZoomMtg.html#init + ZoomMtg.init({ + debug: true, //optional + leaveUrl: config.leaveUrl, //required + // webEndpoint: 'PSO web domain', // PSO option + showMeetingHeader: true, //option + disableInvite: false, //optional + disableCallOut: false, //optional + disableRecord: false, //optional + disableJoinAudio: false, //optional + audioPanelAlwaysOpen: true, //optional + showPureSharingContent: false, //optional + isSupportAV: true, //optional, + isSupportChat: false, //optional, + isSupportQA: true, //optional, + isSupportCC: true, //optional, + screenShare: true, //optional, + rwcBackup: '', //optional, + videoDrag: true, //optional, + sharingMode: 'both', //optional, + videoHeader: true, //optional, + isLockBottom: true, // optional, + isSupportNonverbal: true, // optional, + isShowJoiningErrorDialog: true, // optional, + inviteUrlFormat: '', // optional + loginWindow: { // optional, + width: 400, + height: 380 + }, + // meetingInfo: [ // optional + // 'topic', + // 'host', + // 'mn', + // 'pwd', + // 'telPwd', + // 'invite', + // 'participant', + // 'dc' + // ], + disableVoIP: false, // optional + disableReport: false, // optional + error: function (res) { + console.warn("INIT ERROR") + console.log(res) + }, + success: function () { + ZoomMtg.join({ + signature: signature, + apiKey: API_KEY, + meetingNumber: config.meetingNumber, + userName: config.userName, + /* passWord: meetConfig.passWord, */ + error(res) { + console.warn("JOIN ERROR") + console.log(res) + } + }) + } + }) </script> </body> </html> \ No newline at end of file diff --git a/SafeExamBrowser.Proctoring/Zoom/index.js b/SafeExamBrowser.Proctoring/Zoom/index.js deleted file mode 100644 index 9adba733..00000000 --- a/SafeExamBrowser.Proctoring/Zoom/index.js +++ /dev/null @@ -1,95 +0,0 @@ -const API_KEY = "..."; -const API_SECRET = "..."; - -console.log("Checking system requirements..."); -console.log(JSON.stringify(ZoomMtg.checkSystemRequirements())); - -console.log("Initializing Zoom..."); -ZoomMtg.setZoomJSLib('https://source.zoom.us/1.8.1/lib', '/av'); -ZoomMtg.preLoadWasm(); -ZoomMtg.prepareJssdk(); - -const config = { - meetingNumber: 123456, - leaveUrl: 'https://google.ch', - userName: 'Firstname Lastname', - userEmail: 'firstname.lastname@yoursite.com', - /* passWord: 'password', // if required */ - role: 0 // 1 for host; 0 for attendee -}; - -const signature = ZoomMtg.generateSignature({ - meetingNumber: config.meetingNumber, - apiKey: API_KEY, - apiSecret: API_SECRET, - role: config.role, - error: function (res) { - console.error("FAILED TO GENERATE SIGNATURE: " + res) - }, - success: function (res) { - console.log("Successfully generated signature."); - console.log(res.result); - }, -}); - -console.log("Initializing meeting..."); - -// See documentation: https://zoom.github.io/sample-app-web/ZoomMtg.html#init -ZoomMtg.init({ - debug: true, //optional - leaveUrl: config.leaveUrl, //required - // webEndpoint: 'PSO web domain', // PSO option - showMeetingHeader: true, //option - disableInvite: false, //optional - disableCallOut: false, //optional - disableRecord: false, //optional - disableJoinAudio: false, //optional - audioPanelAlwaysOpen: true, //optional - showPureSharingContent: false, //optional - isSupportAV: true, //optional, - isSupportChat: false, //optional, - isSupportQA: true, //optional, - isSupportCC: true, //optional, - screenShare: true, //optional, - rwcBackup: '', //optional, - videoDrag: true, //optional, - sharingMode: 'both', //optional, - videoHeader: true, //optional, - isLockBottom: true, // optional, - isSupportNonverbal: true, // optional, - isShowJoiningErrorDialog: true, // optional, - inviteUrlFormat: '', // optional - loginWindow: { // optional, - width: 400, - height: 380 - }, - // meetingInfo: [ // optional - // 'topic', - // 'host', - // 'mn', - // 'pwd', - // 'telPwd', - // 'invite', - // 'participant', - // 'dc' - // ], - disableVoIP: false, // optional - disableReport: false, // optional - error: function(res) { - console.warn("INIT ERROR") - console.log(res) - }, - success: function() { - ZoomMtg.join({ - signature: signature, - apiKey: API_KEY, - meetingNumber: config.meetingNumber, - userName: config.userName, - /* passWord: meetConfig.passWord, */ - error(res) { - console.warn("JOIN ERROR") - console.log(res) - } - }) - } -}) diff --git a/SafeExamBrowser.Settings/Proctoring/JitsiMeetSettings.cs b/SafeExamBrowser.Settings/Proctoring/JitsiMeetSettings.cs new file mode 100644 index 00000000..ff8bb029 --- /dev/null +++ b/SafeExamBrowser.Settings/Proctoring/JitsiMeetSettings.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; + +namespace SafeExamBrowser.Settings.Proctoring +{ + /// <summary> + /// All settings for the meeting provider Jitsi Meet. + /// </summary> + [Serializable] + public class JitsiMeetSettings + { + /// <summary> + /// Determines whether proctoring with Jitsi Meet is enabled. + /// </summary> + public bool Enabled { get; set; } + + /// <summary> + /// The name of the meeting room. + /// </summary> + public string RoomName { get; set; } + + /// <summary> + /// The URL of the Jitsi Meet server. + /// </summary> + public string ServerUrl { get; set; } + + /// <summary> + /// The subject of the meeting. + /// </summary> + public string Subject { get; set; } + + /// <summary> + /// The authentication token for the meeting. + /// </summary> + public string Token { get; set; } + } +} diff --git a/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs b/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs index 306c68e8..cb14ccb8 100644 --- a/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs +++ b/SafeExamBrowser.Settings/Proctoring/ProctoringSettings.cs @@ -20,5 +20,21 @@ namespace SafeExamBrowser.Settings.Proctoring /// Determines whether the entire remote proctoring feature is enabled. /// </summary> public bool Enabled { get; set; } + + /// <summary> + /// All settings for remote proctoring with Jitsi Meet. + /// </summary> + public JitsiMeetSettings JitsiMeet { get; set; } + + /// <summary> + /// All settings for remote proctoring with Zoom. + /// </summary> + public ZoomSettings Zoom { get; set; } + + public ProctoringSettings() + { + JitsiMeet = new JitsiMeetSettings(); + Zoom = new ZoomSettings(); + } } } diff --git a/SafeExamBrowser.Settings/Proctoring/ZoomSettings.cs b/SafeExamBrowser.Settings/Proctoring/ZoomSettings.cs new file mode 100644 index 00000000..85739958 --- /dev/null +++ b/SafeExamBrowser.Settings/Proctoring/ZoomSettings.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; + +namespace SafeExamBrowser.Settings.Proctoring +{ + /// <summary> + /// All settings for the meeting provider Zoom. + /// </summary> + [Serializable] + public class ZoomSettings + { + /// <summary> + /// The API key to be used for authentication. + /// </summary> + public string ApiKey { get; set; } + + /// <summary> + /// The API secret to be used for authentication. + /// </summary> + public string ApiSecret { get; set; } + + /// <summary> + /// Determines whether proctoring with Zoom is enabled. + /// </summary> + public bool Enabled { get; set; } + + /// <summary> + /// The number of the meeting. + /// </summary> + public int MeetingNumber { get; set; } + + /// <summary> + /// The user name to be used for the meeting. + /// </summary> + public string UserName { get; set; } + } +} diff --git a/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj b/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj index 5f85298c..6780fbb9 100644 --- a/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj +++ b/SafeExamBrowser.Settings/SafeExamBrowser.Settings.csproj @@ -71,7 +71,9 @@ <Compile Include="Browser\Proxy\ProxyProtocol.cs" /> <Compile Include="Browser\Proxy\ProxyConfiguration.cs" /> <Compile Include="ConfigurationMode.cs" /> + <Compile Include="Proctoring\JitsiMeetSettings.cs" /> <Compile Include="Proctoring\ProctoringSettings.cs" /> + <Compile Include="Proctoring\ZoomSettings.cs" /> <Compile Include="SessionMode.cs" /> <Compile Include="Security\KioskMode.cs" /> <Compile Include="Logging\LogLevel.cs" />