SEBWIN-405: Fixed and improved LMS session detection.

This commit is contained in:
Damian Büchel 2020-08-03 14:41:25 +02:00
parent 682c2a2ce5
commit 8d94750078
9 changed files with 113 additions and 70 deletions

View file

@ -18,6 +18,7 @@ using SafeExamBrowser.Settings.Browser.Filter;
using SafeExamBrowser.Settings.Browser.Proxy; using SafeExamBrowser.Settings.Browser.Proxy;
using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings; using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings;
using Request = SafeExamBrowser.Browser.Contracts.Filters.Request; using Request = SafeExamBrowser.Browser.Contracts.Filters.Request;
using ResourceHandler = SafeExamBrowser.Browser.Handlers.ResourceHandler;
namespace SafeExamBrowser.Browser.UnitTests.Handlers namespace SafeExamBrowser.Browser.UnitTests.Handlers
{ {
@ -28,6 +29,7 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers
private Mock<IRequestFilter> filter; private Mock<IRequestFilter> filter;
private Mock<ILogger> logger; private Mock<ILogger> logger;
private BrowserSettings settings; private BrowserSettings settings;
private ResourceHandler resourceHandler;
private Mock<IText> text; private Mock<IText> text;
private TestableRequestHandler sut; private TestableRequestHandler sut;
@ -39,8 +41,9 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers
logger = new Mock<ILogger>(); logger = new Mock<ILogger>();
settings = new BrowserSettings(); settings = new BrowserSettings();
text = new Mock<IText>(); text = new Mock<IText>();
resourceHandler = new ResourceHandler(appConfig, settings, filter.Object, logger.Object, text.Object);
sut = new TestableRequestHandler(appConfig, filter.Object, logger.Object, settings, text.Object); sut = new TestableRequestHandler(appConfig, filter.Object, logger.Object, settings, resourceHandler, text.Object);
} }
[TestMethod] [TestMethod]
@ -194,7 +197,13 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers
private class TestableRequestHandler : RequestHandler private class TestableRequestHandler : RequestHandler
{ {
internal TestableRequestHandler(AppConfig appConfig, IRequestFilter filter, ILogger logger, BrowserSettings settings, IText text) : base(appConfig, filter, logger, settings, text) internal TestableRequestHandler(
AppConfig appConfig,
IRequestFilter filter,
ILogger logger,
BrowserSettings settings,
ResourceHandler resourceHandler,
IText text) : base(appConfig, filter, logger, settings, resourceHandler, text)
{ {
} }

View file

@ -10,8 +10,6 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using CefSharp; using CefSharp;
using CefSharp.WinForms; using CefSharp.WinForms;
using SafeExamBrowser.Applications.Contracts; using SafeExamBrowser.Applications.Contracts;
@ -41,10 +39,8 @@ namespace SafeExamBrowser.Browser
private IFileSystemDialog fileSystemDialog; private IFileSystemDialog fileSystemDialog;
private IMessageBox messageBox; private IMessageBox messageBox;
private IModuleLogger logger; private IModuleLogger logger;
private List<string> sessionCookies;
private BrowserSettings settings; private BrowserSettings settings;
private IText text; private IText text;
private Timer timer;
private IUserInterfaceFactory uiFactory; private IUserInterfaceFactory uiFactory;
public bool AutoStart { get; private set; } public bool AutoStart { get; private set; }
@ -72,10 +68,8 @@ namespace SafeExamBrowser.Browser
this.instances = new List<BrowserApplicationInstance>(); this.instances = new List<BrowserApplicationInstance>();
this.logger = logger; this.logger = logger;
this.messageBox = messageBox; this.messageBox = messageBox;
this.sessionCookies = new List<string>();
this.settings = settings; this.settings = settings;
this.text = text; this.text = text;
this.timer = new Timer();
this.uiFactory = uiFactory; this.uiFactory = uiFactory;
} }
@ -111,15 +105,12 @@ namespace SafeExamBrowser.Browser
public void Start() public void Start()
{ {
CreateNewInstance(); CreateNewInstance();
StartMonitoringCookies();
} }
public void Terminate() public void Terminate()
{ {
logger.Info("Initiating termination..."); logger.Info("Initiating termination...");
StopMonitoringCookies();
foreach (var instance in instances) foreach (var instance in instances)
{ {
instance.Terminated -= Instance_Terminated; instance.Terminated -= Instance_Terminated;
@ -155,6 +146,7 @@ namespace SafeExamBrowser.Browser
instance.ConfigurationDownloadRequested += (fileName, args) => ConfigurationDownloadRequested?.Invoke(fileName, args); instance.ConfigurationDownloadRequested += (fileName, args) => ConfigurationDownloadRequested?.Invoke(fileName, args);
instance.PopupRequested += Instance_PopupRequested; instance.PopupRequested += Instance_PopupRequested;
instance.SessionIdentifierDetected += (i) => SessionIdentifierDetected?.Invoke(i);
instance.Terminated += Instance_Terminated; instance.Terminated += Instance_Terminated;
instance.TerminationRequested += () => TerminationRequested?.Invoke(); instance.TerminationRequested += () => TerminationRequested?.Invoke();
@ -304,20 +296,6 @@ namespace SafeExamBrowser.Browser
return userAgent; return userAgent;
} }
private void StartMonitoringCookies()
{
timer.AutoReset = false;
timer.Interval = 1000;
timer.Elapsed += Timer_Elapsed;
timer.Start();
}
private void StopMonitoringCookies()
{
timer.Stop();
timer.Elapsed -= Timer_Elapsed;
}
private string ToScheme(ProxyProtocol protocol) private string ToScheme(ProxyProtocol protocol)
{ {
switch (protocol) switch (protocol)
@ -346,40 +324,5 @@ namespace SafeExamBrowser.Browser
instances.Remove(instances.First(i => i.Id == id)); instances.Remove(instances.First(i => i.Id == id));
WindowsChanged?.Invoke(); WindowsChanged?.Invoke();
} }
private void Timer_Elapsed(object sender, ElapsedEventArgs args)
{
try
{
var manager = Cef.GetGlobalCookieManager();
var task = manager.VisitAllCookiesAsync();
var cookies = task.GetAwaiter().GetResult();
var edxLogin = cookies.FirstOrDefault(c => c.Name == "edxloggedin");
var moodleSession = cookies.FirstOrDefault(c => c.Name == "MoodleSession");
if (edxLogin != default(Cookie))
{
var edxSession = cookies.FirstOrDefault(c => c.Domain == edxLogin.Domain && c.Name == "sessionid");
if (edxSession != default(Cookie) && !sessionCookies.Contains(edxSession.Domain))
{
sessionCookies.Add(edxSession.Domain);
Task.Run(() => SessionIdentifierDetected?.Invoke(edxSession.Value));
}
}
if (moodleSession != default(Cookie) && !sessionCookies.Contains(moodleSession.Domain))
{
sessionCookies.Add(moodleSession.Domain);
Task.Run(() => SessionIdentifierDetected?.Invoke(moodleSession.Value));
}
}
catch (Exception e)
{
logger.Error("Failed to read cookies!", e);
}
timer.Start();
}
} }
} }

