SEBWIN-762: Added user identifier detection via Moodle plugin and overall renamed session to user identifier.

This commit is contained in:
Damian Büchel 2023-11-01 13:52:39 +01:00
parent 8c3d9a31d7
commit 751bfcb144
13 changed files with 173 additions and 125 deletions

View file

@ -9,7 +9,7 @@
namespace SafeExamBrowser.Browser.Contracts.Events
{
/// <summary>
/// Event handler used to indicate that the browser has detected a session identifier of a LMS.
/// Event handler used to indicate that the browser has detected a user identifier of an LMS.
/// </summary>
public delegate void SessionIdentifierDetectedEventHandler(string identifier);
public delegate void UserIdentifierDetectedEventHandler(string identifier);
}

View file

@ -22,9 +22,9 @@ namespace SafeExamBrowser.Browser.Contracts
event DownloadRequestedEventHandler ConfigurationDownloadRequested;
/// <summary>
/// Event fired when the browser application detects a session identifier of an LMS.
/// Event fired when the user tries to focus the taskbar.
/// </summary>
event SessionIdentifierDetectedEventHandler SessionIdentifierDetected;
event LoseFocusRequestedEventHandler LoseFocusRequested;
/// <summary>
/// Event fired when the browser application detects a request to terminate SEB.
@ -32,9 +32,9 @@ namespace SafeExamBrowser.Browser.Contracts
event TerminationRequestedEventHandler TerminationRequested;
/// <summary>
/// Event fired when the user tries to focus the taskbar.
/// Event fired when the browser application detects a user identifier of an LMS.
/// </summary>
event LoseFocusRequestedEventHandler LoseFocusRequested;
event UserIdentifierDetectedEventHandler UserIdentifierDetected;
/// <summary>
/// Transfers the focus to the browser application. If the parameter is <c>true</c>, the first focusable element in the browser window

View file

@ -60,7 +60,7 @@
<Compile Include="Events\DownloadRequestedEventHandler.cs" />
<Compile Include="Events\TabPressedEventHandler.cs" />
<Compile Include="Events\LoseFocusRequestedEventHandler.cs" />
<Compile Include="Events\SessionIdentifierDetectedEventHandler.cs" />
<Compile Include="Events\UserIdentifierDetectedEventHandler.cs" />
<Compile Include="Events\TerminationRequestedEventHandler.cs" />
<Compile Include="Filters\IRequestFilter.cs" />
<Compile Include="Filters\IRule.cs" />

View file

