2019-09-10 11:01:49 +02:00
/ *
2024-03-05 18:37:42 +01:00
* Copyright ( c ) 2024 ETH Zürich , IT Services
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 ;
2023-11-01 13:52:39 +01:00
private string userIdentifier ;
2019-09-10 11:01:49 +02:00
2023-11-01 13:52:39 +01:00
internal event UserIdentifierDetectedEventHandler UserIdentifierDetected ;
2020-08-03 14:41:25 +02:00
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 )
{
2023-11-01 13:52:39 +01:00
SearchUserIdentifier ( request , response ) ;
2023-03-08 00:01:20 +01:00
}
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 )
{
2023-11-01 13:52:39 +01:00
SearchUserIdentifier ( request , response ) ;
2023-03-08 00:01:20 +01:00
}
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 ) ;
2023-05-02 17:35:52 +02:00
if ( request . ResourceType = = ResourceType . MainFrame | | 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
2023-11-01 13:52:39 +01:00
private void SearchUserIdentifier ( IRequest request , IResponse response )
2020-08-03 14:41:25 +02:00
{
2023-11-01 13:52:39 +01:00
var success = TrySearchGenericUserIdentifier ( response ) ;
2021-04-21 19:53:52 +02:00
if ( ! success )
{
2023-11-01 13:52:39 +01:00
success = TrySearchEdxUserIdentifier ( response ) ;
}
if ( ! success )
{
TrySearchMoodleUserIdentifier ( request , response ) ;
2021-04-21 19:53:52 +02:00
}
}
2023-11-01 13:52:39 +01:00
private bool TrySearchGenericUserIdentifier ( IResponse response )
2021-04-21 19:53:52 +02:00
{
var ids = response . Headers . GetValues ( "X-LMS-USER-ID" ) ;
2023-11-01 13:52:39 +01:00
var success = false ;
2021-04-21 19:53:52 +02:00
if ( ids ! = default ( string [ ] ) )
{
var userId = ids . FirstOrDefault ( ) ;
2023-11-01 13:52:39 +01:00
if ( userId ! = default & & userIdentifier ! = userId )
2021-04-21 19:53:52 +02:00
{
2023-11-01 13:52:39 +01:00
userIdentifier = userId ;
Task . Run ( ( ) = > UserIdentifierDetected ? . Invoke ( userIdentifier ) ) ;
logger . Info ( "Generic LMS user identifier detected." ) ;
success = true ;
2021-04-21 19:53:52 +02:00
}
}
2023-11-01 13:52:39 +01:00
return success ;
2020-08-03 14:41:25 +02:00
}
2023-11-01 13:52:39 +01:00
private bool TrySearchEdxUserIdentifier ( IResponse response )
2020-08-03 14:41:25 +02:00
{
var cookies = response . Headers . GetValues ( "Set-Cookie" ) ;
2023-11-01 13:52:39 +01:00
var success = false ;
2020-08-03 14:41:25 +02:00
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 > ( ) ;
2023-11-01 13:52:39 +01:00
if ( userIdentifier ! = userName )
2021-10-04 18:10:36 +02:00
{
2023-11-01 13:52:39 +01:00
userIdentifier = userName ;
Task . Run ( ( ) = > UserIdentifierDetected ? . Invoke ( userIdentifier ) ) ;
logger . Info ( "EdX user identifier detected." ) ;
success = true ;
2021-10-04 18:10:36 +02:00
}
2020-08-03 14:41:25 +02:00
}
}
catch ( Exception e )
{
2023-11-01 13:52:39 +01:00
logger . Error ( "Failed to parse edX user identifier!" , e ) ;
2020-08-03 14:41:25 +02:00
}
}
2023-11-01 13:52:39 +01:00
return success ;
2020-08-03 14:41:25 +02:00
}
2023-11-01 13:52:39 +01:00
private bool TrySearchMoodleUserIdentifier ( IRequest request , IResponse response )
2021-02-23 15:32:08 +01:00
{
2023-11-01 13:52:39 +01:00
var success = TrySearchMoodleUserIdentifierByLocation ( response ) ;
if ( ! success )
{
success = TrySearchMoodleUserIdentifierByRequest ( MoodleRequestType . Plugin , request , response ) ;
}
2021-02-23 15:32:08 +01:00
if ( ! success )
{
2023-11-01 13:52:39 +01:00
success = TrySearchMoodleUserIdentifierByRequest ( MoodleRequestType . Theme , request , response ) ;
2021-02-23 15:32:08 +01:00
}
2023-11-01 13:52:39 +01:00
return success ;
2021-02-23 15:32:08 +01:00
}
2023-11-01 13:52:39 +01:00
private bool TrySearchMoodleUserIdentifierByLocation ( 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 ) ;
2023-11-01 13:52:39 +01:00
if ( userIdentifier ! = userId )
2021-10-04 18:10:36 +02:00
{
2023-11-01 13:52:39 +01:00
userIdentifier = userId ;
Task . Run ( ( ) = > UserIdentifierDetected ? . Invoke ( userIdentifier ) ) ;
logger . Info ( "Moodle user identifier detected by location." ) ;
2021-10-04 18:10:36 +02:00
}
2021-02-23 15:32:08 +01:00
return true ;
2020-08-03 14:41:25 +02:00
}
}
catch ( Exception e )
{
2023-11-01 13:52:39 +01:00
logger . Error ( "Failed to parse Moodle user identifier by location!" , e ) ;
2020-08-03 14:41:25 +02:00
}
}
2021-02-23 15:32:08 +01:00
return false ;
}
2023-11-01 13:52:39 +01:00
private bool TrySearchMoodleUserIdentifierByRequest ( MoodleRequestType type , IRequest request , IResponse response )
2021-02-23 15:32:08 +01:00
{
var cookies = response . Headers . GetValues ( "Set-Cookie" ) ;
2023-11-01 13:52:39 +01:00
var success = false ;
2021-02-23 15:32:08 +01:00
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
{
2023-11-01 13:52:39 +01:00
var userId = ExecuteMoodleUserIdentifierRequest ( request . Url , session , type ) ;
2021-02-23 15:32:08 +01:00
2023-11-01 13:52:39 +01:00
if ( int . TryParse ( userId , out var id ) & & id > 0 & & userIdentifier ! = userId )
{
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
2021-03-04 17:25:19 +01:00
{
2023-11-01 13:52:39 +01:00
var endpointUrl = default ( string ) ;
var start = session . IndexOf ( "=" ) + 1 ;
var end = session . IndexOf ( ";" ) ;
2024-04-17 12:05:34 +02:00
var name = session . Substring ( 0 , start - 1 ) ;
2023-11-01 13:52:39 +01:00
var value = session . Substring ( start , end - start ) ;
var uri = new Uri ( requestUrl ) ;
if ( type = = MoodleRequestType . Plugin )
2021-02-23 15:32:08 +01:00
{
2023-11-01 13:52:39 +01:00
endpointUrl = $"{uri.Scheme}{Uri.SchemeDelimiter}{uri.Host}/mod/quiz/accessrule/sebserver/classes/external/user.php" ;
2021-03-04 17:25:19 +01:00
}
2023-11-01 13:52:39 +01:00
else
2021-02-23 15:32:08 +01:00
{
2023-11-01 13:52:39 +01:00
endpointUrl = $"{uri.Scheme}{Uri.SchemeDelimiter}{uri.Host}/theme/boost_ethz/sebuser.php" ;
2021-02-23 15:32:08 +01:00
}
2023-11-01 13:52:39 +01:00
var message = new HttpRequestMessage ( HttpMethod . Get , endpointUrl ) ;
using ( var handler = new HttpClientHandler { UseCookies = false } )
using ( var client = new HttpClient ( handler ) )
{
2024-04-17 12:05:34 +02:00
message . Headers . Add ( "Cookie" , $"{name}={value}" ) ;
2023-11-01 13:52:39 +01:00
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 ( ) ;
2021-02-23 15:32:08 +01:00
}
2023-11-01 13:52:39 +01:00
catch ( Exception e )
{
logger . Error ( $"Failed to execute Moodle user identifier request ({type})!" , e ) ;
}
return userId ;
}
private enum MoodleRequestType
{
Plugin ,
Theme
2020-08-03 14:41:25 +02:00
}
2019-09-10 11:01:49 +02:00
}
}