View file

@ -30,6 +30,7 @@ using SafeExamBrowser.UserInterface.Contracts.FileSystemDialog;
using SafeExamBrowser.UserInterface.Contracts.MessageBox; using SafeExamBrowser.UserInterface.Contracts.MessageBox;
using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings; using BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings;
using Request = SafeExamBrowser.Browser.Contracts.Filters.Request; using Request = SafeExamBrowser.Browser.Contracts.Filters.Request;
using ResourceHandler = SafeExamBrowser.Browser.Handlers.ResourceHandler;
using TitleChangedEventHandler = SafeExamBrowser.Applications.Contracts.Events.TitleChangedEventHandler; using TitleChangedEventHandler = SafeExamBrowser.Applications.Contracts.Events.TitleChangedEventHandler;
namespace SafeExamBrowser.Browser namespace SafeExamBrowser.Browser
@ -65,6 +66,7 @@ namespace SafeExamBrowser.Browser
internal event DownloadRequestedEventHandler ConfigurationDownloadRequested; internal event DownloadRequestedEventHandler ConfigurationDownloadRequested;
internal event PopupRequestedEventHandler PopupRequested; internal event PopupRequestedEventHandler PopupRequested;
internal event SessionIdentifierDetectedEventHandler SessionIdentifierDetected;
internal event InstanceTerminatedEventHandler Terminated; internal event InstanceTerminatedEventHandler Terminated;
internal event TerminationRequestedEventHandler TerminationRequested; internal event TerminationRequestedEventHandler TerminationRequested;
@ -124,7 +126,8 @@ namespace SafeExamBrowser.Browser
var lifeSpanHandler = new LifeSpanHandler(); var lifeSpanHandler = new LifeSpanHandler();
var requestFilter = new RequestFilter(); var requestFilter = new RequestFilter();
var requestLogger = logger.CloneFor($"{nameof(RequestHandler)} #{Id}"); var requestLogger = logger.CloneFor($"{nameof(RequestHandler)} #{Id}");
var requestHandler = new RequestHandler(appConfig, requestFilter, requestLogger, settings, text); var resourceHandler = new ResourceHandler(appConfig, settings, requestFilter, logger, text);
var requestHandler = new RequestHandler(appConfig, requestFilter, requestLogger, settings, resourceHandler, text);
Icon = new BrowserIconResource(); Icon = new BrowserIconResource();
@ -138,6 +141,7 @@ namespace SafeExamBrowser.Browser
keyboardHandler.ZoomOutRequested += ZoomOutRequested; keyboardHandler.ZoomOutRequested += ZoomOutRequested;
keyboardHandler.ZoomResetRequested += ZoomResetRequested; keyboardHandler.ZoomResetRequested += ZoomResetRequested;
lifeSpanHandler.PopupRequested += LifeSpanHandler_PopupRequested; lifeSpanHandler.PopupRequested += LifeSpanHandler_PopupRequested;
resourceHandler.SessionIdentifierDetected += (id) => SessionIdentifierDetected?.Invoke(id);
requestHandler.QuitUrlVisited += RequestHandler_QuitUrlVisited; requestHandler.QuitUrlVisited += RequestHandler_QuitUrlVisited;
requestHandler.RequestBlocked += RequestHandler_RequestBlocked; requestHandler.RequestBlocked += RequestHandler_RequestBlocked;