@ -166,7 +166,7 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers
[TestMethod]
public void MustLetOperatingSystemHandleUnknownProtocols()
{
Assert.IsTrue(sut.OnProtocolExecution(default(IWebBrowser), default(IBrowser), default(IFrame), default(IRequest)));
Assert.IsTrue(sut.OnProtocolExecution(default, default, default, default));
}
[TestMethod]
@ -232,28 +232,28 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers
var newUrl = default(string);
var request = new Mock<IRequest>();
var response = new Mock<IResponse>();
var sessionId = default(string);
var userId = default(string);
headers.Add("X-LMS-USER-ID", "some-session-id-123");
request.SetupGet(r => r.Url).Returns("https://www.somelms.org");
response.SetupGet(r => r.Headers).Returns(headers);
sut.SessionIdentifierDetected += (id) =>
sut.UserIdentifierDetected += (id) =>
{
sessionId = id;
userId = id;
@event.Set();
};
sut.OnResourceRedirect(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), Mock.Of<IRequest>(), response.Object, ref newUrl);
@event.WaitOne();
Assert.AreEqual("some-session-id-123", sessionId);
Assert.AreEqual("some-session-id-123", userId);
headers.Clear();
headers.Add("X-LMS-USER-ID", "other-session-id-123");
sessionId = default(string);
userId = default;
sut.OnResourceResponse(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, response.Object);
@event.WaitOne();
Assert.AreEqual("other-session-id-123", sessionId);
Assert.AreEqual("other-session-id-123", userId);
}
[TestMethod]
@ -264,28 +264,28 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers
var newUrl = default(string);
var request = new Mock<IRequest>();
var response = new Mock<IResponse>();
var sessionId = default(string);
var userId = default(string);
headers.Add("Set-Cookie", "edx-user-info=\"{\\\"username\\\": \\\"edx-123\\\"}\"; expires");
request.SetupGet(r => r.Url).Returns("https://www.somelms.org");
response.SetupGet(r => r.Headers).Returns(headers);
sut.SessionIdentifierDetected += (id) =>
sut.UserIdentifierDetected += (id) =>
{
sessionId = id;
userId = id;
@event.Set();
};
sut.OnResourceRedirect(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), Mock.Of<IRequest>(), response.Object, ref newUrl);
@event.WaitOne();
Assert.AreEqual("edx-123", sessionId);
Assert.AreEqual("edx-123", userId);
headers.Clear();
headers.Add("Set-Cookie", "edx-user-info=\"{\\\"username\\\": \\\"edx-345\\\"}\"; expires");
sessionId = default(string);
userId = default;
sut.OnResourceResponse(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, response.Object);
@event.WaitOne();
Assert.AreEqual("edx-345", sessionId);
Assert.AreEqual("edx-345", userId);
}
[TestMethod]
@ -296,28 +296,28 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers
var newUrl = default(string);
var request = new Mock<IRequest>();
var response = new Mock<IResponse>();
var sessionId = default(string);
var userId = default(string);
headers.Add("Location", "https://www.some-moodle-instance.org/moodle/login/index.php?testsession=123");
request.SetupGet(r => r.Url).Returns("https://www.some-moodle-instance.org");
response.SetupGet(r => r.Headers).Returns(headers);
sut.SessionIdentifierDetected += (id) =>
sut.UserIdentifierDetected += (id) =>
{
sessionId = id;
userId = id;
@event.Set();
};
sut.OnResourceRedirect(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), Mock.Of<IRequest>(), response.Object, ref newUrl);
@event.WaitOne();
Assert.AreEqual("123", sessionId);
Assert.AreEqual("123", userId);
headers.Clear();
headers.Add("Location", "https://www.some-moodle-instance.org/moodle/login/index.php?testsession=456");
sessionId = default(string);
userId = default;
sut.OnResourceResponse(Mock.Of<IWebBrowser>(), Mock.Of<IBrowser>(), Mock.Of<IFrame>(), request.Object, response.Object);
@event.WaitOne();
Assert.AreEqual("456", sessionId);
Assert.AreEqual("456", userId);
}
private class TestableResourceHandler : ResourceHandler

View file

