Skip to content

Commit 8530d98

Browse files
authored
Merge branch 'dev' into CSCEXAM-1158
2 parents 258c5e3 + 677a22b commit 8530d98

File tree

86 files changed

+1660
-1552
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

86 files changed

+1660
-1552
lines changed

angular.json

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,6 @@
5050
{
5151
"replace": "ui/src/environments/environment.ts",
5252
"with": "ui/src/environments/environment.prod.ts"
53-
},
54-
{
55-
"replace": "ui/src/app/interceptors/auth-interceptor.ts",
56-
"with": "ui/src/app/interceptors/auth-interceptor.prod.ts"
5753
}
5854
],
5955
"outputHashing": "all",

app/controllers/examination/ExaminationController.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import play.mvc.With;
5353
import repository.ExaminationRepository;
5454
import scala.concurrent.duration.Duration;
55+
import scala.jdk.javaapi.OptionConverters;
5556
import security.Authenticated;
5657
import system.interceptors.ExamActionRouter;
5758
import system.interceptors.SensitiveDataPolicy;
@@ -449,7 +450,11 @@ protected CompletionStage<Optional<Result>> getEnrolmentError(ExamEnrolment enro
449450
boolean isUnchecked = exam != null && exam.getImplementation() == Exam.Implementation.WHATEVER;
450451
if (isByod) {
451452
return CompletableFuture.completedFuture(
452-
byodConfigHandler.checkUserAgent(request, enrolment.getExaminationEventConfiguration().getConfigKey())
453+
OptionConverters.toJava(
454+
byodConfigHandler
455+
.checkUserAgent(request.asScala(), enrolment.getExaminationEventConfiguration().getConfigKey())
456+
.map(play.api.mvc.Result::asJava)
457+
)
453458
);
454459
} else if (isUnchecked) {
455460
return CompletableFuture.completedFuture(Optional.empty());

app/miscellaneous/config/ByodConfigHandler.scala

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,11 @@
44

55
package miscellaneous.config
66

7-
import play.mvc.{Http, Result}
8-
9-
import java.util.Optional
7+
import play.api.mvc.{RequestHeader, Result}
108

119
trait ByodConfigHandler:
1210
def getExamConfig(hash: String, pwd: Array[Byte], salt: String, quitPwd: String): Array[Byte]
1311
def calculateConfigKey(hash: String, quitPwd: String): String
1412
def getPlaintextPassword(pwd: Array[Byte], salt: String): String
1513
def getEncryptedPassword(pwd: String, salt: String): Array[Byte]
16-
def checkUserAgent(request: Http.RequestHeader, examConfigKey: String): Optional[Result]
14+
def checkUserAgent(request: RequestHeader, examConfigKey: String): Option[Result]

app/miscellaneous/config/ByodConfigHandlerImpl.scala

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,17 @@ import org.apache.commons.codec.digest.DigestUtils
88
import org.cryptonode.jncryptor.AES256JNCryptor
99
import play.Environment
1010
import 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

1414
import java.io.ByteArrayOutputStream
1515
import java.net.URI
1616
import java.nio.charset.StandardCharsets
17-
import java.util.Optional
1817
import java.util.zip.GZIPOutputStream
1918
import javax.inject.Inject
2019
import javax.xml.parsers.SAXParserFactory
20+
2121
import scala.io.Source
22-
import scala.jdk.OptionConverters._
2322
import scala.xml.{Node, XML}
2423

2524
object 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

3536
class 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

app/repository/EnrolmentRepository.java

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import models.user.Role;
3030
import models.user.User;
3131
import org.apache.commons.codec.binary.Base64;
32+
import org.apache.pekko.util.OptionConverters;
3233
import org.joda.time.DateTime;
3334
import org.joda.time.DateTimeZone;
3435
import org.joda.time.Minutes;
@@ -193,13 +194,22 @@ private boolean isMachineOk(ExamEnrolment enrolment, Http.RequestHeader request,
193194
enrolment.getExam() != null && enrolment.getExam().getImplementation() == Exam.Implementation.CLIENT_AUTH;
194195

195196
if (requiresClientAuth) {
197+
logger.info("Checking SEB config...");
196198
// SEB examination
197199
ExaminationEventConfiguration config = enrolment.getExaminationEventConfiguration();
198-
Optional<Result> error = byodConfigHandler.checkUserAgent(request, config.getConfigKey());
200+
Optional<Result> error = OptionConverters.toJava(
201+
byodConfigHandler
202+
.checkUserAgent(request.asScala(), config.getConfigKey())
203+
.map(play.api.mvc.Result::asJava)
204+
);
205+
199206
if (error.isPresent()) {
200207
String msg = ISODateTimeFormat.dateTime().print(new DateTime(config.getExaminationEvent().getStart()));
201208
headers.put("x-exam-wrong-agent-config", msg);
209+
logger.warn("Wrong agent config for SEB");
202210
return false;
211+
} else {
212+
logger.info("SEB config OK");
203213
}
204214
} else if (requiresReservation) {
205215
// Aquarium examination

conf/template/seb.template.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@
171171
<key>browserWindowTitleSuffix</key>
172172
<string></string>
173173
<key>browserWindowWebView</key>
174-
<integer>2</integer>
174+
<integer>3</integer>
175175
<key>chooseFileToUploadPolicy</key>
176176
<integer>0</integer>
177177
<key>configFileCreateIdentity</key>

project/Karma.scala

Lines changed: 0 additions & 35 deletions
This file was deleted.

project/MockCourseInfo.scala

Lines changed: 0 additions & 35 deletions
This file was deleted.

project/NoOp.scala

Lines changed: 0 additions & 26 deletions
This file was deleted.

project/Protractor.scala

Lines changed: 0 additions & 46 deletions
This file was deleted.

0 commit comments

Comments
 (0)