769 lines
23 KiB
C#
769 lines
23 KiB
C#
/*
|
|
* Copyright (c) 2022 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;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Net;
|
|
using System.Net.Http;
|
|
using System.Net.Http.Headers;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Timers;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using SafeExamBrowser.Configuration.Contracts;
|
|
using SafeExamBrowser.Logging.Contracts;
|
|
using SafeExamBrowser.Server.Contracts;
|
|
using SafeExamBrowser.Server.Contracts.Data;
|
|
using SafeExamBrowser.Server.Contracts.Events;
|
|
using SafeExamBrowser.Server.Data;
|
|
using SafeExamBrowser.Settings.Logging;
|
|
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 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 string connectionToken;
|
|
private bool connectedToPowergrid;
|
|
private int currentLockScreenId;
|
|
private int currentPowerSupplyValue;
|
|
private int currentRaisHandId;
|
|
private int currentWlanValue;
|
|
private string examId;
|
|
private HttpClient httpClient;
|
|
private int notificationId;
|
|
private string oauth2Token;
|
|
private int pingNumber;
|
|
private ServerSettings settings;
|
|
|
|
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,
|
|
ILogger logger,
|
|
ISystemInfo systemInfo,
|
|
IUserInfo userInfo,
|
|
IPowerSupply powerSupply = default,
|
|
INetworkAdapter networkAdapter = default)
|
|
{
|
|
this.api = new ApiVersion1();
|
|
this.appConfig = appConfig;
|
|
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 Connect()
|
|
{
|
|
var success = TryExecute(HttpMethod.Get, settings.ApiUrl, out var response);
|
|
var message = response.ToLogString();
|
|
|
|
if (success && parser.TryParseApi(response.Content, out api))
|
|
{
|
|
logger.Info("Successfully loaded server API.");
|
|
success = TryRetrieveOAuth2Token(out message);
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to load server API!");
|
|
}
|
|
|
|
return new ServerResponse(success, message);
|
|
}
|
|
|
|
public ServerResponse Disconnect()
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var content = "delete=true";
|
|
var contentType = "application/x-www-form-urlencoded";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
|
|
var success = TryExecute(HttpMethod.Delete, api.HandshakeEndpoint, out var response, content, contentType, authorization, token);
|
|
var message = response.ToLogString();
|
|
|
|
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 authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var clientInfo = $"client_id={userInfo.GetUserName()}&seb_machine_name={systemInfo.Name}";
|
|
var versionInfo = $"seb_os_name={systemInfo.OperatingSystemInfo}&seb_version={appConfig.ProgramInformationalVersion}";
|
|
var content = $"institutionId={settings.Institution}&{clientInfo}&{versionInfo}{(examId == default ? "" : $"&examId={examId}")}";
|
|
var contentType = "application/x-www-form-urlencoded";
|
|
var exams = default(IList<Exam>);
|
|
|
|
var success = TryExecute(HttpMethod.Post, api.HandshakeEndpoint, out var response, content, contentType, authorization);
|
|
var message = response.ToLogString();
|
|
|
|
if (success)
|
|
{
|
|
var hasExams = parser.TryParseExams(response.Content, out exams);
|
|
var hasToken = parser.TryParseConnectionToken(response, out connectionToken);
|
|
|
|
success = hasExams && hasToken;
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully retrieved connection token and available exams.");
|
|
}
|
|
else if (!hasExams)
|
|
{
|
|
logger.Error("Failed to retrieve available exams!");
|
|
}
|
|
else if (!hasToken)
|
|
{
|
|
logger.Error("Failed to retrieve connection token!");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to load connection token and available exams!");
|
|
}
|
|
|
|
return new ServerResponse<IEnumerable<Exam>>(success, exams, message);
|
|
}
|
|
|
|
public ServerResponse<Uri> GetConfigurationFor(Exam exam)
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
var uri = default(Uri);
|
|
|
|
var success = TryExecute(HttpMethod.Get, $"{api.ConfigurationEndpoint}?examId={exam.Id}", out var response, default, default, authorization, token);
|
|
var message = response.ToLogString();
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully retrieved exam configuration.");
|
|
|
|
success = fileSystem.TrySaveFile(response.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 = connectionToken,
|
|
Oauth2Token = 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.connectionToken = connectionToken;
|
|
this.examId = examId;
|
|
this.oauth2Token = oauth2Token;
|
|
|
|
Initialize(settings);
|
|
}
|
|
|
|
public ServerResponse LowerHand()
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var contentType = "application/json;charset=UTF-8";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
var json = new JObject
|
|
{
|
|
["type"] = "NOTIFICATION_CONFIRMED",
|
|
["timestamp"] = DateTime.Now.ToUnixTimestamp(),
|
|
["numericValue"] = currentRaisHandId,
|
|
};
|
|
var content = json.ToString();
|
|
var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent lower hand notification.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send lower hand notification!");
|
|
}
|
|
|
|
return new ServerResponse(success, response.ToLogString());
|
|
}
|
|
|
|
public ServerResponse ConfirmLockScreen()
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var contentType = "application/json;charset=UTF-8";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
var json = new JObject
|
|
{
|
|
["type"] = "NOTIFICATION_CONFIRMED",
|
|
["timestamp"] = DateTime.Now.ToUnixTimestamp(),
|
|
["numericValue"] = currentLockScreenId,
|
|
};
|
|
var content = json.ToString();
|
|
var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent lock screen confirm notification.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send lock screen confirm notification!");
|
|
}
|
|
|
|
return new ServerResponse(success, response.ToLogString());
|
|
}
|
|
|
|
public void Notify(ILogContent content)
|
|
{
|
|
logContent.Enqueue(content);
|
|
}
|
|
|
|
public ServerResponse RaiseHand(string message = null)
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var contentType = "application/json;charset=UTF-8";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
var json = new JObject
|
|
{
|
|
["type"] = "NOTIFICATION",
|
|
["timestamp"] = DateTime.Now.ToUnixTimestamp(),
|
|
["numericValue"] = currentRaisHandId = ++notificationId,
|
|
["text"] = $"<raisehand> {message}"
|
|
};
|
|
var content = json.ToString();
|
|
var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent raise hand notification.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send raise hand notification!");
|
|
}
|
|
|
|
return new ServerResponse(success, response.ToLogString());
|
|
}
|
|
|
|
public ServerResponse LockScreen(string message = null)
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var contentType = "application/json;charset=UTF-8";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
var json = new JObject
|
|
{
|
|
["type"] = "NOTIFICATION",
|
|
["timestamp"] = DateTime.Now.ToUnixTimestamp(),
|
|
["numericValue"] = currentLockScreenId = ++notificationId,
|
|
["text"] = $"<lockscreen> {message}"
|
|
};
|
|
var content = json.ToString();
|
|
var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token);
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent lock screen notification.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send lock screen notification!");
|
|
}
|
|
|
|
return new ServerResponse(success, response.ToLogString());
|
|
}
|
|
|
|
public ServerResponse SendSelectedExam(Exam exam)
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var content = $"examId={exam.Id}";
|
|
var contentType = "application/x-www-form-urlencoded";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
|
|
var success = TryExecute(new HttpMethod("PATCH"), api.HandshakeEndpoint, out var response, content, contentType, authorization, token);
|
|
var message = response.ToLogString();
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent selected exam.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send selected exam!");
|
|
}
|
|
|
|
if (parser.TryParseAppSignatureKeySalt(response, out var salt))
|
|
{
|
|
logger.Info("App signature key salt detected, performing key exchange...");
|
|
success = TrySendAppSignatureKey(out message);
|
|
}
|
|
else
|
|
{
|
|
logger.Info("No app signature key salt detected, skipping key exchange.");
|
|
}
|
|
|
|
return new ServerResponse(success, message);
|
|
}
|
|
|
|
public ServerResponse SendSessionIdentifier(string identifier)
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var content = $"examId={examId}&seb_user_session_id={identifier}";
|
|
var contentType = "application/x-www-form-urlencoded";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
|
|
var success = TryExecute(HttpMethod.Put, api.HandshakeEndpoint, out var response, content, contentType, authorization, token);
|
|
var message = response.ToLogString();
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent session identifier.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send session 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
|
|
{
|
|
var contentType = "application/json;charset=UTF-8";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
|
|
while (!logContent.IsEmpty)
|
|
{
|
|
if (logContent.TryDequeue(out var c) && c is ILogMessage message)
|
|
{
|
|
// IMPORTANT: The token needs to be read for every request, as it may get updated by another thread!
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var json = new JObject
|
|
{
|
|
["type"] = message.Severity.ToLogType(),
|
|
["timestamp"] = message.DateTime.ToUnixTimestamp(),
|
|
["text"] = message.Message
|
|
};
|
|
var content = json.ToString();
|
|
|
|
TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error("Failed to send log!", e);
|
|
}
|
|
|
|
logTimer.Start();
|
|
}
|
|
|
|
private void PingTimer_Elapsed(object sender, ElapsedEventArgs args)
|
|
{
|
|
try
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var content = $"timestamp={DateTime.Now.ToUnixTimestamp()}&ping-number={++pingNumber}";
|
|
var contentType = "application/x-www-form-urlencoded";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
|
|
if (instructionConfirmations.TryDequeue(out var confirmation))
|
|
{
|
|
content = $"{content}&instruction-confirm={confirmation}";
|
|
}
|
|
|
|
var success = TryExecute(HttpMethod.Post, api.PingEndpoint, out var response, content, contentType, authorization, token);
|
|
|
|
if (success)
|
|
{
|
|
if (parser.TryParseInstruction(response.Content, out var attributes, out var instruction, out var instructionConfirmation))
|
|
{
|
|
switch (instruction)
|
|
{
|
|
case Instructions.LOCK_SCREEN:
|
|
Task.Run(() => LockScreenRequested?.Invoke(attributes.Message));
|
|
break;
|
|
case Instructions.NOTIFICATION_CONFIRM when attributes.Type == "lockscreen":
|
|
Task.Run(() => LockScreenConfirmed?.Invoke());
|
|
break;
|
|
case Instructions.NOTIFICATION_CONFIRM when attributes.Type == "raisehand":
|
|
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;
|
|
}
|
|
|
|
if (instructionConfirmation != default)
|
|
{
|
|
instructionConfirmations.Enqueue(instructionConfirmation);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
logger.Error($"Failed to send ping: {response.ToLogString()}");
|
|
}
|
|
}
|
|
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 connected = status.IsOnline;
|
|
|
|
if (value != currentPowerSupplyValue)
|
|
{
|
|
var chargeInfo = $"{status.BatteryChargeStatus} at {value}%";
|
|
var gridInfo = $"{(status.IsOnline ? "connected to" : "disconnected from")} the power grid";
|
|
var text = $"<battery> {chargeInfo}, {status.BatteryTimeRemaining} remaining, {gridInfo}";
|
|
SendPowerSupplyStatus(text, value);
|
|
currentPowerSupplyValue = value;
|
|
}
|
|
else if (connected != connectedToPowergrid)
|
|
{
|
|
var text = $"<battery> Device has been {(connected ? "connected to" : "disconnected from")} power grid";
|
|
SendPowerSupplyStatus(text, value);
|
|
connectedToPowergrid = connected;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error("Failed to send power supply status!", e);
|
|
}
|
|
}
|
|
|
|
private void SendPowerSupplyStatus(string text, int value)
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var contentType = "application/json;charset=UTF-8";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
var json = new JObject
|
|
{
|
|
["type"] = LogLevel.Info.ToLogType(),
|
|
["timestamp"] = DateTime.Now.ToUnixTimestamp(),
|
|
["text"] = text,
|
|
["numericValue"] = value
|
|
};
|
|
var content = json.ToString();
|
|
|
|
TryExecute(HttpMethod.Post, api.LogEndpoint, out _, content, contentType, authorization, token);
|
|
}
|
|
|
|
private void NetworkAdapter_Changed()
|
|
{
|
|
const int NOT_CONNECTED = -1;
|
|
|
|
try
|
|
{
|
|
var network = networkAdapter.GetWirelessNetworks().FirstOrDefault(n => n.Status == ConnectionStatus.Connected);
|
|
|
|
if (network?.SignalStrength != currentWlanValue)
|
|
{
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var contentType = "application/json;charset=UTF-8";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
var json = new JObject { ["type"] = LogLevel.Info.ToLogType(), ["timestamp"] = DateTime.Now.ToUnixTimestamp() };
|
|
|
|
if (network != default(IWirelessNetwork))
|
|
{
|
|
json["text"] = $"<wlan> {network.Name}: {network.Status}, {network.SignalStrength}%";
|
|
json["numericValue"] = network.SignalStrength;
|
|
}
|
|
else
|
|
{
|
|
json["text"] = "<wlan> not connected";
|
|
}
|
|
|
|
TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, json.ToString(), contentType, authorization, token);
|
|
|
|
currentWlanValue = network?.SignalStrength ?? NOT_CONNECTED;
|
|
}
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Error("Failed to send wireless status!", e);
|
|
}
|
|
}
|
|
|
|
private bool TryRetrieveOAuth2Token(out string message)
|
|
{
|
|
var secret = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{settings.ClientName}:{settings.ClientSecret}"));
|
|
var authorization = ("Authorization", $"Basic {secret}");
|
|
var content = "grant_type=client_credentials&scope=read write";
|
|
var contentType = "application/x-www-form-urlencoded";
|
|
var success = TryExecute(HttpMethod.Post, api.AccessTokenEndpoint, out var response, content, contentType, authorization);
|
|
|
|
message = response.ToLogString();
|
|
|
|
if (success && parser.TryParseOauth2Token(response.Content, out oauth2Token))
|
|
{
|
|
logger.Info("Successfully retrieved OAuth2 token.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to retrieve OAuth2 token!");
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
private bool TrySendAppSignatureKey(out string message)
|
|
{
|
|
// TODO:
|
|
// keyGenerator.CalculateAppSignatureKey(configurationKey, server.AppSignatureKeySalt)
|
|
|
|
var authorization = ("Authorization", $"Bearer {oauth2Token}");
|
|
var content = $"seb_signature_key={"WINDOWS-TEST-ASK-1234"}";
|
|
var contentType = "application/x-www-form-urlencoded";
|
|
var token = ("SEBConnectionToken", connectionToken);
|
|
var success = TryExecute(new HttpMethod("PATCH"), api.HandshakeEndpoint, out var response, content, contentType, authorization, token);
|
|
|
|
message = response.ToLogString();
|
|
|
|
if (success)
|
|
{
|
|
logger.Info("Successfully sent app signature key.");
|
|
}
|
|
else
|
|
{
|
|
logger.Error("Failed to send app signature key!");
|
|
}
|
|
|
|
return success;
|
|
}
|
|
|
|
private bool TryExecute(
|
|
HttpMethod method,
|
|
string url,
|
|
out HttpResponseMessage response,
|
|
string content = default,
|
|
string contentType = default,
|
|
params (string name, string value)[] headers)
|
|
{
|
|
response = default;
|
|
|
|
for (var attempt = 0; attempt < settings.RequestAttempts && (response == default || !response.IsSuccessStatusCode); attempt++)
|
|
{
|
|
var request = BuildRequest(method, url, content, contentType, headers);
|
|
|
|
try
|
|
{
|
|
response = httpClient.SendAsync(request).GetAwaiter().GetResult();
|
|
|
|
if (request.RequestUri.AbsolutePath != api.LogEndpoint && request.RequestUri.AbsolutePath != api.PingEndpoint)
|
|
{
|
|
logger.Debug($"Completed request: {request.Method} '{request.RequestUri}' -> {response.ToLogString()}");
|
|
}
|
|
|
|
if (response.StatusCode == HttpStatusCode.Unauthorized && parser.IsTokenExpired(response.Content))
|
|
{
|
|
logger.Info("OAuth2 token has expired, attempting to retrieve new one...");
|
|
|
|
if (TryRetrieveOAuth2Token(out var message))
|
|
{
|
|
headers = UpdateOAuth2Token(headers);
|
|
}
|
|
}
|
|
}
|
|
catch (TaskCanceledException)
|
|
{
|
|
logger.Debug($"Request {request.Method} '{request.RequestUri}' did not complete within {settings.RequestTimeout}ms!");
|
|
break;
|
|
}
|
|
catch (Exception e)
|
|
{
|
|
logger.Debug($"Request {request.Method} '{request.RequestUri}' failed due to {e}");
|
|
}
|
|
}
|
|
|
|
return response != default && response.IsSuccessStatusCode;
|
|
}
|
|
|
|
private HttpRequestMessage BuildRequest(
|
|
HttpMethod method,
|
|
string url,
|
|
string content = default,
|
|
string contentType = default,
|
|
params (string name, string value)[] headers)
|
|
{
|
|
var request = new HttpRequestMessage(method, url);
|
|
|
|
if (content != default)
|
|
{
|
|
request.Content = new StringContent(content, Encoding.UTF8);
|
|
|
|
if (contentType != default)
|
|
{
|
|
request.Content.Headers.ContentType = MediaTypeHeaderValue.Parse(contentType);
|
|
}
|
|
}
|
|
|
|
request.Headers.Add("Accept", "application/json, */*");
|
|
|
|
foreach (var (name, value) in headers)
|
|
{
|
|
request.Headers.Add(name, value);
|
|
}
|
|
|
|
return request;
|
|
}
|
|
|
|
private (string name, string value)[] UpdateOAuth2Token((string name, string value)[] headers)
|
|
{
|
|
var result = new List<(string name, string value)>();
|
|
|
|
foreach (var header in headers)
|
|
{
|
|
if (header.name == "Authorization")
|
|
{
|
|
result.Add(("Authorization", $"Bearer {oauth2Token}"));
|
|
}
|
|
else
|
|
{
|
|
result.Add(header);
|
|
}
|
|
}
|
|
|
|
return result.ToArray();
|
|
}
|
|
}
|
|
}
|