@ -59,9 +59,9 @@ namespace SafeExamBrowser.Browser
public string Tooltip { get; private set; }
public event DownloadRequestedEventHandler ConfigurationDownloadRequested;
public event SessionIdentifierDetectedEventHandler SessionIdentifierDetected;
public event LoseFocusRequestedEventHandler LoseFocusRequested;
public event TerminationRequestedEventHandler TerminationRequested;
public event UserIdentifierDetectedEventHandler UserIdentifierDetected;
public event WindowsChangedEventHandler WindowsChanged;
public BrowserApplication(
@ -209,7 +209,7 @@ namespace SafeExamBrowser.Browser
window.ConfigurationDownloadRequested += (f, a) => ConfigurationDownloadRequested?.Invoke(f, a);
window.PopupRequested += Window_PopupRequested;
window.ResetRequested += Window_ResetRequested;
window.SessionIdentifierDetected += (i) => SessionIdentifierDetected?.Invoke(i);
window.UserIdentifierDetected += (i) => UserIdentifierDetected?.Invoke(i);
window.TerminationRequested += () => TerminationRequested?.Invoke();
window.LoseFocusRequested += (forward) => LoseFocusRequested?.Invoke(forward);

View file

@ -81,11 +81,11 @@ namespace SafeExamBrowser.Browser
internal event WindowClosedEventHandler Closed;
internal event DownloadRequestedEventHandler ConfigurationDownloadRequested;
internal event LoseFocusRequestedEventHandler LoseFocusRequested;
internal event PopupRequestedEventHandler PopupRequested;
internal event ResetRequestedEventHandler ResetRequested;
internal event SessionIdentifierDetectedEventHandler SessionIdentifierDetected;
internal event LoseFocusRequestedEventHandler LoseFocusRequested;
internal event TerminationRequestedEventHandler TerminationRequested;
internal event UserIdentifierDetectedEventHandler UserIdentifierDetected;
public event IconChangedEventHandler IconChanged;
public event TitleChangedEventHandler TitleChanged;
@ -185,9 +185,9 @@ namespace SafeExamBrowser.Browser
keyboardHandler.ZoomInRequested += ZoomInRequested;
keyboardHandler.ZoomOutRequested += ZoomOutRequested;
keyboardHandler.ZoomResetRequested += ZoomResetRequested;
resourceHandler.SessionIdentifierDetected += (id) => SessionIdentifierDetected?.Invoke(id);
requestHandler.QuitUrlVisited += RequestHandler_QuitUrlVisited;
requestHandler.RequestBlocked += RequestHandler_RequestBlocked;
resourceHandler.UserIdentifierDetected += (id) => UserIdentifierDetected?.Invoke(id);
InitializeRequestFilter(requestFilter);

View file

@ -44,9 +44,9 @@ namespace SafeExamBrowser.Browser.Handlers
private IResourceHandler contentHandler;
private IResourceHandler pageHandler;
private string sessionIdentifier;
private string userIdentifier;
internal event SessionIdentifierDetectedEventHandler SessionIdentifierDetected;
internal event UserIdentifierDetectedEventHandler UserIdentifierDetected;
internal ResourceHandler(
AppConfig appConfig,
@ -100,7 +100,7 @@ namespace SafeExamBrowser.Browser.Handlers
{
if (sessionMode == SessionMode.Server)
{
SearchSessionIdentifiers(request, response);
SearchUserIdentifier(request, response);
}
base.OnResourceRedirect(chromiumWebBrowser, browser, frame, request, response, ref newUrl);
@ -117,7 +117,7 @@ namespace SafeExamBrowser.Browser.Handlers
if (sessionMode == SessionMode.Server)
{
SearchSessionIdentifiers(request, response);
SearchUserIdentifier(request, response);
}
return base.OnResourceResponse(webBrowser, browser, frame, request, response);
@ -233,41 +233,46 @@ namespace SafeExamBrowser.Browser.Handlers
}
}
private void SearchSessionIdentifiers(IRequest request, IResponse response)
private void SearchUserIdentifier(IRequest request, IResponse response)
{
var success = TrySearchGenericSessionIdentifier(response);
var success = TrySearchGenericUserIdentifier(response);
if (!success)
{
SearchEdxIdentifier(response);
SearchMoodleIdentifier(request, response);
success = TrySearchEdxUserIdentifier(response);
}
if (!success)
{
TrySearchMoodleUserIdentifier(request, response);
}
}
private bool TrySearchGenericSessionIdentifier(IResponse response)
private bool TrySearchGenericUserIdentifier(IResponse response)
{
var ids = response.Headers.GetValues("X-LMS-USER-ID");
var success = false;
if (ids != default(string[]))
{
var userId = ids.FirstOrDefault();
if (userId != default && sessionIdentifier != userId)
if (userId != default && userIdentifier != userId)
{
sessionIdentifier = userId;
Task.Run(() => SessionIdentifierDetected?.Invoke(sessionIdentifier));
logger.Info("Generic LMS session detected.");
return true;
userIdentifier = userId;
Task.Run(() => UserIdentifierDetected?.Invoke(userIdentifier));
logger.Info("Generic LMS user identifier detected.");
success = true;
}
}
return false;
return success;
}
private void SearchEdxIdentifier(IResponse response)
private bool TrySearchEdxUserIdentifier(IResponse response)
{
var cookies = response.Headers.GetValues("Set-Cookie");
var success = false;
if (cookies != default(string[]))
{
@ -284,32 +289,42 @@ namespace SafeExamBrowser.Browser.Handlers
var json = JsonConvert.DeserializeObject(sanitized) as JObject;
var userName = json["username"].Value<string>();
if (sessionIdentifier != userName)
if (userIdentifier != userName)
{
sessionIdentifier = userName;
Task.Run(() => SessionIdentifierDetected?.Invoke(sessionIdentifier));
logger.Info("EdX session detected.");
userIdentifier = userName;
Task.Run(() => UserIdentifierDetected?.Invoke(userIdentifier));
logger.Info("EdX user identifier detected.");
success = true;
}
}
}
catch (Exception e)
{
logger.Error("Failed to parse edX session identifier!", e);
logger.Error("Failed to parse edX user identifier!", e);
}
}
return success;
}
private void SearchMoodleIdentifier(IRequest request, IResponse response)
private bool TrySearchMoodleUserIdentifier(IRequest request, IResponse response)
{
var success = TrySearchByLocation(response);
var success = TrySearchMoodleUserIdentifierByLocation(response);
if (!success)
{
TrySearchBySession(request, response);
success = TrySearchMoodleUserIdentifierByRequest(MoodleRequestType.Plugin, request, response);
}
if (!success)
{
success = TrySearchMoodleUserIdentifierByRequest(MoodleRequestType.Theme, request, response);
}
return success;
}
private bool TrySearchByLocation(IResponse response)
private bool TrySearchMoodleUserIdentifierByLocation(IResponse response)
{
var locations = response.Headers.GetValues("Location");
@ -323,11 +338,11 @@ namespace SafeExamBrowser.Browser.Handlers
{
var userId = location.Substring(location.IndexOf("=") + 1);
if (sessionIdentifier != userId)
if (userIdentifier != userId)
{
sessionIdentifier = userId;
Task.Run(() => SessionIdentifierDetected?.Invoke(sessionIdentifier));
logger.Info("Moodle session detected.");
userIdentifier = userId;
Task.Run(() => UserIdentifierDetected?.Invoke(userIdentifier));
logger.Info("Moodle user identifier detected by location.");
}
return true;
@ -335,16 +350,17 @@ namespace SafeExamBrowser.Browser.Handlers
}
catch (Exception e)
{
logger.Error("Failed to parse Moodle session identifier!", e);
logger.Error("Failed to parse Moodle user identifier by location!", e);
}
}
return false;
}
private void TrySearchBySession(IRequest request, IResponse response)
private bool TrySearchMoodleUserIdentifierByRequest(MoodleRequestType type, IRequest request, IResponse response)
{
var cookies = response.Headers.GetValues("Set-Cookie");
var success = false;
if (cookies != default(string[]))
{
@ -352,51 +368,83 @@ namespace SafeExamBrowser.Browser.Handlers
if (session != default)
{
var requestUrl = request.Url;
var userId = ExecuteMoodleUserIdentifierRequest(request.Url, session, type);
Task.Run(async () =>
if (int.TryParse(userId, out var id) && id > 0 && userIdentifier != userId)
{
try
{
var start = session.IndexOf("=") + 1;
var end = session.IndexOf(";");
var value = session.Substring(start, end - start);
var uri = new Uri(requestUrl);
var message = new HttpRequestMessage(HttpMethod.Get, $"{uri.Scheme}{Uri.SchemeDelimiter}{uri.Host}/theme/boost_ethz/sebuser.php");
using (var handler = new HttpClientHandler { UseCookies = false })
using (var client = new HttpClient(handler))
{
message.Headers.Add("Cookie", $"MoodleSession={value}");
var result = await client.SendAsync(message);
if (result.IsSuccessStatusCode)
{
var userId = await result.Content.ReadAsStringAsync();
if (int.TryParse(userId, out var id) && id > 0 && sessionIdentifier != userId)
{
#pragma warning disable CS4014
sessionIdentifier = userId;
Task.Run(() => SessionIdentifierDetected?.Invoke(sessionIdentifier));
logger.Info("Moodle session detected.");
#pragma warning restore CS4014
}
}
else
{
logger.Error($"Failed to retrieve Moodle session identifier! Response: {result.StatusCode} {result.ReasonPhrase}");
}
}
}
catch (Exception e)
{
logger.Error("Failed to parse Moodle session identifier!", e);
}
});
userIdentifier = userId;
Task.Run(() => UserIdentifierDetected?.Invoke(userIdentifier));
logger.Info($"Moodle user identifier detected by request ({type}).");
success = true;
}
}
}
return success;
}
private string ExecuteMoodleUserIdentifierRequest(string requestUrl, string session, MoodleRequestType type)
{
var userId = default(string);
try
{
Task.Run(async () =>
{
try
{
var endpointUrl = default(string);
var start = session.IndexOf("=") + 1;
var end = session.IndexOf(";");
var value = session.Substring(start, end - start);
var uri = new Uri(requestUrl);
if (type == MoodleRequestType.Plugin)
{
endpointUrl = $"{uri.Scheme}{Uri.SchemeDelimiter}{uri.Host}/mod/quiz/accessrule/sebserver/classes/external/user.php";
}
else
{
endpointUrl = $"{uri.Scheme}{Uri.SchemeDelimiter}{uri.Host}/theme/boost_ethz/sebuser.php";
}
var message = new HttpRequestMessage(HttpMethod.Get, endpointUrl);
using (var handler = new HttpClientHandler { UseCookies = false })
using (var client = new HttpClient(handler))
{
message.Headers.Add("Cookie", $"MoodleSession={value}");
var result = await client.SendAsync(message);
if (result.IsSuccessStatusCode)
{
userId = await result.Content.ReadAsStringAsync();
}
else if (result.StatusCode != HttpStatusCode.NotFound)
{
logger.Error($"Failed to retrieve Moodle user identifier by request ({type})! Response: {(int) result.StatusCode} {result.ReasonPhrase}");
}
}
}
catch (Exception e)
{
logger.Error($"Failed to parse Moodle user identifier by request ({type})!", e);
}
}).GetAwaiter().GetResult();
}
catch (Exception e)
{
logger.Error($"Failed to execute Moodle user identifier request ({type})!", e);
}
return userId;
}
private enum MoodleRequestType
{
Plugin,
Theme
}
}
}

