diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java index fc177e97..364b474e 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/ClientConnectionDAO.java @@ -52,6 +52,13 @@ public interface ClientConnectionDAO extends return getConnectionTokens(examId); } + /** Get a list of all connection tokens of all connections of an exam + * that are in state active + * + * @param examId The exam identifier + * @return Result refer to the collection of connection tokens or to an error when happened */ + Result> getActiveConnctionTokens(Long examId); + /** Get a collection of all client connections records that needs a room update * and that are in the status ACTIVE. * This also flags the involved connections for no update needed within the diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java index bceccb38..d437b2c7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/dao/impl/ClientConnectionDAOImpl.java @@ -146,6 +146,25 @@ public class ClientConnectionDAOImpl implements ClientConnectionDAO { .collect(Collectors.toList())); } + @Override + @Transactional(readOnly = true) + public Result> getActiveConnctionTokens(final Long examId) { + return Result.tryCatch(() -> this.clientConnectionRecordMapper + .selectByExample() + .where( + ClientConnectionRecordDynamicSqlSupport.examId, + SqlBuilder.isEqualTo(examId)) + .and( + ClientConnectionRecordDynamicSqlSupport.status, + SqlBuilder.isEqualTo(ConnectionStatus.ACTIVE.name())) + .build() + .execute() + .stream() + .map(ClientConnectionRecord::getConnectionToken) + .filter(StringUtils::isNotBlank) + .collect(Collectors.toList())); + } + @Override @Transactional public Result> getAllConnectionIdsForRoomUpdateActive() { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java index e9c9f092..0bbeb237 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/ExamAdminService.java @@ -45,25 +45,25 @@ public interface ExamAdminService { * * @param examId the exam instance * @return Result refer to ExamProctoring data for the exam. */ - default Result getExamProctoring(final Exam exam) { + default Result getExamProctoringSettings(final Exam exam) { if (exam == null || exam.id == null) { return Result.ofRuntimeError("Invalid Exam model"); } - return getExamProctoring(exam.id); + return getExamProctoringSettings(exam.id); } /** Get ExamProctoring data for a certain exam to an error when happened. * * @param examId the exam identifier * @return Result refer to ExamProctoring data for the exam. */ - Result getExamProctoring(Long examId); + Result getExamProctoringSettings(Long examId); /** Save the given ExamProctoring data for an existing Exam. * * @param examId the exam identifier * @param examProctoring The ExamProctoring data to save for the exam * @return Result refer to saved ExamProctoring data or to an error when happened. */ - Result saveExamProctoring(Long examId, ProctoringSettings examProctoring); + Result saveExamProctoringSettings(Long examId, ProctoringSettings examProctoring); /** This indicates if proctoring is set and enabled for a certain exam. * @@ -85,7 +85,25 @@ public interface ExamAdminService { /** Get the exam proctoring service implementation of specified type. * * @param type exam proctoring service server type - * @return Result refer to the ExamProctoringService or to an error when happened */ - public Result getExamProctoringService(final ProctoringServerType type); + * @return ExamProctoringService instance */ + Result getExamProctoringService(final ProctoringServerType type); + + /** Get the exam proctoring service implementation of specified type. + * + * @param settings the ProctoringSettings that defines the ProctoringServerType + * @return ExamProctoringService instance */ + default Result getExamProctoringService(final ProctoringSettings settings) { + return Result.tryCatch(() -> getExamProctoringService(settings.serverType).getOrThrow()); + } + + default Result getExamProctoringService(final Exam exam) { + return Result.tryCatch(() -> getExamProctoringService(exam.id).getOrThrow()); + } + + default Result getExamProctoringService(final Long examId) { + return getExamProctoringSettings(examId) + .flatMap(this::getExamProctoringService); + + } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java index 7f1b071c..eeea1a99 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/exam/impl/ExamAdminServiceImpl.java @@ -186,7 +186,7 @@ public class ExamAdminServiceImpl implements ExamAdminService { } @Override - public Result getExamProctoring(final Long examId) { + public Result getExamProctoringSettings(final Long examId) { return this.additionalAttributesDAO.getAdditionalAttributes(EntityType.EXAM, examId) .map(attrs -> attrs.stream() .collect(Collectors.toMap( @@ -206,7 +206,8 @@ public class ExamAdminServiceImpl implements ExamAdminService { @Override @Transactional - public Result saveExamProctoring(final Long examId, final ProctoringSettings examProctoring) { + public Result saveExamProctoringSettings(final Long examId, + final ProctoringSettings examProctoring) { return Result.tryCatch(() -> { this.additionalAttributesDAO.saveAdditionalAttribute( @@ -264,7 +265,8 @@ public class ExamAdminServiceImpl implements ExamAdminService { @Override public Result getExamProctoringService(final ProctoringServerType type) { - return this.examProctoringServiceFactory.getExamProctoringService(type); + return this.examProctoringServiceFactory + .getExamProctoringService(type); } private Boolean getEnabled(final Map mapping) { diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java index 80da5551..2c294959 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringRoomService.java @@ -14,6 +14,8 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.util.Result; +/** Defines functionality to deal with proctoring rooms in a generic way (independent from meeting service) */ + public interface ExamProctoringRoomService { /** Get all existing default proctoring rooms of an exam. @@ -40,8 +42,10 @@ public interface ExamProctoringRoomService { * This attaches or detaches client connections from or to proctoring rooms of an exam in one batch. * New client connections that are coming in and are established only mark itself for * proctoring room update if proctoring is enabled for the specified exam. This batch processing - * then makes the update synchronous to not create to to many rooms or several rooms with the same - * name of an exam. */ + * then makes the update synchronously to keep track on room creation and naming + * + * If for a specified exam the town-hall room is active incoming client connection are instructed to + * join the town-hall room. If not, incoming client connection are instructed to join a collecting room. */ void updateProctoringCollectingRooms(); /** This creates a town-hall room for a specific exam. The exam must be active and running diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java index 965bc52a..a1937d19 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamProctoringService.java @@ -8,11 +8,17 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session; +import java.util.Base64; +import java.util.Base64.Encoder; +import java.util.Collection; + +import org.apache.commons.lang3.StringUtils; + import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection; -import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.util.Result; +import ch.ethz.seb.sebserver.gbl.util.Utils; public interface ExamProctoringService { @@ -27,49 +33,69 @@ public interface ExamProctoringService { * @return Result refer to true if the settings are correct and the proctoring server can be accessed. */ Result testExamProctoring(final ProctoringSettings examProctoring); - /** Used to get the proctor's room connection data. - * - * @param proctoringSettings the proctoring settings - * @param roomName the name of the room - * @param subject name of the room - * @return SEBProctoringConnectionData that contains all connection data */ - Result createProctorPublicRoomConnection( - final ProctoringSettings proctoringSettings, - final String roomName, - final String subject); + Result getProctorRoomConnection( + ProctoringSettings proctoringSettings, + String roomName, + String subject); - Result getClientExamCollectingRoomConnection( - final ProctoringSettings proctoringSettings, - final ClientConnection connection); + Result sendJoinRoomToClients( + ProctoringSettings proctoringSettings, + Collection clientConnectionTokens, + String roomName, + String subject); - Result getClientExamCollectingRoomConnection( - final ProctoringSettings proctoringSettings, - final String connectionToken, - final String roomName, - final String subject); + Result sendJoinCollectingRoomToClients( + ProctoringSettings proctoringSettings, + Collection clientConnectionTokens); - Result getClientRoomConnection( - final ProctoringSettings examProctoring, - final String connectionToken, - final String roomName, - final String subject); + default String verifyRoomName(final String requestedRoomName, final String connectionToken) { + if (StringUtils.isNotBlank(requestedRoomName)) { + return requestedRoomName; + } - Result createProctoringConnection( - final ProctoringServerType proctoringServerType, - final String connectionToken, - final String url, - final String appKey, - final CharSequence appSecret, - final String clientName, - final String clientKey, - final String roomName, - final String subject, - final Long expTime, - final boolean moderator); + final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding(); + return urlEncoder.encodeToString( + Utils.toByteArray(connectionToken)); + } - Result createClientAccessToken( - final ProctoringSettings proctoringSettings, - final String connectionToken, - final String roomName); +// /** Used to get the proctor's room connection data. +// * +// * @param proctoringSettings the proctoring settings +// * @param roomName the name of the room +// * @param subject name of the room +// * @return SEBProctoringConnectionData that contains all connection data */ +// Result createProctorPublicRoomConnection( +// final ProctoringSettings proctoringSettings, +// final String roomName, +// final String subject); +// +// Result getClientExamCollectingRoomConnection( +// final ProctoringSettings proctoringSettings, +// final ClientConnection connection); +// +// Result getClientExamCollectingRoomConnection( +// final ProctoringSettings proctoringSettings, +// final String connectionToken, +// final String roomName, +// final String subject); +// +// Result getClientRoomConnection( +// final ProctoringSettings examProctoring, +// final String connectionToken, +// final String roomName, +// final String subject); +// +// Result createProctoringConnection( +// final ProctoringServerType proctoringServerType, +// final String connectionToken, +// final String url, +// final String appKey, +// final CharSequence appSecret, +// final String clientName, +// final String clientKey, +// final String roomName, +// final String subject, +// final Long expTime, +// final boolean moderator); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java index 7163b65c..a1a68ece 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/ExamSessionService.java @@ -155,9 +155,12 @@ public interface ExamSessionService { Long examId, Predicate filter); - default Result> getAllActiveConnectionData(final Long examId) { - return getConnectionData(examId, ACTIVE_CONNECTION_DATA_FILTER); - } + /** Gets all connection tokens of active client connection that are related to a specified exam + * from persistence storage without caching involved. + * + * @param examId the exam identifier + * @return Result refer to the collection of connection tokens or to an error when happened. */ + Result> getActiveConnectionTokens(final Long examId); /** Use this to check if the current cached running exam is up to date * and if not to flush the cache. diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java index 63b21282..88b6846d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java @@ -13,10 +13,16 @@ import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.Base64.Encoder; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; +import org.apache.commons.lang3.StringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; @@ -26,8 +32,9 @@ import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection; -import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnectionData; +import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; +import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Cryptor; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -36,12 +43,15 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.Authorization import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; @Lazy @Service @WebServiceProfile public class ExamJITSIProctoringService implements ExamProctoringService { + private static final Logger log = LoggerFactory.getLogger(ExamJITSIProctoringService.class); + private static final String JITSI_ACCESS_TOKEN_HEADER = "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; @@ -51,17 +61,20 @@ public class ExamJITSIProctoringService implements ExamProctoringService { private final RemoteProctoringRoomDAO remoteProctoringRoomDAO; private final AuthorizationService authorizationService; private final ExamSessionService examSessionService; + private final SEBClientInstructionService sebClientInstructionService; private final Cryptor cryptor; protected ExamJITSIProctoringService( final RemoteProctoringRoomDAO remoteProctoringRoomDAO, final AuthorizationService authorizationService, final ExamSessionService examSessionService, + final SEBClientInstructionService sebClientInstructionService, final Cryptor cryptor) { this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; this.authorizationService = authorizationService; this.examSessionService = examSessionService; + this.sebClientInstructionService = sebClientInstructionService; this.cryptor = cryptor; } @@ -77,7 +90,124 @@ public class ExamJITSIProctoringService implements ExamProctoringService { } @Override - public Result createProctorPublicRoomConnection( + public Result getProctorRoomConnection( + final ProctoringSettings proctoringSettings, + final String roomName, + final String subject) { + + return this.createProctorPublicRoomConnection( + proctoringSettings, + roomName, + StringUtils.isNoneBlank(subject) ? subject : roomName); + } + + @Override + public Result sendJoinRoomToClients( + final ProctoringSettings proctoringSettings, + final Collection clientConnectionTokens, + final String roomName, + final String subject) { + + return Result.tryCatch(() -> { + clientConnectionTokens + .stream() + .forEach(connectionToken -> { + final SEBProctoringConnection proctoringConnection = + getClientRoomConnection( + proctoringSettings, + connectionToken, + verifyRoomName(roomName, connectionToken), + (StringUtils.isNotBlank(subject)) ? subject : roomName) + .onError(error -> log.error( + "Failed to get client room connection data for {} cause: {}", + connectionToken, + error.getMessage())) + .get(); + if (proctoringConnection != null) { + sendJoinInstruction( + proctoringSettings.examId, + connectionToken, + proctoringConnection); + } + }); + + return createProctorPublicRoomConnection( + proctoringSettings, + roomName, + (StringUtils.isNotBlank(subject)) ? subject : roomName) + .getOrThrow(); + }); + } + + @Override + public Result sendJoinCollectingRoomToClients( + final ProctoringSettings proctoringSettings, + final Collection clientConnectionTokens) { + + return Result.tryCatch(() -> { + clientConnectionTokens + .stream() + .forEach(connectionToken -> { + final ClientConnectionData clientConnection = this.examSessionService + .getConnectionData(connectionToken) + .getOrThrow(); + final String roomName = this.remoteProctoringRoomDAO + .getRoomName(clientConnection.clientConnection.getRemoteProctoringRoomId()) + .getOrThrow(); + + final SEBProctoringConnection proctoringConnection = getClientExamCollectingRoomConnection( + proctoringSettings, + clientConnection.clientConnection.connectionToken, + roomName, + clientConnection.clientConnection.userSessionId) + .getOrThrow(); + + sendJoinInstruction( + proctoringSettings.examId, + clientConnection.clientConnection.connectionToken, + proctoringConnection); + }); + }); + } + + private void sendJoinInstruction( + final Long examId, + final String connectionToken, + final SEBProctoringConnection proctoringConnection) { + + final Map attributes = new HashMap<>(); + + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE, + ProctoringSettings.ProctoringServerType.JITSI_MEET.name()); + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD, + ClientInstruction.ProctoringInstructionMethod.JOIN.name()); + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL, + proctoringConnection.serverURL); + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM, + proctoringConnection.roomName); + if (StringUtils.isNotBlank(proctoringConnection.subject)) { + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM_SUBJECT, + proctoringConnection.subject); + } + attributes.put( + ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN, + proctoringConnection.accessToken); + + this.sebClientInstructionService.registerInstruction( + examId, + InstructionType.SEB_PROCTORING, + attributes, + connectionToken, + true) + .onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error)); + } + + private Result createProctorPublicRoomConnection( final ProctoringSettings proctoringSettings, final String roomName, final String subject) { @@ -99,35 +229,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService { }); } - @Override - public Result getClientExamCollectingRoomConnection( - final ProctoringSettings proctoringSettings, - final ClientConnection connection) { - - return Result.tryCatch(() -> { - - final String roomName = this.remoteProctoringRoomDAO - .getRoomName(connection.getRemoteProctoringRoomId()) - .getOrThrow(); - - return createProctoringConnection( - proctoringSettings.serverType, - null, - proctoringSettings.serverURL, - proctoringSettings.appKey, - proctoringSettings.getAppSecret(), - connection.userSessionId, - "seb-client", - roomName, - connection.userSessionId, - forExam(proctoringSettings), - false) - .getOrThrow(); - }); - } - - @Override - public Result getClientExamCollectingRoomConnection( + private Result getClientExamCollectingRoomConnection( final ProctoringSettings proctoringSettings, final String connectionToken, final String roomName, @@ -154,8 +256,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService { }); } - @Override - public Result getClientRoomConnection( + private Result getClientRoomConnection( final ProctoringSettings proctoringSettings, final String connectionToken, final String roomName, @@ -185,8 +286,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService { } - @Override - public Result createProctoringConnection( + protected Result createProctoringConnection( final ProctoringServerType proctoringServerType, final String connectionToken, final String url, @@ -227,35 +327,6 @@ public class ExamJITSIProctoringService implements ExamProctoringService { }); } - @Override - public Result createClientAccessToken( - final ProctoringSettings proctoringSettings, - final String connectionToken, - final String roomName) { - - return Result.tryCatch(() -> { - - final ClientConnectionData connectionData = this.examSessionService - .getConnectionData(connectionToken) - .getOrThrow(); - - final String host = UriComponentsBuilder.fromHttpUrl(proctoringSettings.serverURL) - .build() - .getHost(); - final CharSequence decryptedSecret = this.cryptor.decrypt(proctoringSettings.appSecret); - - return internalCreateAccessToken( - proctoringSettings.appKey, - decryptedSecret, - connectionData.clientConnection.userSessionId, - "seb-client", - roomName, - forExam(proctoringSettings), - host, - false); - }); - } - protected String internalCreateAccessToken( final String appKey, final CharSequence appSecret, diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java index 3c90f48f..d7cd4639 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamProctoringRoomServiceImpl.java @@ -8,9 +8,8 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; +import java.util.Arrays; import java.util.Collection; -import java.util.HashMap; -import java.util.Map; import java.util.UUID; import org.slf4j.Logger; @@ -19,12 +18,7 @@ import org.springframework.context.annotation.Lazy; import org.springframework.stereotype.Service; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings; -import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringSettings.ProctoringServerType; -import ch.ethz.seb.sebserver.gbl.model.exam.SEBProctoringConnection; import ch.ethz.seb.sebserver.gbl.model.session.ClientConnection; -import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; -import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; -import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.ProctoringInstructionMethod; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.util.Result; @@ -34,7 +28,6 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.dao.RemoteProctoringRoomDAO import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamProctoringRoomService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; -import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructionService; @Lazy @Service @@ -45,20 +38,17 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService private final RemoteProctoringRoomDAO remoteProctoringRoomDAO; private final ClientConnectionDAO clientConnectionDAO; - private final SEBClientInstructionService sebInstructionService; private final ExamAdminService examAdminService; private final ExamSessionService examSessionService; public ExamProctoringRoomServiceImpl( final RemoteProctoringRoomDAO remoteProctoringRoomDAO, final ClientConnectionDAO clientConnectionDAO, - final SEBClientInstructionService sebInstructionService, final ExamAdminService examAdminService, final ExamSessionService examSessionService) { this.remoteProctoringRoomDAO = remoteProctoringRoomDAO; this.clientConnectionDAO = clientConnectionDAO; - this.sebInstructionService = sebInstructionService; this.examAdminService = examAdminService; this.examSessionService = examSessionService; } @@ -84,7 +74,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService this.clientConnectionDAO.getAllConnectionIdsForRoomUpdateActive() .getOrThrow() .stream() - .forEach(this::assignToRoom); + .forEach(this::assignToCollectingRoom); this.clientConnectionDAO.getAllConnectionIdsForRoomUpdateInactive() .getOrThrow() @@ -114,40 +104,40 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService this.remoteProctoringRoomDAO.deleteTownhallRoom(examId); } - private void assignToRoom(final ClientConnectionRecord cc) { + private void assignToCollectingRoom(final ClientConnectionRecord cc) { try { + final RemoteProctoringRoom proctoringRoom = getProctoringRoom( cc.getExamId(), cc.getConnectionToken()); - if (proctoringRoom != null) { - this.clientConnectionDAO.assignToProctoringRoom( - cc.getId(), - cc.getConnectionToken(), - proctoringRoom.id) - .onError(error -> log.error("Failed to assign to proctoring room: ", error)) - .getOrThrow(); + this.clientConnectionDAO.assignToProctoringRoom( + cc.getId(), + cc.getConnectionToken(), + proctoringRoom.id) + .getOrThrow(); - final Result townhallRoomResult = this.remoteProctoringRoomDAO - .getTownhallRoom(cc.getExamId()); - if (townhallRoomResult.hasValue()) { - final RemoteProctoringRoom townhallRoom = townhallRoomResult.get(); - applyProcotringInstruction( - cc.getExamId(), - cc.getConnectionToken(), - townhallRoom.name, - townhallRoom.subject); - } else { - applyProcotringInstruction( - cc.getExamId(), - cc.getConnectionToken(), - proctoringRoom.name, - proctoringRoom.subject); - } + final Result townhallRoomResult = this.remoteProctoringRoomDAO + .getTownhallRoom(cc.getExamId()); + + if (townhallRoomResult.hasValue()) { + final RemoteProctoringRoom townhallRoom = townhallRoomResult.get(); + applyProcotringInstruction( + cc.getExamId(), + cc.getConnectionToken(), + townhallRoom.name, + townhallRoom.subject) + .getOrThrow(); + } else { + applyProcotringInstruction( + cc.getExamId(), + cc.getConnectionToken(), + proctoringRoom.name, + proctoringRoom.subject) + .getOrThrow(); } } catch (final Exception e) { - log.error("Failed to update client connection for proctoring room: ", e); - this.clientConnectionDAO.setNeedsRoomUpdate(cc.getId()); + log.error("Failed to assign connection to collecting room: {}", cc, e); } } @@ -167,7 +157,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService private RemoteProctoringRoom getProctoringRoom(final Long examId, final String connectionToken) { try { final ProctoringSettings proctoringSettings = this.examAdminService - .getExamProctoring(examId) + .getExamProctoringSettings(examId) .getOrThrow(); return this.remoteProctoringRoomDAO.reservePlaceInCollectingRoom( examId, @@ -184,63 +174,21 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService } } - private void applyProcotringInstruction( + private Result applyProcotringInstruction( final Long examId, final String connectionToken, final String roomName, final String subject) { - try { - // apply a SEB_PROCOTIRNG instruction for the specified SEB client connection - final ProctoringSettings proctoringSettings = this.examAdminService - .getExamProctoring(examId) - .getOrThrow(); + return this.examAdminService + .getExamProctoringSettings(examId) + .flatMap(proctoringSettings -> this.examAdminService + .getExamProctoringService(proctoringSettings.serverType) + .getOrThrow() + .sendJoinCollectingRoomToClients( + proctoringSettings, + Arrays.asList(connectionToken))); - final SEBProctoringConnection proctoringData = - this.examAdminService.getExamProctoringService(proctoringSettings.serverType) - .flatMap(s -> s.getClientExamCollectingRoomConnection( - proctoringSettings, - connectionToken, - roomName, - subject)) - .getOrThrow(); - - final Map attributes = new HashMap<>(); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE, - ProctoringServerType.JITSI_MEET.name()); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD, - ProctoringInstructionMethod.JOIN.name()); - - if (proctoringSettings.serverType == ProctoringServerType.JITSI_MEET) { - - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM, - proctoringData.roomName); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM_SUBJECT, - proctoringData.subject); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL, - proctoringData.serverURL); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN, - proctoringData.accessToken); - } - - this.sebInstructionService.registerInstruction( - examId, - InstructionType.SEB_PROCTORING, - attributes, - connectionToken, - true); - - } catch (final Exception e) { - log.error( - "Failed to process proctoring initialization for established SEB client connection: {}", - connectionToken, e); - } } } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java index f940f8c6..248c8bee 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamSessionServiceImpl.java @@ -379,6 +379,12 @@ public class ExamSessionServiceImpl implements ExamSessionService { } } + @Override + public Result> getActiveConnectionTokens(final Long examId) { + return this.clientConnectionDAO + .getActiveConnctionTokens(examId); + } + @Override public Result updateExamCache(final Long examId) { final Exam exam = this.examSessionCacheService.getRunningExam(examId); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java index ff1a9994..9b3f0178 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamAdministrationController.java @@ -396,7 +396,7 @@ public class ExamAdministrationController extends EntityController { checkReadPrivilege(institutionId); return this.entityDAO.byPK(modelId) .flatMap(this.authorization::checkRead) - .flatMap(this.examAdminService::getExamProctoring) + .flatMap(this.examAdminService::getExamProctoringSettings) .getOrThrow(); } @@ -417,7 +417,7 @@ public class ExamAdministrationController extends EntityController { return this.entityDAO.byPK(examId) .flatMap(this.authorization::checkModify) .map(exam -> { - this.examAdminService.saveExamProctoring(examId, examProctoring); + this.examAdminService.saveExamProctoringSettings(examId, examProctoring); return exam; }) .flatMap(this.userActivityLogDAO::logModify) diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java index 47caaa92..774919af 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/weblayer/api/ExamProctoringController.java @@ -9,8 +9,6 @@ package ch.ethz.seb.sebserver.webservice.weblayer.api; import java.util.Arrays; -import java.util.Base64; -import java.util.Base64.Encoder; import java.util.Collection; import java.util.HashMap; import java.util.Map; @@ -39,8 +37,6 @@ import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction; import ch.ethz.seb.sebserver.gbl.model.session.ClientInstruction.InstructionType; import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; -import ch.ethz.seb.sebserver.gbl.util.Result; -import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService; import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.UserService; import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamAdminService; @@ -112,7 +108,7 @@ public class ExamProctoringController { consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) - public SEBProctoringConnection getProctorRoomData( + public SEBProctoringConnection getProctorRoomConnection( @RequestParam( name = API.PARAM_INSTITUTION_ID, required = true, @@ -125,13 +121,11 @@ public class ExamProctoringController { return this.examSessionService.getRunningExam(examId) .flatMap(this.authorization::checkRead) - .flatMap(this.examAdminService::getExamProctoring) - .flatMap(proc -> this.examAdminService - .getExamProctoringService(proc.serverType) - .flatMap(s -> s.createProctorPublicRoomConnection( - proc, - roomName, - StringUtils.isNoneBlank(subject) ? subject : roomName))) + .flatMap(this.examAdminService::getExamProctoringService) + .flatMap(service -> service.getProctorRoomConnection( + this.examAdminService.getExamProctoringSettings(examId).getOrThrow(), + roomName, + StringUtils.isNoneBlank(subject) ? subject : roomName)) .getOrThrow(); } @@ -223,10 +217,17 @@ public class ExamProctoringController { final ProctoringSettings proctoringSettings = this.examSessionService .getRunningExam(examId) - .flatMap(this.examAdminService::getExamProctoring) + .flatMap(this.examAdminService::getExamProctoringSettings) .getOrThrow(); - sendJoinInstructions(connectionTokens, proctoringSettings); + this.examAdminService + .getExamProctoringService(proctoringSettings.serverType) + .flatMap(service -> service.sendJoinCollectingRoomToClients( + proctoringSettings, + Arrays.asList(StringUtils.split( + connectionTokens, + Constants.LIST_SEPARATOR_CHAR)))) + .onError(error -> log.error("Failed to send rejoin collecting room to: {}", connectionTokens, error)); } @RequestMapping( @@ -254,55 +255,19 @@ public class ExamProctoringController { final ProctoringSettings settings = this.examSessionService .getRunningExam(examId) - .flatMap(this.examAdminService::getExamProctoring) + .flatMap(this.examAdminService::getExamProctoringSettings) .getOrThrow(); - final ExamProctoringService examProctoringService = this.examAdminService + return this.examAdminService .getExamProctoringService(settings.serverType) + .flatMap(service -> service.sendJoinRoomToClients( + settings, + Arrays.asList(StringUtils.split( + connectionTokens, + Constants.LIST_SEPARATOR_CHAR)), + roomName, + subject)) .getOrThrow(); - - if (StringUtils.isNotBlank(connectionTokens)) { - - Arrays.asList(connectionTokens.split(Constants.LIST_SEPARATOR)) - .stream() - .forEach(connectionToken -> { - final SEBProctoringConnection proctoringConnection = - examProctoringService - .getClientRoomConnection( - settings, - connectionToken, - verifyRoomName(roomName, connectionToken), - (StringUtils.isNotBlank(subject)) ? subject : roomName) - .onError(error -> log.error( - "Failed to get client room connection data for {} cause: {}", - connectionToken, - error.getMessage())) - .get(); - if (proctoringConnection != null) { - sendJoinInstruction(settings.examId, connectionToken, proctoringConnection) - .onError(error -> log.error( - "Failed to send proctoring leave instruction to client: {} cause: {}", - connectionToken, - error.getMessage())); - } - }); - } - - return examProctoringService.createProctorPublicRoomConnection( - settings, - roomName, - (StringUtils.isNotBlank(subject)) ? subject : roomName) - .getOrThrow(); - } - - private String verifyRoomName(final String requestedRoomName, final String connectionToken) { - if (StringUtils.isNotBlank(requestedRoomName)) { - return requestedRoomName; - } - - final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding(); - return urlEncoder.encodeToString( - Utils.toByteArray(connectionToken)); } @RequestMapping( @@ -342,48 +307,27 @@ public class ExamProctoringController { final ProctoringSettings settings = this.examSessionService .getRunningExam(examId) - .flatMap(this.examAdminService::getExamProctoring) + .flatMap(this.examAdminService::getExamProctoringSettings) .getOrThrow(); - final ExamProctoringService examProctoringService = this.examAdminService + // First create and get the town-hall room for specified exam + final RemoteProctoringRoom townhallRoom = this.examProcotringRoomService + .createTownhallRoom(examId, subject) + .onError(error -> { + log.error("Failed to create town-hall room: ", error); + this.examProcotringRoomService.disposeTownhallRoom(examId); + }) + .getOrThrow(); + + // Then send a join instruction to all active clients of the exam to join the town-hall + return this.examAdminService .getExamProctoringService(settings.serverType) - .getOrThrow(); - - // first create and register a room to collect all connection of the exam - // As long as the room exists new connections will join this room immediately - // after have been applied to the default collecting room - final RemoteProctoringRoom townhallRoom = this.examProcotringRoomService.createTownhallRoom(examId, subject) - .onError(error -> this.examProcotringRoomService.disposeTownhallRoom(examId)) - .getOrThrow(); - - // get all active connections for the exam and send the join instruction - this.examSessionService.getAllActiveConnectionData(examId) - .getOrThrow() - .stream() - .forEach(cc -> { - final SEBProctoringConnection data = examProctoringService - .getClientRoomConnection( - settings, - cc.clientConnection.connectionToken, - townhallRoom.name, - townhallRoom.subject) - .onError(error -> log.error( - "Failed to get client room connection data for {} cause: {}", - cc.clientConnection.connectionToken, - error.getMessage())) - .get(); - if (data != null) { - sendJoinInstruction(examId, cc.clientConnection.connectionToken, data) - .onError(error -> log.error( - "Failed to send proctoring leave instruction to client: {} ", - cc.clientConnection.connectionToken, error)); - } - }); - - return examProctoringService.createProctorPublicRoomConnection( - settings, - townhallRoom.name, - townhallRoom.subject) + .flatMap(service -> service.sendJoinRoomToClients( + settings, + this.examSessionService.getActiveConnectionTokens(examId) + .getOrThrow(), + townhallRoom.name, + townhallRoom.subject)) .getOrThrow(); } @@ -403,7 +347,7 @@ public class ExamProctoringController { final ProctoringSettings settings = this.examSessionService .getRunningExam(examId) - .flatMap(this.examAdminService::getExamProctoring) + .flatMap(this.examAdminService::getExamProctoringSettings) .getOrThrow(); final ExamProctoringService examProctoringService = this.examAdminService @@ -413,25 +357,11 @@ public class ExamProctoringController { // first unregister the current room to collect all connection of the exam this.examProcotringRoomService.disposeTownhallRoom(examId); - // get all active connections for the exam and send the join instruction - this.examSessionService.getConnectionData( - examId, - ExamSessionService.ACTIVE_CONNECTION_DATA_FILTER) - .getOrThrow() - .stream() - .forEach(cc -> { - examProctoringService - .getClientExamCollectingRoomConnection( - settings, - cc.clientConnection) - .flatMap(data -> this.sendJoinInstruction( - examId, - cc.clientConnection.connectionToken, - data)) - .onError(error -> log.error("Failed to send rejoin for: {} cause: {}", - cc.clientConnection.connectionToken, - error.getMessage())); - }); + // then get all active connections for the exam and send the rejoin to collecting room instruction + examProctoringService.sendJoinCollectingRoomToClients( + settings, + this.examSessionService.getActiveConnectionTokens(examId) + .getOrThrow()); } private void sendBroadcastInstructions( @@ -486,19 +416,19 @@ public class ExamProctoringController { private void sendBroadcastInstructionToClientsInExam(final Long examId, final Map attributes) { this.examSessionService - .getAllActiveConnectionData(examId) + .getActiveConnectionTokens(examId) .getOrThrow() .stream() - .forEach(connection -> { + .forEach(connectionToken -> { this.sebInstructionService.registerInstruction( examId, InstructionType.SEB_RECONFIGURE_SETTINGS, attributes, - connection.clientConnection.connectionToken, + connectionToken, true) .onError(error -> log.error( "Failed to register reconfiguring instruction for connection: {}", - connection.clientConnection.connectionToken, + connectionToken, error)); }); } @@ -527,70 +457,6 @@ public class ExamProctoringController { }); } - private void sendJoinInstructions( - final String connectionTokens, - final ProctoringSettings proctoringSettings) { - - final ExamProctoringService examProctoringService = this.examAdminService - .getExamProctoringService(proctoringSettings.serverType) - .getOrThrow(); - - Arrays.asList(StringUtils.split(connectionTokens, Constants.LIST_SEPARATOR)) - .stream() - .forEach(connectionToken -> { - sendJoinInstructionToClient(proctoringSettings, examProctoringService, connectionToken); - }); - } - - private void sendJoinInstructionToClient( - final ProctoringSettings proctoringSettings, - final ExamProctoringService examProctoringService, - final String connectionToken) { - - this.examSessionService - .getConnectionData(connectionToken) - .flatMap(connection -> examProctoringService.getClientExamCollectingRoomConnection( - proctoringSettings, - connection.clientConnection)) - .flatMap(data -> this.sendJoinInstruction( - proctoringSettings.examId, - connectionToken, data)) - .onError(error -> log.error("Failed to send rejoin for: {} cause: {}", - connectionToken, - error.getMessage())); - } - - private Result sendJoinInstruction( - final Long examId, - final String connectionToken, - final SEBProctoringConnection data) { - - final Map attributes = new HashMap<>(); - - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE, - ProctoringSettings.ProctoringServerType.JITSI_MEET.name()); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD, - ClientInstruction.ProctoringInstructionMethod.JOIN.name()); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_URL, - data.serverURL); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_ROOM, - data.roomName); - attributes.put( - ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.JITSI_TOKEN, - data.accessToken); - - return this.sebInstructionService.registerInstruction( - examId, - InstructionType.SEB_PROCTORING, - attributes, - connectionToken, - true); - } - private void checkAccess(final Long institutionId, final Long examId) { this.authorization.check( PrivilegeType.READ, diff --git a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringServiceTest.java b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringServiceTest.java index 4b138e37..1f72b323 100644 --- a/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringServiceTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringServiceTest.java @@ -28,7 +28,7 @@ public class ExamJITSIProctoringServiceTest { final Cryptor cryptorMock = Mockito.mock(Cryptor.class); Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123"); final ExamJITSIProctoringService examJITSIProctoringService = - new ExamJITSIProctoringService(null, null, null, cryptorMock); + new ExamJITSIProctoringService(null, null, null, null, cryptorMock); String accessToken = examJITSIProctoringService.createPayload( "test-app", @@ -62,7 +62,7 @@ public class ExamJITSIProctoringServiceTest { final Cryptor cryptorMock = Mockito.mock(Cryptor.class); Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123"); final ExamJITSIProctoringService examJITSIProctoringService = - new ExamJITSIProctoringService(null, null, null, cryptorMock); + new ExamJITSIProctoringService(null, null, null, null, cryptorMock); final SEBProctoringConnection data = examJITSIProctoringService.createProctoringConnection( ProctoringServerType.JITSI_MEET, "connectionToken",