@@ -8,18 +8,17 @@ import org.apache.commons.codec.digest.DigestUtils
88import org .cryptonode .jncryptor .AES256JNCryptor
99import play .Environment
1010import play .api .Logging
11- import play .api .libs .json ._
12- import play .mvc .{Http , Result , Results }
11+ import play .api .libs .json .*
12+ import play .api . mvc .{Result , Results , RequestHeader }
1313
1414import java .io .ByteArrayOutputStream
1515import java .net .URI
1616import java .nio .charset .StandardCharsets
17- import java .util .Optional
1817import java .util .zip .GZIPOutputStream
1918import javax .inject .Inject
2019import javax .xml .parsers .SAXParserFactory
20+
2121import scala .io .Source
22- import scala .jdk .OptionConverters ._
2322import scala .xml .{Node , XML }
2423
2524object ByodConfigHandlerImpl :
@@ -29,14 +28,16 @@ object ByodConfigHandlerImpl:
2928 private val AdminPwdPlaceholder = " *** adminPwd ***"
3029 private val AllowQuittingPlaceholder = " <!-- allowQuit /-->"
3130 private val PasswordEncryption = " pswd"
32- private val ConfigKeyHeader = " X-SafeExamBrowser-ConfigKeyHash"
31+ private val ConfigKeyHeader = " X-SafeExamBrowser-ConfigKeyHash" // Standard SEB header
32+ private val CustomConfigKeyHeader = " X-Exam-Seb-Config-Key" // Custom header from JS API
33+ private val CustomConfigUrlHeader = " X-Exam-Seb-Config-Url" // Custom header from JS API
3334 private val IgnoredKeys = Seq (" originatorVersion" )
3435
3536class ByodConfigHandlerImpl @ Inject () (configReader : ConfigReader , env : Environment )
3637 extends ByodConfigHandler
3738 with Logging :
3839
39- import ByodConfigHandlerImpl ._
40+ import ByodConfigHandlerImpl .*
4041 private val crypto = new AES256JNCryptor
4142 private val encryptionKey = configReader.getSettingsPasswordEncryptionKey
4243
@@ -117,19 +118,43 @@ class ByodConfigHandlerImpl @Inject() (configReader: ConfigReader, env: Environm
117118 override def getEncryptedPassword (pwd : String , salt : String ): Array [Byte ] =
118119 crypto.encryptData((pwd + salt).getBytes(StandardCharsets .UTF_8 ), encryptionKey.toCharArray)
119120
120- override def checkUserAgent (request : Http .RequestHeader , configKey : String ): Optional [Result ] =
121- request.header(ConfigKeyHeader ).toScala match {
122- case None => Some (Results .unauthorized(" SEB headers missing" )).toJava
123- case Some (digest) =>
124- val absoluteUrl = s " $protocol:// ${request.host}${request.uri}"
125- DigestUtils .sha256Hex(absoluteUrl + configKey) match
126- case sha if sha == digest => None .toJava
127- case sha =>
128- logger.warn(
129- s " Config key mismatch for URL $absoluteUrl and exam config key $configKey. Digest received: $sha"
130- )
131- Some (Results .unauthorized(" Wrong configuration key digest" )).toJava
132- }
121+ override def checkUserAgent (request : RequestHeader , configKey : String ): Option [Result ] =
122+ // Check both standard SEB header (old API) and custom JS API header (new API)
123+ val standardHeader = request.headers.get(ConfigKeyHeader )
124+ val customHeader = request.headers.get(CustomConfigKeyHeader )
125+
126+ (standardHeader, customHeader) match
127+ case (None , None ) =>
128+ logger.warn(
129+ s """ SEB headers MISSING from request to ${request.uri}.
130+ |Checked: ' $ConfigKeyHeader' and ' $CustomConfigKeyHeader' """ .stripMargin.replaceAll(" \n " , " " )
131+ )
132+ Some (Results .Unauthorized (" SEB headers missing" ))
133+ case (Some (digest), _) =>
134+ // Standard SEB header present (old API) - automatically sent by SEB with classic WebView
135+ logger.debug(s " Using STANDARD SEB header: $ConfigKeyHeader" )
136+ validate(s " $protocol:// ${request.host}${request.uri}" , digest, configKey)
137+ case (None , Some (digest)) =>
138+ // Custom header from JavaScript API (new API) - modern WebView
139+ logger.debug(s " Using CUSTOM header from JS API: $CustomConfigKeyHeader" )
140+ request.headers.get(CustomConfigUrlHeader ) match
141+ case Some (url) => validate(url, digest, configKey)
142+ case None =>
143+ logger.warn(s " SEB validation FAILED (JavaScript API): $CustomConfigUrlHeader header is missing " )
144+ Some (Results .Unauthorized (" SEB page URL header missing" ))
145+
146+ private def validate (url : String , digest : String , configKey : String ): Option [Result ] =
147+ DigestUtils .sha256Hex(url + configKey) match
148+ case expected if expected == digest => None
149+ case expected =>
150+ logger.warn(
151+ s """ SEB validation FAILED!
152+ |PageURL= $url,
153+ |ConfigFileHash= $configKey,
154+ |ExpectedDigest= $expected,
155+ |ReceivedKey= $digest""" .stripMargin.replaceAll(" \n " , " " )
156+ )
157+ Some (Results .Unauthorized (" Wrong configuration key digest" ))
133158
134159 override def calculateConfigKey (hash : String , quitPwd : String ): String =
135160 // Override the DTD setting. We need it with PLIST format and to integrate with SBT
0 commit comments