View file

@ -324,18 +324,18 @@ namespace SafeExamBrowser.Client.UnitTests
}
[TestMethod]
public void Browser_MustHandleSessionIdentifierDetection()
public void Browser_MustHandleUserIdentifierDetection()
{
var counter = 0;
var identifier = "abc123";
settings.SessionMode = SessionMode.Server;
server.Setup(s => s.SendSessionIdentifier(It.IsAny<string>())).Returns(() => new ServerResponse(++counter == 3));
server.Setup(s => s.SendUserIdentifier(It.IsAny<string>())).Returns(() => new ServerResponse(++counter == 3));
sut.TryStart();
browser.Raise(b => b.SessionIdentifierDetected += null, identifier);
browser.Raise(b => b.UserIdentifierDetected += null, identifier);
server.Verify(s => s.SendSessionIdentifier(It.Is<string>(id => id == identifier)), Times.Exactly(3));
server.Verify(s => s.SendUserIdentifier(It.Is<string>(id => id == identifier)), Times.Exactly(3));
}
[TestMethod]

View file

@ -200,9 +200,9 @@ namespace SafeExamBrowser.Client
applicationMonitor.ExplorerStarted += ApplicationMonitor_ExplorerStarted;
applicationMonitor.TerminationFailed += ApplicationMonitor_TerminationFailed;
Browser.ConfigurationDownloadRequested += Browser_ConfigurationDownloadRequested;
Browser.SessionIdentifierDetected += Browser_SessionIdentifierDetected;
Browser.TerminationRequested += Browser_TerminationRequested;
Browser.LoseFocusRequested += Browser_LoseFocusRequested;
Browser.TerminationRequested += Browser_TerminationRequested;
Browser.UserIdentifierDetected += Browser_UserIdentifierDetected;
ClientHost.ExamSelectionRequested += ClientHost_ExamSelectionRequested;
ClientHost.MessageBoxRequested += ClientHost_MessageBoxRequested;
ClientHost.PasswordRequested += ClientHost_PasswordRequested;
@ -254,9 +254,9 @@ namespace SafeExamBrowser.Client
if (Browser != null)
{
Browser.ConfigurationDownloadRequested -= Browser_ConfigurationDownloadRequested;
Browser.SessionIdentifierDetected -= Browser_SessionIdentifierDetected;
Browser.TerminationRequested -= Browser_TerminationRequested;
Browser.LoseFocusRequested -= Browser_LoseFocusRequested;
Browser.TerminationRequested -= Browser_TerminationRequested;
Browser.UserIdentifierDetected -= Browser_UserIdentifierDetected;
}
if (ClientHost != null)
@ -531,17 +531,17 @@ namespace SafeExamBrowser.Client
}
}
private void Browser_SessionIdentifierDetected(string identifier)
private void Browser_UserIdentifierDetected(string identifier)
{
if (Settings.SessionMode == SessionMode.Server)
{
var response = Server.SendSessionIdentifier(identifier);
var response = Server.SendUserIdentifier(identifier);
while (!response.Success)
{
logger.Error($"Failed to communicate session identifier with server! {response.Message}");
logger.Error($"Failed to communicate user identifier with server! {response.Message}");
Thread.Sleep(Settings.Server.RequestAttemptInterval);
response = Server.SendSessionIdentifier(identifier);
response = Server.SendUserIdentifier(identifier);
}
}
}

