2019-09-10 11:01:49 +02:00
/ *
2023-03-08 00:30:20 +01:00
* Copyright ( c ) 2023 ETH Zürich , Educational Development and Technology ( LET )
2019-09-10 11:01:49 +02:00
*
* 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.Specialized ;
2020-08-03 14:41:25 +02:00
using System.Linq ;
2021-08-31 18:15:26 +02:00
using System.Net ;
2021-02-23 15:32:08 +01:00
using System.Net.Http ;
2020-01-30 11:15:28 +01:00
using System.Net.Mime ;
2020-08-03 14:41:25 +02:00
using System.Threading.Tasks ;
2019-09-10 11:01:49 +02:00
using CefSharp ;
2020-08-03 14:41:25 +02:00
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
2021-10-18 12:06:10 +02:00
using SafeExamBrowser.Browser.Content ;
2020-08-03 14:41:25 +02:00
using SafeExamBrowser.Browser.Contracts.Events ;
2019-09-13 09:17:14 +02:00
using SafeExamBrowser.Browser.Contracts.Filters ;
2019-09-10 11:01:49 +02:00
using SafeExamBrowser.Configuration.Contracts ;
2021-10-18 12:06:10 +02:00
using SafeExamBrowser.Configuration.Contracts.Cryptography ;
2019-09-10 11:01:49 +02:00
using SafeExamBrowser.I18n.Contracts ;
using SafeExamBrowser.Logging.Contracts ;
2023-03-08 00:01:20 +01:00
using SafeExamBrowser.Settings ;
2020-09-29 14:01:17 +02:00
using SafeExamBrowser.Settings.Browser ;
2019-12-18 08:24:55 +01:00
using SafeExamBrowser.Settings.Browser.Filter ;
2020-01-10 08:54:10 +01:00
using BrowserSettings = SafeExamBrowser . Settings . Browser . BrowserSettings ;
2020-02-14 09:51:52 +01:00
using Request = SafeExamBrowser . Browser . Contracts . Filters . Request ;
2019-09-10 11:01:49 +02:00
namespace SafeExamBrowser.Browser.Handlers
{
internal class ResourceHandler : CefSharp . Handler . ResourceRequestHandler
{
2021-10-18 12:06:10 +02:00
private readonly AppConfig appConfig ;
private readonly ContentLoader contentLoader ;
private readonly IRequestFilter filter ;
private readonly IKeyGenerator keyGenerator ;
private readonly ILogger logger ;
2023-03-08 00:01:20 +01:00
private readonly SessionMode sessionMode ;
2021-10-18 12:06:10 +02:00
private readonly BrowserSettings settings ;
private readonly WindowSettings windowSettings ;
2019-09-12 12:31:17 +02:00
private IResourceHandler contentHandler ;
private IResourceHandler pageHandler ;
2021-10-04 18:10:36 +02:00
private string sessionIdentifier ;
2019-09-10 11:01:49 +02:00
2020-08-03 14:41:25 +02:00
internal event SessionIdentifierDetectedEventHandler SessionIdentifierDetected ;
2020-09-29 14:01:17 +02:00
internal ResourceHandler (
AppConfig appConfig ,
IRequestFilter filter ,
2021-10-18 12:06:10 +02:00
IKeyGenerator keyGenerator ,
2020-09-29 14:01:17 +02:00
ILogger logger ,
2023-03-08 00:01:20 +01:00
SessionMode sessionMode ,
2020-09-29 14:01:17 +02:00
BrowserSettings settings ,
WindowSettings windowSettings ,
IText text )
2019-09-10 11:01:49 +02:00
{
this . appConfig = appConfig ;
2019-09-12 12:31:17 +02:00
this . filter = filter ;
2021-10-18 12:06:10 +02:00
this . contentLoader = new ContentLoader ( text ) ;
this . keyGenerator = keyGenerator ;
2019-09-10 11:01:49 +02:00
this . logger = logger ;
2023-03-08 00:01:20 +01:00
this . sessionMode = sessionMode ;
2019-09-10 11:01:49 +02:00
this . settings = settings ;
2020-09-29 14:01:17 +02:00
this . windowSettings = windowSettings ;
2019-09-10 11:01:49 +02:00
}
2020-04-06 14:13:13 +02:00
protected override IResourceHandler GetResourceHandler ( IWebBrowser webBrowser , IBrowser browser , IFrame frame , IRequest request )
2019-09-10 11:01:49 +02:00
{
2019-09-12 12:31:17 +02:00
if ( Block ( request ) )
2019-09-10 11:01:49 +02:00
{
2019-09-12 12:31:17 +02:00
return ResourceHandlerFor ( request . ResourceType ) ;
2019-09-10 11:01:49 +02:00
}
2020-04-06 14:13:13 +02:00
return base . GetResourceHandler ( webBrowser , browser , frame , request ) ;
2019-09-10 11:01:49 +02:00
}
protected override CefReturnValue OnBeforeResourceLoad ( IWebBrowser webBrowser , IBrowser browser , IFrame frame , IRequest request , IRequestCallback callback )
{
2020-01-10 08:54:10 +01:00
if ( IsMailtoUrl ( request . Url ) )
{
return CefReturnValue . Cancel ;
}
2020-11-27 15:14:33 +01:00
AppendCustomHeaders ( webBrowser , request ) ;
2019-09-12 12:31:17 +02:00
ReplaceSebScheme ( request ) ;
2019-09-10 11:01:49 +02:00
return base . OnBeforeResourceLoad ( webBrowser , browser , frame , request , callback ) ;
}
2020-08-06 14:26:40 +02:00
protected override bool OnProtocolExecution ( IWebBrowser webBrowser , IBrowser browser , IFrame frame , IRequest request )
{
return true ;
}
2020-08-03 14:41:25 +02:00
protected override void OnResourceRedirect ( IWebBrowser chromiumWebBrowser , IBrowser browser , IFrame frame , IRequest request , IResponse response , ref string newUrl )
{
2023-03-08 00:01:20 +01:00
if ( sessionMode = = SessionMode . Server )
{
SearchSessionIdentifiers ( request , response ) ;
}
2020-08-03 14:41:25 +02:00
base . OnResourceRedirect ( chromiumWebBrowser , browser , frame , request , response , ref newUrl ) ;
}
2020-04-06 14:13:13 +02:00
protected override bool OnResourceResponse ( IWebBrowser webBrowser , IBrowser browser , IFrame frame , IRequest request , IResponse response )
2020-01-30 11:15:28 +01:00
{
2022-07-26 17:56:40 +02:00
if ( RedirectToDisablePdfReaderToolbar ( request , response , out var url ) )
2020-01-30 11:15:28 +01:00
{
2022-07-26 17:56:40 +02:00
frame ? . LoadUrl ( url ) ;
2020-03-04 10:08:34 +01:00
return true ;
2020-01-30 11:15:28 +01:00
}
2023-03-08 00:01:20 +01:00
if ( sessionMode = = SessionMode . Server )
{
SearchSessionIdentifiers ( request , response ) ;
}
2020-08-03 14:41:25 +02:00
2020-04-06 14:13:13 +02:00
return base . OnResourceResponse ( webBrowser , browser , frame , request , response ) ;
2020-01-30 11:15:28 +01:00
}
2020-11-27 15:14:33 +01:00
private void AppendCustomHeaders ( IWebBrowser webBrowser , IRequest request )
2019-09-10 11:01:49 +02:00
{
2020-11-27 15:14:33 +01:00
Uri . TryCreate ( webBrowser . Address , UriKind . Absolute , out var pageUrl ) ;
Uri . TryCreate ( request . Url , UriKind . Absolute , out var requestUrl ) ;
if ( pageUrl ? . Host ? . Equals ( requestUrl ? . Host ) = = true )
2020-02-13 11:01:07 +01:00
{
2020-11-30 18:30:29 +01:00
var headers = new NameValueCollection ( request . Headers ) ;
2020-11-27 15:14:33 +01:00
if ( settings . SendConfigurationKey )
{
2023-03-02 23:48:11 +01:00
headers [ "X-SafeExamBrowser-ConfigKeyHash" ] = keyGenerator . CalculateConfigurationKeyHash ( settings . ConfigurationKey , request . Url ) ;
2020-11-27 15:14:33 +01:00
}
2020-01-10 08:54:10 +01:00
2021-10-18 12:06:10 +02:00
if ( settings . SendBrowserExamKey )
2020-11-27 15:14:33 +01:00
{
2023-03-02 23:48:11 +01:00
headers [ "X-SafeExamBrowser-RequestHash" ] = keyGenerator . CalculateBrowserExamKeyHash ( settings . ConfigurationKey , settings . BrowserExamKeySalt , request . Url ) ;
2020-11-27 15:14:33 +01:00
}
2020-02-10 12:19:25 +01:00
2020-11-27 15:14:33 +01:00
request . Headers = headers ;
}
2020-01-10 08:54:10 +01:00
}
2019-09-12 12:31:17 +02:00
private bool Block ( IRequest request )
2019-09-10 11:01:49 +02:00
{
2020-01-30 11:15:28 +01:00
var block = false ;
2021-08-31 18:15:26 +02:00
var url = WebUtility . UrlDecode ( request . Url ) ;
var isValidUri = Uri . TryCreate ( url , UriKind . Absolute , out _ ) ;
2020-01-30 11:15:28 +01:00
2021-08-31 18:15:26 +02:00
if ( settings . Filter . ProcessContentRequests & & isValidUri )
2019-09-10 11:01:49 +02:00
{
2021-08-31 18:15:26 +02:00
var result = filter . Process ( new Request { Url = url } ) ;
2019-09-10 11:01:49 +02:00
2020-01-30 11:15:28 +01:00
if ( result = = FilterResult . Block )
2019-09-10 11:01:49 +02:00
{
2020-01-30 11:15:28 +01:00
block = true ;
2021-08-31 18:15:26 +02:00
logger . Info ( $"Blocked content request{(windowSettings.UrlPolicy.CanLog() ? $" for ' { url } ' " : " ")} ({request.ResourceType}, {request.TransitionType})." ) ;
2019-09-10 11:01:49 +02:00
}
}
2021-08-31 18:15:26 +02:00
else if ( ! isValidUri )
{
logger . Warn ( $"Filter could not process request{(windowSettings.UrlPolicy.CanLog() ? $" for ' { url } ' " : " ")} ({request.ResourceType}, {request.TransitionType})!" ) ;
}
2019-09-10 11:01:49 +02:00
2020-01-30 11:15:28 +01:00
return block ;
2019-09-10 11:01:49 +02:00
}
private bool IsMailtoUrl ( string url )
{
return url . StartsWith ( Uri . UriSchemeMailto ) ;
}
2022-07-26 17:56:40 +02:00
private bool RedirectToDisablePdfReaderToolbar ( IRequest request , IResponse response , out string url )
2020-01-30 11:15:28 +01:00
{
2022-07-26 17:56:40 +02:00
const string DISABLE_PDF_READER_TOOLBAR = "#toolbar=0" ;
2020-01-30 11:15:28 +01:00
var isPdf = response . Headers [ "Content-Type" ] = = MediaTypeNames . Application . Pdf ;
2020-03-27 13:18:24 +01:00
var isMainFrame = request . ResourceType = = ResourceType . MainFrame ;
2022-07-26 17:56:40 +02:00
var hasFragment = request . Url . Contains ( DISABLE_PDF_READER_TOOLBAR ) ;
2020-03-27 13:18:24 +01:00
var redirect = settings . AllowPdfReader & & ! settings . AllowPdfReaderToolbar & & isPdf & & isMainFrame & & ! hasFragment ;
2020-01-30 11:15:28 +01:00
2022-07-26 17:56:40 +02:00
url = request . Url + DISABLE_PDF_READER_TOOLBAR ;
2020-01-30 11:15:28 +01:00
if ( redirect )
{
2020-09-29 14:01:17 +02:00
logger . Info ( $"Redirecting{(windowSettings.UrlPolicy.CanLog() ? $" to ' { url } ' " : " ")} to disable PDF reader toolbar." ) ;
2020-01-30 11:15:28 +01:00
}
return redirect ;
}
2019-09-12 12:31:17 +02:00
private void ReplaceSebScheme ( IRequest request )
2019-09-10 11:01:49 +02:00
{
if ( Uri . IsWellFormedUriString ( request . Url , UriKind . RelativeOrAbsolute ) )
{
var uri = new Uri ( request . Url ) ;
if ( uri . Scheme = = appConfig . SebUriScheme )
{
request . Url = new UriBuilder ( uri ) { Scheme = Uri . UriSchemeHttp } . Uri . AbsoluteUri ;
}
else if ( uri . Scheme = = appConfig . SebUriSchemeSecure )
{
request . Url = new UriBuilder ( uri ) { Scheme = Uri . UriSchemeHttps } . Uri . AbsoluteUri ;
}
}
}
2019-09-12 12:31:17 +02:00
private IResourceHandler ResourceHandlerFor ( ResourceType resourceType )
{
2020-03-13 15:56:32 +01:00
if ( contentHandler = = default ( IResourceHandler ) )
2019-09-13 09:17:14 +02:00
{
2021-10-18 12:06:10 +02:00
contentHandler = CefSharp . ResourceHandler . FromString ( contentLoader . LoadBlockedContent ( ) ) ;
2020-03-13 15:56:32 +01:00
}
if ( pageHandler = = default ( IResourceHandler ) )
{
2021-10-18 12:06:10 +02:00
pageHandler = CefSharp . ResourceHandler . FromString ( contentLoader . LoadBlockedPage ( ) ) ;
2019-09-13 09:17:14 +02:00
}
2019-09-12 12:31:17 +02:00
switch ( resourceType )
{
case ResourceType . MainFrame :
case ResourceType . SubFrame :
return pageHandler ;
default :
return contentHandler ;
}
}
2020-08-03 14:41:25 +02:00
2021-02-23 15:32:08 +01:00
private void SearchSessionIdentifiers ( IRequest request , IResponse response )
2020-08-03 14:41:25 +02:00
{
2021-04-21 19:53:52 +02:00
var success = TrySearchGenericSessionIdentifier ( response ) ;
if ( ! success )
{
SearchEdxIdentifier ( response ) ;
SearchMoodleIdentifier ( request , response ) ;
}
}
private bool TrySearchGenericSessionIdentifier ( IResponse response )
{
var ids = response . Headers . GetValues ( "X-LMS-USER-ID" ) ;
if ( ids ! = default ( string [ ] ) )
{
var userId = ids . FirstOrDefault ( ) ;
2022-07-26 17:56:40 +02:00
if ( userId ! = default & & sessionIdentifier ! = userId )
2021-04-21 19:53:52 +02:00
{
2021-10-04 18:10:36 +02:00
sessionIdentifier = userId ;
Task . Run ( ( ) = > SessionIdentifierDetected ? . Invoke ( sessionIdentifier ) ) ;
2021-04-21 19:53:52 +02:00
logger . Info ( "Generic LMS session detected." ) ;
return true ;
}
}
return false ;
2020-08-03 14:41:25 +02:00
}
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" ) ) ;
2022-07-26 17:56:40 +02:00
if ( userInfo ! = default )
2020-08-03 14:41:25 +02:00
{
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 > ( ) ;
2021-10-04 18:10:36 +02:00
if ( sessionIdentifier ! = userName )
{
sessionIdentifier = userName ;
Task . Run ( ( ) = > SessionIdentifierDetected ? . Invoke ( sessionIdentifier ) ) ;
logger . Info ( "EdX session detected." ) ;
}
2020-08-03 14:41:25 +02:00
}
}
catch ( Exception e )
{
logger . Error ( "Failed to parse edX session identifier!" , e ) ;
}
}
}
2021-02-23 15:32:08 +01:00
private void SearchMoodleIdentifier ( IRequest request , IResponse response )
{
var success = TrySearchByLocation ( response ) ;
if ( ! success )
{
TrySearchBySession ( request , response ) ;
}
}
private bool TrySearchByLocation ( IResponse response )
2020-08-03 14:41:25 +02:00
{
var locations = response . Headers . GetValues ( "Location" ) ;
if ( locations ! = default ( string [ ] ) )
{
try
{
2020-12-02 17:43:02 +01:00
var location = locations . FirstOrDefault ( l = > l . Contains ( "/login/index.php?testsession" ) ) ;
2020-08-03 14:41:25 +02:00
2022-07-26 17:56:40 +02:00
if ( location ! = default )
2020-08-03 14:41:25 +02:00
{
var userId = location . Substring ( location . IndexOf ( "=" ) + 1 ) ;
2021-10-04 18:10:36 +02:00
if ( sessionIdentifier ! = userId )
{
sessionIdentifier = userId ;
Task . Run ( ( ) = > SessionIdentifierDetected ? . Invoke ( sessionIdentifier ) ) ;
logger . Info ( "Moodle session detected." ) ;
}
2021-02-23 15:32:08 +01:00
return true ;
2020-08-03 14:41:25 +02:00
}
}
catch ( Exception e )
{
logger . Error ( "Failed to parse Moodle session identifier!" , e ) ;
}
}
2021-02-23 15:32:08 +01:00
return false ;
}
2021-03-04 17:25:19 +01:00
private void TrySearchBySession ( IRequest request , IResponse response )
2021-02-23 15:32:08 +01:00
{
var cookies = response . Headers . GetValues ( "Set-Cookie" ) ;
if ( cookies ! = default ( string [ ] ) )
{
var session = cookies . FirstOrDefault ( c = > c . Contains ( "MoodleSession" ) ) ;
2022-07-26 17:56:40 +02:00
if ( session ! = default )
2021-02-23 15:32:08 +01:00
{
2021-03-04 17:25:19 +01:00
var requestUrl = request . Url ;
2021-02-23 15:32:08 +01:00
2021-03-04 17:25:19 +01:00
Task . Run ( async ( ) = >
{
try
2021-02-23 15:32:08 +01:00
{
2021-03-04 17:25:19 +01:00
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" ) ;
2021-02-23 15:32:08 +01:00
using ( var handler = new HttpClientHandler { UseCookies = false } )
using ( var client = new HttpClient ( handler ) )
{
message . Headers . Add ( "Cookie" , $"MoodleSession={value}" ) ;
2021-03-04 17:25:19 +01:00
var result = await client . SendAsync ( message ) ;
if ( result . IsSuccessStatusCode )
{
var userId = await result . Content . ReadAsStringAsync ( ) ;
2021-10-04 18:10:36 +02:00
if ( int . TryParse ( userId , out var id ) & & id > 0 & & sessionIdentifier ! = userId )
2021-03-04 17:25:19 +01:00
{
#pragma warning disable CS4014
2021-10-04 18:10:36 +02:00
sessionIdentifier = userId ;
Task . Run ( ( ) = > SessionIdentifierDetected ? . Invoke ( sessionIdentifier ) ) ;
2021-03-04 17:25:19 +01:00
logger . Info ( "Moodle session detected." ) ;
2021-10-04 18:10:36 +02:00
#pragma warning restore CS4014
2021-03-04 17:25:19 +01:00
}
}
else
{
logger . Error ( $"Failed to retrieve Moodle session identifier! Response: {result.StatusCode} {result.ReasonPhrase}" ) ;
}
2021-02-23 15:32:08 +01:00
}
2021-03-04 17:25:19 +01:00
}
catch ( Exception e )
2021-02-23 15:32:08 +01:00
{
2021-03-04 17:25:19 +01:00
logger . Error ( "Failed to parse Moodle session identifier!" , e ) ;
2021-02-23 15:32:08 +01:00
}
2021-03-04 17:25:19 +01:00
} ) ;
2021-02-23 15:32:08 +01:00
}
}
2020-08-03 14:41:25 +02:00
}
2019-09-10 11:01:49 +02:00
}
}