View file

@ -31,12 +31,18 @@ namespace SafeExamBrowser.Browser.Handlers
internal event UrlEventHandler QuitUrlVisited; internal event UrlEventHandler QuitUrlVisited;
internal event UrlEventHandler RequestBlocked; internal event UrlEventHandler RequestBlocked;
internal RequestHandler(AppConfig appConfig, IRequestFilter filter, ILogger logger, BrowserSettings settings, IText text) internal RequestHandler(
AppConfig appConfig,
IRequestFilter filter,
ILogger logger,
BrowserSettings settings,
ResourceHandler resourceHandler,
IText text)
{ {
this.filter = filter; this.filter = filter;
this.logger = logger; this.logger = logger;
this.settings = settings; this.settings = settings;
this.resourceHandler = new ResourceHandler(appConfig, settings, filter, logger, text); this.resourceHandler = resourceHandler;
} }
protected override bool GetAuthCredentials(IWebBrowser webBrowser, IBrowser browser, string originUrl, bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback) protected override bool GetAuthCredentials(IWebBrowser webBrowser, IBrowser browser, string originUrl, bool isProxy, string host, int port, string realm, string scheme, IAuthCallback callback)

View file

@ -8,10 +8,15 @@
using System; using System;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
using System.Net.Mime; using System.Net.Mime;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading.Tasks;
using CefSharp; using CefSharp;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SafeExamBrowser.Browser.Contracts.Events;
using SafeExamBrowser.Browser.Contracts.Filters; using SafeExamBrowser.Browser.Contracts.Filters;
using SafeExamBrowser.Browser.Pages; using SafeExamBrowser.Browser.Pages;
using SafeExamBrowser.Configuration.Contracts; using SafeExamBrowser.Configuration.Contracts;
@ -36,6 +41,8 @@ namespace SafeExamBrowser.Browser.Handlers
private BrowserSettings settings; private BrowserSettings settings;
private IText text; private IText text;
internal event SessionIdentifierDetectedEventHandler SessionIdentifierDetected;
internal ResourceHandler(AppConfig appConfig, BrowserSettings settings, IRequestFilter filter, ILogger logger, IText text) internal ResourceHandler(AppConfig appConfig, BrowserSettings settings, IRequestFilter filter, ILogger logger, IText text)
{ {
this.appConfig = appConfig; this.appConfig = appConfig;
@ -70,6 +77,13 @@ namespace SafeExamBrowser.Browser.Handlers
return base.OnBeforeResourceLoad(webBrowser, browser, frame, request, callback); return base.OnBeforeResourceLoad(webBrowser, browser, frame, request, callback);
} }
protected override void OnResourceRedirect(IWebBrowser chromiumWebBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response, ref string newUrl)
{
SearchSessionIdentifiers(response);
base.OnResourceRedirect(chromiumWebBrowser, browser, frame, request, response, ref newUrl);
}
protected override bool OnResourceResponse(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response) protected override bool OnResourceResponse(IWebBrowser webBrowser, IBrowser browser, IFrame frame, IRequest request, IResponse response)
{ {
if (RedirectToDisablePdfToolbar(request, response, out var url)) if (RedirectToDisablePdfToolbar(request, response, out var url))
@ -79,6 +93,8 @@ namespace SafeExamBrowser.Browser.Handlers
return true; return true;
} }
SearchSessionIdentifiers(response);
return base.OnResourceResponse(webBrowser, browser, frame, request, response); return base.OnResourceResponse(webBrowser, browser, frame, request, response);
} }
@ -206,5 +222,64 @@ namespace SafeExamBrowser.Browser.Handlers
return contentHandler; return contentHandler;
} }
} }
private void SearchSessionIdentifiers(IResponse response)
{
SearchEdxIdentifier(response);
SearchMoodleIdentifier(response);
}
private void SearchEdxIdentifier(IResponse response)
{
var cookies = response.Headers.GetValues("Set-Cookie");
if (cookies != default(string[]))
{
try
{
var userInfo = cookies.FirstOrDefault(c => c.Contains("edx-user-info"));
if (userInfo != default(string))
{
var start = userInfo.IndexOf("=") + 1;
var end = userInfo.IndexOf("; expires");
var cookie = userInfo.Substring(start, end - start);
var sanitized = cookie.Replace("\\\"", "\"").Replace("\\054", ",").Trim('"');
var json = JsonConvert.DeserializeObject(sanitized) as JObject;
var userName = json["username"].Value<string>();
Task.Run(() => SessionIdentifierDetected?.Invoke(userName));
}
}
catch (Exception e)
{
logger.Error("Failed to parse edX session identifier!", e);
}
}
}
private void SearchMoodleIdentifier(IResponse response)
{
var locations = response.Headers.GetValues("Location");
if (locations != default(string[]))
{
try
{
var location = locations.FirstOrDefault(l => l.Contains("moodle/login/index.php?testsession"));
if (location != default(string))
{
var userId = location.Substring(location.IndexOf("=") + 1);
Task.Run(() => SessionIdentifierDetected?.Invoke(userId));
}
}
catch (Exception e)
{
logger.Error("Failed to parse Moodle session identifier!", e);
}
}
}
} }
} }

