536 lines
15 KiB
C#
536 lines
15 KiB
C#
/*
|
|
* Copyright (c) 2024 ETH Zürich, IT Services
|
|
*
|
|
* 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.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net.Http;
|
|
using System.Threading.Tasks;
|
|
using System.Timers;
|
|
using Newtonsoft.Json;
|
|
using SafeExamBrowser.Configuration.Contracts;
|
|
using SafeExamBrowser.Configuration.Contracts.Cryptography;
|
|
using SafeExamBrowser.Logging.Contracts;
|
|
using SafeExamBrowser.Server.Contracts;
|
|
using SafeExamBrowser.Server.Contracts.Data;
|
|
using SafeExamBrowser.Server.Contracts.Events;
|
|
using SafeExamBrowser.Server.Contracts.Events.Proctoring;
|
|
using SafeExamBrowser.Server.Data;
|
|
using SafeExamBrowser.Server.Requests;
|
|
using SafeExamBrowser.Settings.Server;
|
|
using SafeExamBrowser.SystemComponents.Contracts;
|
|
using SafeExamBrowser.SystemComponents.Contracts.Network;
|
|
using SafeExamBrowser.SystemComponents.Contracts.PowerSupply;
|
|
using Timer = System.Timers.Timer;
|
|
|
|
namespace SafeExamBrowser.Server
|
|
{
|
|
public class ServerProxy : ILogObserver, IServerProxy
|
|
{
|
|
private readonly AppConfig appConfig;
|
|
private readonly FileSystem fileSystem;
|
|
private readonly ConcurrentQueue<string> instructionConfirmations;
|
|
private readonly IKeyGenerator keyGenerator;
|
|
private readonly ILogger logger;
|
|
private readonly ConcurrentQueue<ILogContent> logContent;
|
|
private readonly Timer logTimer;
|
|
private readonly Parser parser;
|
|
private readonly Timer pingTimer;
|
|
private readonly IPowerSupply powerSupply;
|
|
private readonly ISystemInfo systemInfo;
|
|
private readonly IUserInfo userInfo;
|
|
private readonly INetworkAdapter networkAdapter;
|
|
|
|
private ApiVersion1 api;
|
|
private int currentHandId;
|
|
private int currentLockScreenId;
|
|
private string examId;
|
|
private HttpClient httpClient;
|
|
private int notificationId;
|
|
private int pingNumber;
|
|
private bool powerSupplyConnected;
|
|
private int powerSupplyValue;
|
|
private ServerSettings settings;
|
|
private int wirelessNetworkValue;
|
|
|
|
public event ServerEventHandler HandConfirmed;
|
|
public event ServerEventHandler LockScreenConfirmed;
|
|
public event ProctoringConfigurationReceivedEventHandler ProctoringConfigurationReceived;
|
|
public event ProctoringInstructionReceivedEventHandler ProctoringInstructionReceived;
|
|
public event TerminationRequestedEventHandler TerminationRequested;
|
|
public event LockScreenRequestedEventHandler LockScreenRequested;
|
|
|
|
public ServerProxy(
|
|
AppConfig appConfig,
|
|
IKeyGenerator keyGenerator,
|
|
ILogger logger,
|
|
ISystemInfo systemInfo,
|
|
IUserInfo userInfo,
|
|
IPowerSupply powerSupply = default,
|
|
INetworkAdapter networkAdapter = default)
|
|
{
|
|
this.api = new ApiVersion1();
|
|
this.appConfig = appConfig;
|
|
this.keyGenerator = keyGenerator;
|
|
this.fileSystem = new FileSystem(appConfig, logger);
|
|
this.instructionConfirmations = new ConcurrentQueue<string>();
|
|
this.logger = logger;
|
|
this.logContent = new ConcurrentQueue<ILogContent>();
|
|
this.logTimer = new Timer();
|
|
this.networkAdapter = networkAdapter;
|
|
this.parser = new Parser(logger);
|
|
this.pingTimer = new Timer();
|
|
this.powerSupply = powerSupply;
|
|
this.systemInfo = systemInfo;
|
|
this.userInfo = userInfo;
|
|
}
|
|
|
|
public ServerResponse ConfirmLockScreen()
|
|
{
|
|
var request = new ConfirmLockScreenRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(currentLockScreenId, out var message);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info($"Successfully sent notification confirmation for lock screen #{currentLockScreenId}.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error($"Failed to send notification confirmation for lock screen #{currentLockScreenId}!");
|
|
}
|
|
|
|
return new ServerResponse(success, message);
|
|
}
|
|
|
|
public ServerResponse Connect()
|
|
{
|
|
var request = new ApiRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(out api, out var message);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully loaded server API.");
|
|
success = new OAuth2TokenRequest(api, httpClient, logger, parser, settings).TryExecute(out message);
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to load server API!");
|
|
}
|
|
|
|
return new ServerResponse(success, message);
|
|
}
|
|
|
|
public ServerResponse Disconnect()
|
|
{
|
|
var request = new DisconnectionRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(out var message);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully terminated connection.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to terminate connection!");
|
|
}
|
|
|
|
return new ServerResponse(success, message);
|
|
}
|
|
|
|
public ServerResponse<IEnumerable<Exam>> GetAvailableExams(string examId = default)
|
|
{
|
|
var request = new AvailableExamsRequest(api, appConfig, httpClient, logger, parser, settings, systemInfo, userInfo);
|
|
var success = request.TryExecute(examId, out var exams, out var message);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully retrieved available exams.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to retrieve available exams!");
|
|
}
|
|
|
|
return new ServerResponse<IEnumerable<Exam>>(success, exams, message);
|
|
}
|
|
|
|
public ServerResponse<Uri> GetConfigurationFor(Exam exam)
|
|
{
|
|
var request = new ExamConfigurationRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(exam, out var content, out var message);
|
|
var uri = default(Uri);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully retrieved exam configuration.");
|
|
|
|
success = fileSystem.TrySaveFile(content, out uri);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info($"Successfully saved exam configuration as '{uri}'.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to save exam configuration!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to retrieve exam configuration!");
|
|
}
|
|
|
|
return new ServerResponse<Uri>(success, uri, message);
|
|
}
|
|
|
|
public ConnectionInfo GetConnectionInfo()
|
|
{
|
|
return new ConnectionInfo
|
|
{
|
|
Api = JsonConvert.SerializeObject(api),
|
|
ConnectionToken = BaseRequest.ConnectionToken,
|
|
Oauth2Token = BaseRequest.Oauth2Token
|
|
};
|
|
}
|
|
|
|
public void Initialize(ServerSettings settings)
|
|
{
|
|
this.settings = settings;
|
|
|
|
httpClient = new HttpClient();
|
|
httpClient.BaseAddress = new Uri(settings.ServerUrl);
|
|
|
|
if (settings.RequestTimeout > 0)
|
|
{
|
|
httpClient.Timeout = TimeSpan.FromMilliseconds(settings.RequestTimeout);
|
|
}
|
|
}
|
|
|
|
public void Initialize(string api, string connectionToken, string examId, string oauth2Token, ServerSettings settings)
|
|
{
|
|
this.api = JsonConvert.DeserializeObject<ApiVersion1>(api);
|
|
this.examId = examId;
|
|
|
|
BaseRequest.ConnectionToken = connectionToken;
|
|
BaseRequest.Oauth2Token = oauth2Token;
|
|
|
|
Initialize(settings);
|
|
}
|
|
|
|
public ServerResponse LockScreen(string text = null)
|
|
{
|
|
var request = new LockScreenRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(currentLockScreenId = ++notificationId, text, out var message);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info($"Successfully sent notification for lock screen #{currentLockScreenId}.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error($"Failed to send notification for lock screen #{currentLockScreenId}!");
|
|
}
|
|
|
|
return new ServerResponse(success, message);
|
|
}
|
|
|
|
public ServerResponse LowerHand()
|
|
{
|
|
var request = new LowerHandRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(currentHandId, out var message);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent lower hand notification.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send lower hand notification!");
|
|
}
|
|
|
|
return new ServerResponse(success, message);
|
|
}
|
|
|
|
public void Notify(ILogContent content)
|
|
{
|
|
logContent.Enqueue(content);
|
|
}
|
|
|
|
public ServerResponse RaiseHand(string text = null)
|
|
{
|
|
var request = new RaiseHandRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(currentHandId = ++notificationId, text, out var message);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent raise hand notification.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send raise hand notification!");
|
|
}
|
|
|
|
return new ServerResponse(success, message);
|
|
}
|
|
|
|
public ServerResponse<string> SendSelectedExam(Exam exam)
|
|
{
|
|
var request = new SelectExamRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(exam, out var message, out var appSignatureKeySalt, out var browserExamKey);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent selected exam.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send selected exam!");
|
|
}
|
|
|
|
if (success && appSignatureKeySalt != default)
|
|
{
|
|
logger.Info("App signature key salt detected, performing key exchange...");
|
|
success = TrySendAppSignatureKey(appSignatureKeySalt, out message);
|
|
}
|
|
else
|
|
{
|
|
logger.Info("No app signature key salt detected, skipping key exchange.");
|
|
}
|
|
|
|
if (browserExamKey != default)
|
|
{
|
|
logger.Info("Custom browser exam key detected.");
|
|
}
|
|
|
|
return new ServerResponse<string>(success, browserExamKey, message);
|
|
}
|
|
|
|
public ServerResponse SendUserIdentifier(string identifier)
|
|
{
|
|
var request = new UserIdentifierRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(examId, identifier, out var message);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent user identifier.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send user identifier!");
|
|
}
|
|
|
|
return new ServerResponse(success, message);
|
|
}
|
|
|
|
public void StartConnectivity()
|
|
{
|
|
foreach (var item in logger.GetLog())
|
|
{
|
|
logContent.Enqueue(item);
|
|
}
|
|
|
|
logger.Subscribe(this);
|
|
logTimer.AutoReset = false;
|
|
logTimer.Elapsed += LogTimer_Elapsed;
|
|
logTimer.Interval = 500;
|
|
logTimer.Start();
|
|
logger.Info("Started sending log items.");
|
|
|
|
pingTimer.AutoReset = false;
|
|
pingTimer.Elapsed += PingTimer_Elapsed;
|
|
pingTimer.Interval = settings.PingInterval;
|
|
pingTimer.Start();
|
|
logger.Info("Started sending pings.");
|
|
|
|
if (powerSupply != default && networkAdapter != default)
|
|
{
|
|
powerSupply.StatusChanged += PowerSupply_StatusChanged;
|
|
networkAdapter.Changed += NetworkAdapter_Changed;
|
|
logger.Info("Started monitoring system components.");
|
|
}
|
|
}
|
|
|
|
public void StopConnectivity()
|
|
{
|
|
if (powerSupply != default && networkAdapter != default)
|
|
{
|
|
powerSupply.StatusChanged -= PowerSupply_StatusChanged;
|
|
networkAdapter.Changed -= NetworkAdapter_Changed;
|
|
logger.Info("Stopped monitoring system components.");
|
|
}
|
|
|
|
logger.Unsubscribe(this);
|
|
logTimer.Stop();
|
|
logTimer.Elapsed -= LogTimer_Elapsed;
|
|
logger.Info("Stopped sending log items.");
|
|
|
|
pingTimer.Stop();
|
|
pingTimer.Elapsed -= PingTimer_Elapsed;
|
|
logger.Info("Stopped sending pings.");
|
|
}
|
|
|
|
private void LogTimer_Elapsed(object sender, ElapsedEventArgs args)
|
|
{
|
|
try
|
|
{
|
|
while (!logContent.IsEmpty)
|
|
{
|
|
if (logContent.TryDequeue(out var c) && c is ILogMessage message)
|
|
{
|
|
new LogRequest(api, httpClient, logger, parser, settings).TryExecute(message);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error("Failed to send log!", e);
|
|
}
|
|
|
|
logTimer.Start();
|
|
}
|
|
|
|
private void PingTimer_Elapsed(object sender, ElapsedEventArgs args)
|
|
{
|
|
try
|
|
{
|
|
instructionConfirmations.TryDequeue(out var confirmation);
|
|
|
|
var request = new PingRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(++pingNumber, out var content, out var message, confirmation);
|
|
|
|
if (success)
|
|
{
|
|
if (parser.TryParseInstruction(content, out var attributes, out var instruction, out var instructionConfirmation))
|
|
{
|
|
HandleInstruction(attributes, instruction);
|
|
|
|
if (instructionConfirmation != default)
|
|
{
|
|
instructionConfirmations.Enqueue(instructionConfirmation);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logger.Warn($"Failed to send ping: {message}!");
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error("Failed to send ping!", e);
|
|
}
|
|
|
|
pingTimer.Start();
|
|
}
|
|
|
|
private void PowerSupply_StatusChanged(IPowerSupplyStatus status)
|
|
{
|
|
try
|
|
{
|
|
var value = Convert.ToInt32(status.BatteryCharge * 100);
|
|
var statusChanged = powerSupplyConnected != status.IsOnline;
|
|
var valueChanged = powerSupplyValue != value;
|
|
|
|
if (statusChanged || valueChanged)
|
|
{
|
|
var request = new PowerSupplyRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(status, powerSupplyConnected, powerSupplyValue, out var message);
|
|
|
|
if (success)
|
|
{
|
|
powerSupplyConnected = status.IsOnline;
|
|
powerSupplyValue = value;
|
|
}
|
|
else
|
|
{
|
|
logger.Warn($"Failed to send power supply status: {message}!");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error("Failed to send power supply status!", e);
|
|
}
|
|
}
|
|
|
|
private void NetworkAdapter_Changed()
|
|
{
|
|
const int NOT_CONNECTED = -1;
|
|
|
|
try
|
|
{
|
|
var network = networkAdapter.GetWirelessNetworks().FirstOrDefault(n => n.Status == ConnectionStatus.Connected);
|
|
var statusChanged = network == default && wirelessNetworkValue != NOT_CONNECTED;
|
|
var valueChanged = network != default && Math.Abs(network.SignalStrength - wirelessNetworkValue) >= 5;
|
|
|
|
if (statusChanged || valueChanged)
|
|
{
|
|
var request = new NetworkAdapterRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(network, out var message);
|
|
|
|
if (success)
|
|
{
|
|
wirelessNetworkValue = network?.SignalStrength ?? NOT_CONNECTED;
|
|
}
|
|
else
|
|
{
|
|
logger.Warn($"Failed to send wireless status: {message}!");
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error("Failed to send wireless status!", e);
|
|
}
|
|
}
|
|
|
|
private void HandleInstruction(Attributes attributes, string instruction)
|
|
{
|
|
switch (instruction)
|
|
{
|
|
case Instructions.LOCK_SCREEN:
|
|
Task.Run(() => LockScreenRequested?.Invoke(attributes.Message));
|
|
break;
|
|
case Instructions.NOTIFICATION_CONFIRM when attributes.Type == AttributeType.LockScreen:
|
|
Task.Run(() => LockScreenConfirmed?.Invoke());
|
|
break;
|
|
case Instructions.NOTIFICATION_CONFIRM when attributes.Type == AttributeType.Hand:
|
|
Task.Run(() => HandConfirmed?.Invoke());
|
|
break;
|
|
case Instructions.PROCTORING:
|
|
Task.Run(() => ProctoringInstructionReceived?.Invoke(attributes.Instruction));
|
|
break;
|
|
case Instructions.PROCTORING_RECONFIGURATION:
|
|
Task.Run(() => ProctoringConfigurationReceived?.Invoke(attributes.AllowChat, attributes.ReceiveAudio, attributes.ReceiveVideo));
|
|
break;
|
|
case Instructions.QUIT:
|
|
Task.Run(() => TerminationRequested?.Invoke());
|
|
break;
|
|
}
|
|
}
|
|
|
|
private bool TrySendAppSignatureKey(string salt, out string message)
|
|
{
|
|
var appSignatureKey = keyGenerator.CalculateAppSignatureKey(BaseRequest.ConnectionToken, salt);
|
|
var request = new AppSignatureKeyRequest(api, httpClient, logger, parser, settings);
|
|
var success = request.TryExecute(appSignatureKey, out message);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent app signature key.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send app signature key!");
|
|
}
|
|
|
|
return success;
|
|
}
|
|
}
|
|
}
|