seb-win-refactoring/SafeExamBrowser.Server/ServerProxy.cs

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