View file

@ -54,6 +54,9 @@
<ErrorReport>prompt</ErrorReport> <ErrorReport>prompt</ErrorReport>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed, processorArchitecture=MSIL">
<HintPath>..\packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll</HintPath>
</Reference>
<Reference Include="Syroot.KnownFolders, Version=1.2.3.0, Culture=neutral, processorArchitecture=MSIL"> <Reference Include="Syroot.KnownFolders, Version=1.2.3.0, Culture=neutral, processorArchitecture=MSIL">
<HintPath>..\packages\Syroot.Windows.IO.KnownFolders.1.2.3\lib\netstandard2.0\Syroot.KnownFolders.dll</HintPath> <HintPath>..\packages\Syroot.Windows.IO.KnownFolders.1.2.3\lib\netstandard2.0\Syroot.KnownFolders.dll</HintPath>
</Reference> </Reference>

View file

@ -4,6 +4,7 @@
<package id="cef.redist.x86" version="81.3.10" targetFramework="net472" /> <package id="cef.redist.x86" version="81.3.10" targetFramework="net472" />
<package id="CefSharp.Common" version="81.3.100" targetFramework="net472" /> <package id="CefSharp.Common" version="81.3.100" targetFramework="net472" />
<package id="CefSharp.WinForms" version="81.3.100" targetFramework="net472" /> <package id="CefSharp.WinForms" version="81.3.100" targetFramework="net472" />
<package id="Newtonsoft.Json" version="12.0.3" targetFramework="net472" />
<package id="Syroot.Windows.IO.KnownFolders" version="1.2.3" targetFramework="net472" /> <package id="Syroot.Windows.IO.KnownFolders" version="1.2.3" targetFramework="net472" />
<package id="System.Security.Principal.Windows" version="4.7.0" targetFramework="net472" /> <package id="System.Security.Principal.Windows" version="4.7.0" targetFramework="net472" />
</packages> </packages>

