SEBWIN-449: Implemented basic functionality of Jitsi Meet.

This commit is contained in:
Damian Büchel 2021-03-10 21:26:45 +01:00
parent e72456b79e
commit 52217fa477
13 changed files with 358 additions and 113 deletions

View file

@ -249,7 +249,7 @@ namespace SafeExamBrowser.Client
private IOperation BuildProctoringOperation() private IOperation BuildProctoringOperation()
{ {
var controller = new ProctoringController(uiFactory); var controller = new ProctoringController(ModuleLogger(nameof(ProctoringController)), uiFactory);
var operation = new ProctoringOperation(context, logger, controller); var operation = new ProctoringOperation(context, logger, controller);
context.ProctoringController = controller; context.ProctoringController = controller;

View file

@ -17,7 +17,50 @@ namespace SafeExamBrowser.Configuration.ConfigurationData.DataMapping
{ {
switch (key) 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;
} }
} }

View file

@ -219,6 +219,10 @@ namespace SafeExamBrowser.Configuration.ConfigurationData
internal static class JitsiMeet internal static class JitsiMeet
{ {
internal const string Enabled = "jitsiMeetEnable"; 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 internal static class Zoom

View file

@ -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>

View file

@ -6,17 +6,46 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 Microsoft.Web.WebView2.Wpf;
using SafeExamBrowser.Logging.Contracts;
using SafeExamBrowser.UserInterface.Contracts.Proctoring; using SafeExamBrowser.UserInterface.Contracts.Proctoring;
namespace SafeExamBrowser.Proctoring namespace SafeExamBrowser.Proctoring
{ {
internal class ProctoringControl : WebView2, IProctoringControl 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);
}
} }
} }
} }

View file

@ -6,6 +6,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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.Proctoring.Contracts;
using SafeExamBrowser.Settings.Proctoring; using SafeExamBrowser.Settings.Proctoring;
using SafeExamBrowser.UserInterface.Contracts; using SafeExamBrowser.UserInterface.Contracts;
@ -15,31 +19,72 @@ namespace SafeExamBrowser.Proctoring
{ {
public class ProctoringController : IProctoringController public class ProctoringController : IProctoringController
{ {
private readonly IModuleLogger logger;
private readonly IUserInterfaceFactory uiFactory; private readonly IUserInterfaceFactory uiFactory;
private IProctoringWindow window; private IProctoringWindow window;
public ProctoringController(IUserInterfaceFactory uiFactory) public ProctoringController(IModuleLogger logger, IUserInterfaceFactory uiFactory)
{ {
this.logger = logger;
this.uiFactory = uiFactory; this.uiFactory = uiFactory;
} }
public void Initialize(ProctoringSettings settings) 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); control.EnsureCoreWebView2Async().ContinueWith(_ =>
window.Show(); {
control.Dispatcher.Invoke(() => control.NavigateToString(LoadContent(settings)));
});
// TODO window = uiFactory.CreateProctoringWindow(control);
//var content = load Zoom page, replace //INDEX_JS//; 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() public void Terminate()
{ {
window?.Close(); 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;
}
}
} }
} }

View file

@ -95,12 +95,10 @@
<None Include="packages.config" /> <None Include="packages.config" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<Content Include="Zoom\index.html" /> <EmbeddedResource Include="JitsiMeet\index.html" />
<Content Include="Zoom\index.js" /> <EmbeddedResource Include="Zoom\index.html" />
</ItemGroup>
<ItemGroup>
<Folder Include="Jitsi\" />
</ItemGroup> </ItemGroup>
<ItemGroup />
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" /> <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')" /> <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"> <Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">

View file

@ -14,7 +14,100 @@
<script src="https://source.zoom.us/zoom-meeting-1.8.1.min.js"></script> <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 src="https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9/crypto-js.min.js"></script>
<script type="text/javascript"> <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> </script>
</body> </body>
</html> </html>

View file

@ -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)
}
})
}
})

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

View file

@ -20,5 +20,21 @@ namespace SafeExamBrowser.Settings.Proctoring
/// Determines whether the entire remote proctoring feature is enabled. /// Determines whether the entire remote proctoring feature is enabled.
/// </summary> /// </summary>
public bool Enabled { get; set; } 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();
}
} }
} }

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

View file

@ -71,7 +71,9 @@
<Compile Include="Browser\Proxy\ProxyProtocol.cs" /> <Compile Include="Browser\Proxy\ProxyProtocol.cs" />
<Compile Include="Browser\Proxy\ProxyConfiguration.cs" /> <Compile Include="Browser\Proxy\ProxyConfiguration.cs" />
<Compile Include="ConfigurationMode.cs" /> <Compile Include="ConfigurationMode.cs" />
<Compile Include="Proctoring\JitsiMeetSettings.cs" />
<Compile Include="Proctoring\ProctoringSettings.cs" /> <Compile Include="Proctoring\ProctoringSettings.cs" />
<Compile Include="Proctoring\ZoomSettings.cs" />
<Compile Include="SessionMode.cs" /> <Compile Include="SessionMode.cs" />
<Compile Include="Security\KioskMode.cs" /> <Compile Include="Security\KioskMode.cs" />
<Compile Include="Logging\LogLevel.cs" /> <Compile Include="Logging\LogLevel.cs" />