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 BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings;
using Request = SafeExamBrowser.Browser.Contracts.Filters.Request;
using ResourceHandler = SafeExamBrowser.Browser.Handlers.ResourceHandler;
namespace SafeExamBrowser.Browser.UnitTests.Handlers
{
@ -28,6 +29,7 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers
private Mock<IRequestFilter> filter;
private Mock<ILogger> logger;
private BrowserSettings settings;
private ResourceHandler resourceHandler;
private Mock<IText> text;
private TestableRequestHandler sut;
@ -39,8 +41,9 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers
logger = new Mock<ILogger>();
settings = new BrowserSettings();
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]
@ -194,7 +197,13 @@ namespace SafeExamBrowser.Browser.UnitTests.Handlers
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.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Timers;
using CefSharp;
using CefSharp.WinForms;
using SafeExamBrowser.Applications.Contracts;
@ -41,10 +39,8 @@ namespace SafeExamBrowser.Browser
private IFileSystemDialog fileSystemDialog;
private IMessageBox messageBox;
private IModuleLogger logger;
private List<string> sessionCookies;
private BrowserSettings settings;
private IText text;
private Timer timer;
private IUserInterfaceFactory uiFactory;
public bool AutoStart { get; private set; }
@ -72,10 +68,8 @@ namespace SafeExamBrowser.Browser
this.instances = new List<BrowserApplicationInstance>();
this.logger = logger;
this.messageBox = messageBox;
this.sessionCookies = new List<string>();
this.settings = settings;
this.text = text;
this.timer = new Timer();
this.uiFactory = uiFactory;
}
@ -111,15 +105,12 @@ namespace SafeExamBrowser.Browser
public void Start()
{
CreateNewInstance();
StartMonitoringCookies();
}
public void Terminate()
{
logger.Info("Initiating termination...");
StopMonitoringCookies();
foreach (var instance in instances)
{
instance.Terminated -= Instance_Terminated;
@ -155,6 +146,7 @@ namespace SafeExamBrowser.Browser
instance.ConfigurationDownloadRequested += (fileName, args) => ConfigurationDownloadRequested?.Invoke(fileName, args);
instance.PopupRequested += Instance_PopupRequested;
instance.SessionIdentifierDetected += (i) => SessionIdentifierDetected?.Invoke(i);
instance.Terminated += Instance_Terminated;
instance.TerminationRequested += () => TerminationRequested?.Invoke();
@ -304,20 +296,6 @@ namespace SafeExamBrowser.Browser
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)
{
switch (protocol)
@ -346,40 +324,5 @@ namespace SafeExamBrowser.Browser
instances.Remove(instances.First(i => i.Id == id));
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 BrowserSettings = SafeExamBrowser.Settings.Browser.BrowserSettings;
using Request = SafeExamBrowser.Browser.Contracts.Filters.Request;
using ResourceHandler = SafeExamBrowser.Browser.Handlers.ResourceHandler;
using TitleChangedEventHandler = SafeExamBrowser.Applications.Contracts.Events.TitleChangedEventHandler;
namespace SafeExamBrowser.Browser
@ -65,6 +66,7 @@ namespace SafeExamBrowser.Browser
internal event DownloadRequestedEventHandler ConfigurationDownloadRequested;
internal event PopupRequestedEventHandler PopupRequested;
internal event SessionIdentifierDetectedEventHandler SessionIdentifierDetected;
internal event InstanceTerminatedEventHandler Terminated;
internal event TerminationRequestedEventHandler TerminationRequested;
@ -124,7 +126,8 @@ namespace SafeExamBrowser.Browser
var lifeSpanHandler = new LifeSpanHandler();
var requestFilter = new RequestFilter();
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();
@ -138,6 +141,7 @@ namespace SafeExamBrowser.Browser
keyboardHandler.ZoomOutRequested += ZoomOutRequested;
keyboardHandler.ZoomResetRequested += ZoomResetRequested;
lifeSpanHandler.PopupRequested += LifeSpanHandler_PopupRequested;
resourceHandler.SessionIdentifierDetected += (id) => SessionIdentifierDetected?.Invoke(id);
requestHandler.QuitUrlVisited += RequestHandler_QuitUrlVisited;
requestHandler.RequestBlocked += RequestHandler_RequestBlocked;

View file

@ -31,12 +31,18 @@ namespace SafeExamBrowser.Browser.Handlers
internal event UrlEventHandler QuitUrlVisited;
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.logger = logger;
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)

View file

@ -8,10 +8,15 @@
using System;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Mime;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using CefSharp;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SafeExamBrowser.Browser.Contracts.Events;
using SafeExamBrowser.Browser.Contracts.Filters;
using SafeExamBrowser.Browser.Pages;
using SafeExamBrowser.Configuration.Contracts;
@ -36,6 +41,8 @@ namespace SafeExamBrowser.Browser.Handlers
private BrowserSettings settings;
private IText text;
internal event SessionIdentifierDetectedEventHandler SessionIdentifierDetected;
internal ResourceHandler(AppConfig appConfig, BrowserSettings settings, IRequestFilter filter, ILogger logger, IText text)
{
this.appConfig = appConfig;
@ -70,6 +77,13 @@ namespace SafeExamBrowser.Browser.Handlers
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)
{
if (RedirectToDisablePdfToolbar(request, response, out var url))
@ -79,6 +93,8 @@ namespace SafeExamBrowser.Browser.Handlers
return true;
}
SearchSessionIdentifiers(response);
return base.OnResourceResponse(webBrowser, browser, frame, request, response);
}
@ -206,5 +222,64 @@ namespace SafeExamBrowser.Browser.Handlers
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>
</PropertyGroup>
<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">
<HintPath>..\packages\Syroot.Windows.IO.KnownFolders.1.2.3\lib\netstandard2.0\Syroot.KnownFolders.dll</HintPath>
</Reference>

View file

@ -4,6 +4,7 @@
<package id="cef.redist.x86" version="81.3.10" targetFramework="net472" />
<package id="CefSharp.Common" 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="System.Security.Principal.Windows" version="4.7.0" targetFramework="net472" />
</packages>

View file

@ -77,7 +77,6 @@ namespace SafeExamBrowser.Runtime.Operations
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;
Context.Next.AppConfig.ServerApi = info.Api;

View file

@ -256,20 +256,28 @@ namespace SafeExamBrowser.Server
task = new Task(SendLog, cancellationTokenSource.Token);
task.Start();
logger.Info("Started sending log items.");
timer.AutoReset = false;
timer.Elapsed += Timer_Elapsed;
timer.Interval = 1000;
timer.Start();
logger.Info("Starting sending pings.");
}
public void StopConnectivity()
{
logger.Unsubscribe(this);
cancellationTokenSource.Cancel();
task.Wait();
task?.Wait();
logger.Info("Stopped sending log items.");
timer.Stop();
timer.Elapsed -= Timer_Elapsed;
logger.Info("Stopped sending pings.");
}
private void SendLog()
@ -278,8 +286,6 @@ namespace SafeExamBrowser.Server
var contentType = "application/json;charset=UTF-8";
var token = ("SEBConnectionToken", connectionToken);
logger.Info("Starting to send log items...");
while (!cancellationTokenSource.IsCancellationRequested)
{
try
@ -293,7 +299,6 @@ namespace SafeExamBrowser.Server
["text"] = message.Message
};
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);
}
}
@ -302,8 +307,6 @@ namespace SafeExamBrowser.Server
logger.Error("Failed to send log!", e);
}
}
logger.Info("Stopped sending log items.");
}
private void Timer_Elapsed(object sender, ElapsedEventArgs args)