View file

@ -77,7 +77,6 @@ namespace SafeExamBrowser.Runtime.Operations
if (status == LoadStatus.Success) if (status == LoadStatus.Success)
{ {
// TODO: Why aren't the server settings and SEB mode correctly set in the exam configuration?
var serverSettings = Context.Next.Settings.Server; var serverSettings = Context.Next.Settings.Server;
Context.Next.AppConfig.ServerApi = info.Api; Context.Next.AppConfig.ServerApi = info.Api;

View file

@ -256,20 +256,28 @@ namespace SafeExamBrowser.Server
task = new Task(SendLog, cancellationTokenSource.Token); task = new Task(SendLog, cancellationTokenSource.Token);
task.Start(); task.Start();
logger.Info("Started sending log items.");
timer.AutoReset = false; timer.AutoReset = false;
timer.Elapsed += Timer_Elapsed; timer.Elapsed += Timer_Elapsed;
timer.Interval = 1000; timer.Interval = 1000;
timer.Start(); timer.Start();
logger.Info("Starting sending pings.");
} }
public void StopConnectivity() public void StopConnectivity()
{ {
logger.Unsubscribe(this); logger.Unsubscribe(this);
cancellationTokenSource.Cancel(); cancellationTokenSource.Cancel();
task.Wait(); task?.Wait();
logger.Info("Stopped sending log items.");
timer.Stop(); timer.Stop();
timer.Elapsed -= Timer_Elapsed; timer.Elapsed -= Timer_Elapsed;
logger.Info("Stopped sending pings.");
} }
private void SendLog() private void SendLog()
@ -278,8 +286,6 @@ namespace SafeExamBrowser.Server
var contentType = "application/json;charset=UTF-8"; var contentType = "application/json;charset=UTF-8";
var token = ("SEBConnectionToken", connectionToken); var token = ("SEBConnectionToken", connectionToken);
logger.Info("Starting to send log items...");
while (!cancellationTokenSource.IsCancellationRequested) while (!cancellationTokenSource.IsCancellationRequested)
{ {
try try
@ -293,7 +299,6 @@ namespace SafeExamBrowser.Server
["text"] = message.Message ["text"] = message.Message
}; };
var content = json.ToString(); var content = json.ToString();
// TODO: Why can't we send multiple log messages in one request?
var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token); var success = TryExecute(HttpMethod.Post, api.LogEndpoint, out var response, content, contentType, authorization, token);
} }
} }
@ -302,8 +307,6 @@ namespace SafeExamBrowser.Server
logger.Error("Failed to send log!", e); logger.Error("Failed to send log!", e);
} }
} }
logger.Info("Stopped sending log items.");
} }
private void Timer_Elapsed(object sender, ElapsedEventArgs args) private void Timer_Elapsed(object sender, ElapsedEventArgs args)