View file

@ -110,9 +110,9 @@ namespace SafeExamBrowser.Server.Contracts
ServerResponse<string> SendSelectedExam(Exam exam);
/// <summary>
/// Sends the given user session identifier of a LMS and thus establishes a connection with the server.
/// Sends the given user identifier of an LMS and thus establishes a connection with the server.
/// </summary>
ServerResponse SendSessionIdentifier(string identifier);
ServerResponse SendUserIdentifier(string identifier);
/// <summary>
/// Starts sending ping and log data to the server.

View file

@ -13,9 +13,9 @@ using SafeExamBrowser.Settings.Server;
namespace SafeExamBrowser.Server.Requests
{
internal class SessionIdentifierRequest : BaseRequest
internal class UserIdentifierRequest : BaseRequest
{
internal SessionIdentifierRequest(
internal UserIdentifierRequest(
ApiVersion1 api,
HttpClient httpClient,
ILogger logger,

View file

@ -85,7 +85,7 @@
<Compile Include="Requests\PowerSupplyRequest.cs" />
<Compile Include="Requests\RaiseHandRequest.cs" />
<Compile Include="Requests\SelectExamRequest.cs" />
<Compile Include="Requests\SessionIdentifierRequest.cs" />
<Compile Include="Requests\UserIdentifierRequest.cs" />
<Compile Include="ServerProxy.cs" />
</ItemGroup>
<ItemGroup>

View file

@ -310,18 +310,18 @@ namespace SafeExamBrowser.Server
return new ServerResponse<string>(success, browserExamKey, message);
}
public ServerResponse SendSessionIdentifier(string identifier)
public ServerResponse SendUserIdentifier(string identifier)
{
var request = new SessionIdentifierRequest(api, httpClient, logger, parser, settings);
var request = new UserIdentifierRequest(api, httpClient, logger, parser, settings);
var success = request.TryExecute(examId, identifier, out var message);
if (success)
{
logger.Info("Successfully sent session identifier.");
logger.Info("Successfully sent user identifier.");
}
else
{
logger.Error("Failed to send session identifier!");
logger.Error("Failed to send user identifier!");
}
return new ServerResponse(success, message);