zoom integration

This commit is contained in:
anhefti 2021-03-16 17:34:23 +01:00
parent 564b7ec9c9
commit 43383bcebb
13 changed files with 574 additions and 281 deletions

View file

@ -53,6 +53,7 @@ public final class Constants {
public static final Character SLASH = '/'; public static final Character SLASH = '/';
public static final Character BACKSLASH = '\\'; public static final Character BACKSLASH = '\\';
public static final Character QUOTE = '\''; public static final Character QUOTE = '\'';
public static final Character QUERY = '?';
public static final Character DOUBLE_QUOTE = '"'; public static final Character DOUBLE_QUOTE = '"';
public static final Character COMMA = ','; public static final Character COMMA = ',';
public static final Character PIPE = '|'; public static final Character PIPE = '|';

View file

@ -24,6 +24,7 @@ public class ProctoringRoomConnection {
public static final String ATTR_SUBJECT = "subject"; public static final String ATTR_SUBJECT = "subject";
public static final String ATTR_ACCESS_TOKEN = "accessToken"; public static final String ATTR_ACCESS_TOKEN = "accessToken";
public static final String ATTR_CONNECTION_URL = "connectionURL"; public static final String ATTR_CONNECTION_URL = "connectionURL";
public static final String ATTR_USER_NAME = "userName";
@JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE) @JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE)
public final ProctoringServerType proctoringServerType; public final ProctoringServerType proctoringServerType;
@ -46,6 +47,9 @@ public class ProctoringRoomConnection {
@JsonProperty(ATTR_ACCESS_TOKEN) @JsonProperty(ATTR_ACCESS_TOKEN)
public final String accessToken; public final String accessToken;
@JsonProperty(ATTR_USER_NAME)
public final String userName;
@JsonCreator @JsonCreator
public ProctoringRoomConnection( public ProctoringRoomConnection(
@JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE) final ProctoringServerType proctoringServerType, @JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE) final ProctoringServerType proctoringServerType,
@ -54,7 +58,8 @@ public class ProctoringRoomConnection {
@JsonProperty(ATTR_SERVER_URL) final String serverURL, @JsonProperty(ATTR_SERVER_URL) final String serverURL,
@JsonProperty(ATTR_ROOM_NAME) final String roomName, @JsonProperty(ATTR_ROOM_NAME) final String roomName,
@JsonProperty(ATTR_SUBJECT) final String subject, @JsonProperty(ATTR_SUBJECT) final String subject,
@JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken) { @JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken,
@JsonProperty(ATTR_USER_NAME) final String userName) {
this.proctoringServerType = proctoringServerType; this.proctoringServerType = proctoringServerType;
this.connectionToken = connectionToken; this.connectionToken = connectionToken;
@ -63,6 +68,7 @@ public class ProctoringRoomConnection {
this.roomName = roomName; this.roomName = roomName;
this.subject = subject; this.subject = subject;
this.accessToken = accessToken; this.accessToken = accessToken;
this.userName = userName;
} }
public ProctoringServerType getProctoringServerType() { public ProctoringServerType getProctoringServerType() {

View file

@ -51,6 +51,7 @@ public final class ClientInstruction {
public static final String ZOOM_URL = "zoomMeetServerURL"; public static final String ZOOM_URL = "zoomMeetServerURL";
public static final String ZOOM_ROOM = "zoomMeetRoom"; public static final String ZOOM_ROOM = "zoomMeetRoom";
public static final String ZOOM_ROOM_SUBJECT = "zoomMeetSubject"; public static final String ZOOM_ROOM_SUBJECT = "zoomMeetSubject";
public static final String ZOOM_USER_NAME = "zoomUserName";
public static final String ZOOM_TOKEN = "zoomMeetToken"; public static final String ZOOM_TOKEN = "zoomMeetToken";
public static final String ZOOM_RECEIVE_AUDIO = "zoomMeetReceiveAudio"; public static final String ZOOM_RECEIVE_AUDIO = "zoomMeetReceiveAudio";
public static final String ZOOM_RECEIVE_VIDEO = "zoomMeetReceiveVideo"; public static final String ZOOM_RECEIVE_VIDEO = "zoomMeetReceiveVideo";

View file

@ -686,5 +686,4 @@ public final class Utils {
public static String valueOrEmptyNote(final String value) { public static String valueOrEmptyNote(final String value) {
return StringUtils.isBlank(value) ? Constants.EMPTY_NOTE : value; return StringUtils.isBlank(value) ? Constants.EMPTY_NOTE : value;
} }
} }

View file

@ -20,6 +20,8 @@ public interface RemoteProctoringRoomDAO {
Result<Collection<RemoteProctoringRoom>> getCollectingRoomsForExam(Long examId); Result<Collection<RemoteProctoringRoom>> getCollectingRoomsForExam(Long examId);
//Result<Collection<RemoteProctoringRoom>> getRoomsOfExam(Long examId);
Result<RemoteProctoringRoom> getRoom(Long roomId); Result<RemoteProctoringRoom> getRoom(Long roomId);
Result<RemoteProctoringRoom> getRoom(Long examId, String roomName); Result<RemoteProctoringRoom> getRoom(Long examId, String roomName);

View file

@ -44,51 +44,20 @@ public interface ExamProctoringService {
String roomName, String roomName,
String subject); String subject);
Result<ProctoringRoomConnection> getClientBreakOutRoomConnection( Result<ProctoringRoomConnection> getClientRoomConnection(
final ProctoringServiceSettings proctoringSettings, final ProctoringServiceSettings proctoringSettings,
final String connectionToken, final String connectionToken,
final String roomName, final String roomName,
final String subject); final String subject);
Result<ProctoringRoomConnection> getClientCollectingRoomConnection( // Result<ProctoringRoomConnection> getClientCollectingRoomConnection(
final ProctoringServiceSettings proctoringSettings, // final ProctoringServiceSettings proctoringSettings,
final String connectionToken, // final String connectionToken,
final String roomName, // final String roomName,
final String subject); // final String subject);
Map<String, String> createJoinInstructionAttributes(final ProctoringRoomConnection proctoringConnection); Map<String, String> createJoinInstructionAttributes(
final ProctoringRoomConnection proctoringConnection);
// /** This instructs all sepcified SEB clients to join a defined room by creating a individual room access token
// * and join instruction for each client and put this instruction to the clients instruction queue.
// *
// * @param proctoringSettings The proctoring service settings
// * @param clientConnectionTokens A collection of SEB connection tokens. Only active SEB clients will get the
// * instructions
// * @param roomName The name of the room to join
// * @param subject the subject of the room to join
// * @return Result refer to the room connection data for the proctor to join to room too or to an error when
// * happened */
// Result<ProctoringRoomConnection> sendJoinRoomToClients(
// ProctoringServiceSettings proctoringSettings,
// Collection<String> clientConnectionTokens,
// String roomName,
// String subject);
// /** Sends instructions to join or rejoin the individual assigned collecting rooms of each involved SEB client.
// * Creates an individual join instruction for each involved client and put that to the clients instruction queue.
// *
// * INFO:
// * A collecting room is assigned to each SEB client connection while connecting to the SEB server and
// * each SEB client that has successfully connected to the SEB Server and is participating in an exam
// * with proctoring enabled, is assigned to a collecting room.
// *
// * @param proctoringSettings he proctoring service settings
// * @param clientConnectionTokens A collection of SEB connection tokens. Only active SEB clients will get the
// * instructions
// * @return Empty Result that refers to an error when happened */
// Result<Void> sendJoinCollectingRoomToClients(
// ProctoringServiceSettings proctoringSettings,
// Collection<String> clientConnectionTokens);
Result<Void> disposeServiceRoomsForExam(Exam exam); Result<Void> disposeServiceRoomsForExam(Exam exam);
@ -100,11 +69,11 @@ public interface ExamProctoringService {
throw new RuntimeException("Test Why: " + connectionToken); throw new RuntimeException("Test Why: " + connectionToken);
} }
Result<NewRoom> newCollectingRoom(Long roomNumber); Result<NewRoom> newCollectingRoom(ProctoringServiceSettings proctoringSettings, Long roomNumber);
Result<NewRoom> newBreakOutRoom(String subject); Result<NewRoom> newBreakOutRoom(ProctoringServiceSettings proctoringSettings, String subject);
Result<Void> disposeBreakOutRoom(String roomName); Result<Void> disposeBreakOutRoom(ProctoringServiceSettings proctoringSettings, String roomName);
Map<String, String> getDefaultInstructionAttributes(); Map<String, String> getDefaultInstructionAttributes();

View file

@ -149,7 +149,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
// First create and get the town-hall room for specified exam // First create and get the town-hall room for specified exam
final RemoteProctoringRoom townhallRoom = examProctoringService final RemoteProctoringRoom townhallRoom = examProctoringService
.newBreakOutRoom(subject) .newBreakOutRoom(settings, subject)
.flatMap(room -> this.remoteProctoringRoomDAO.createTownhallRoom(examId, room)) .flatMap(room -> this.remoteProctoringRoomDAO.createTownhallRoom(examId, room))
.getOrThrow(); .getOrThrow();
@ -187,7 +187,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.getOrThrow(); .getOrThrow();
final RemoteProctoringRoom breakOutRoom = examProctoringService final RemoteProctoringRoom breakOutRoom = examProctoringService
.newBreakOutRoom(subject) .newBreakOutRoom(settings, subject)
.flatMap(room -> this.remoteProctoringRoomDAO.createBreakOutRoom(examId, room, connectionTokens)) .flatMap(room -> this.remoteProctoringRoomDAO.createBreakOutRoom(examId, room, connectionTokens))
.getOrThrow(); .getOrThrow();
@ -302,7 +302,9 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
return this.remoteProctoringRoomDAO.reservePlaceInCollectingRoom( return this.remoteProctoringRoomDAO.reservePlaceInCollectingRoom(
examId, examId,
proctoringSettings.collectingRoomSize, proctoringSettings.collectingRoomSize,
examProctoringService::newCollectingRoom) roomNumber -> examProctoringService.newCollectingRoom(
proctoringSettings,
roomNumber))
.getOrThrow(); .getOrThrow();
} catch (final Exception e) { } catch (final Exception e) {
@ -334,7 +336,9 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
this.remoteProctoringRoomDAO this.remoteProctoringRoomDAO
.getTownhallRoom(examId) .getTownhallRoom(examId)
.map(RemoteProctoringRoom::getName) .map(RemoteProctoringRoom::getName)
.flatMap(examProctoringService::disposeBreakOutRoom) .flatMap(roomName -> examProctoringService.disposeBreakOutRoom(
proctoringSettings,
roomName))
.flatMap(service -> this.remoteProctoringRoomDAO.deleteTownhallRoom(examId)) .flatMap(service -> this.remoteProctoringRoomDAO.deleteTownhallRoom(examId))
.getOrThrow(); .getOrThrow();
@ -382,7 +386,8 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.getOrThrow(); .getOrThrow();
// Dispose the proctoring room on service side // Dispose the proctoring room on service side
examProctoringService.disposeBreakOutRoom(remoteProctoringRoom.name) examProctoringService
.disposeBreakOutRoom(proctoringSettings, remoteProctoringRoom.name)
.getOrThrow(); .getOrThrow();
// Send join collecting rooms to involving clients // Send join collecting rooms to involving clients
@ -448,12 +453,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
connectionTokens connectionTokens
.stream() .stream()
.forEach(connectionToken -> { .forEach(connectionToken -> {
this.sebInstructionService.registerInstruction( this.sebInstructionService
examId, .registerInstruction(
InstructionType.SEB_RECONFIGURE_SETTINGS, examId,
attributes, InstructionType.SEB_RECONFIGURE_SETTINGS,
connectionToken, attributes,
true) connectionToken,
true)
.onError(error -> log.error( .onError(error -> log.error(
"Failed to register reconfiguring instruction for connection: {}", "Failed to register reconfiguring instruction for connection: {}",
connectionToken, connectionToken,
@ -496,17 +502,17 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
clientConnectionTokens clientConnectionTokens
.stream() .stream()
.forEach(connectionToken -> { .forEach(connectionToken -> {
final ProctoringRoomConnection proctoringConnection = final ProctoringRoomConnection proctoringConnection = examProctoringService
examProctoringService.getClientBreakOutRoomConnection( .getClientRoomConnection(
proctoringSettings, proctoringSettings,
connectionToken, connectionToken,
examProctoringService.verifyRoomName(roomName, connectionToken), examProctoringService.verifyRoomName(roomName, connectionToken),
(StringUtils.isNotBlank(subject)) ? subject : roomName) (StringUtils.isNotBlank(subject)) ? subject : roomName)
.onError(error -> log.error( .onError(error -> log.error(
"Failed to get client room connection data for {} cause: {}", "Failed to get client room connection data for {} cause: {}",
connectionToken, connectionToken,
error.getMessage())) error.getMessage()))
.get(); .get();
if (proctoringConnection != null) { if (proctoringConnection != null) {
sendJoinInstruction( sendJoinInstruction(
proctoringSettings.examId, proctoringSettings.examId,
@ -551,7 +557,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.getOrThrow(); .getOrThrow();
final ProctoringRoomConnection proctoringConnection = examProctoringService final ProctoringRoomConnection proctoringConnection = examProctoringService
.getClientCollectingRoomConnection( .getClientRoomConnection(
proctoringSettings, proctoringSettings,
clientConnection.clientConnection.connectionToken, clientConnection.clientConnection.connectionToken,
roomName, roomName,
@ -577,12 +583,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
final Map<String, String> attributes = examProctoringService final Map<String, String> attributes = examProctoringService
.createJoinInstructionAttributes(proctoringConnection); .createJoinInstructionAttributes(proctoringConnection);
this.sebInstructionService.registerInstruction( this.sebInstructionService
examId, .registerInstruction(
InstructionType.SEB_PROCTORING, examId,
attributes, InstructionType.SEB_PROCTORING,
connectionToken, attributes,
true) connectionToken,
true)
.onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error)); .onError(error -> log.error("Failed to send join instruction: {}", connectionToken, error));
} }
} }

View file

@ -170,21 +170,30 @@ public class JitsiProctoringService implements ExamProctoringService {
} }
@Override @Override
public Result<NewRoom> newCollectingRoom(final Long roomNumber) { public Result<NewRoom> newCollectingRoom(
final ProctoringServiceSettings proctoringSettings,
final Long roomNumber) {
return Result.of(new NewRoom( return Result.of(new NewRoom(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
"Room " + (roomNumber + 1))); "Room " + (roomNumber + 1)));
} }
@Override @Override
public Result<NewRoom> newBreakOutRoom(final String subject) { public Result<NewRoom> newBreakOutRoom(
final ProctoringServiceSettings proctoringSettings,
final String subject) {
return Result.of(new NewRoom( return Result.of(new NewRoom(
UUID.randomUUID().toString(), UUID.randomUUID().toString(),
subject)); subject));
} }
@Override @Override
public Result<Void> disposeBreakOutRoom(final String roomName) { public Result<Void> disposeBreakOutRoom(
final ProctoringServiceSettings proctoringSettings,
final String roomName) {
return Result.EMPTY; return Result.EMPTY;
} }
@ -237,7 +246,6 @@ public class JitsiProctoringService implements ExamProctoringService {
return Result.tryCatch(() -> { return Result.tryCatch(() -> {
return createProctoringConnection( return createProctoringConnection(
proctoringSettings.serverType,
null, null,
proctoringSettings.serverURL, proctoringSettings.serverURL,
proctoringSettings.appKey, proctoringSettings.appKey,
@ -253,7 +261,7 @@ public class JitsiProctoringService implements ExamProctoringService {
} }
@Override @Override
public Result<ProctoringRoomConnection> getClientCollectingRoomConnection( public Result<ProctoringRoomConnection> getClientRoomConnection(
final ProctoringServiceSettings proctoringSettings, final ProctoringServiceSettings proctoringSettings,
final String connectionToken, final String connectionToken,
final String roomName, final String roomName,
@ -265,7 +273,6 @@ public class JitsiProctoringService implements ExamProctoringService {
.getOrThrow(); .getOrThrow();
return createProctoringConnection( return createProctoringConnection(
proctoringSettings.serverType,
null, null,
proctoringSettings.serverURL, proctoringSettings.serverURL,
proctoringSettings.appKey, proctoringSettings.appKey,
@ -280,38 +287,7 @@ public class JitsiProctoringService implements ExamProctoringService {
}); });
} }
@Override
public Result<ProctoringRoomConnection> getClientBreakOutRoomConnection(
final ProctoringServiceSettings proctoringSettings,
final String connectionToken,
final String roomName,
final String subject) {
return Result.tryCatch(() -> {
final long expTime = forExam(proctoringSettings);
final ClientConnectionData connectionData = this.examSessionService
.getConnectionData(connectionToken)
.getOrThrow();
return createProctoringConnection(
proctoringSettings.serverType,
connectionToken,
proctoringSettings.serverURL,
proctoringSettings.appKey,
proctoringSettings.getAppSecret(),
connectionData.clientConnection.userSessionId,
SEB_CLIENT_KEY,
roomName,
subject,
expTime,
false)
.getOrThrow();
});
}
protected Result<ProctoringRoomConnection> createProctoringConnection( protected Result<ProctoringRoomConnection> createProctoringConnection(
final ProctoringServerType proctoringServerType,
final String connectionToken, final String connectionToken,
final String url, final String url,
final String appKey, final String appKey,
@ -341,13 +317,14 @@ public class JitsiProctoringService implements ExamProctoringService {
moderator); moderator);
return new ProctoringRoomConnection( return new ProctoringRoomConnection(
proctoringServerType, ProctoringServerType.JITSI_MEET,
connectionToken, connectionToken,
host, host,
url, url,
roomName, roomName,
subject, subject,
token); token,
clientName);
}); });
} }

View file

@ -8,16 +8,20 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring; package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring;
import java.net.URL;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Arrays; import java.util.Arrays;
import java.util.Base64; import java.util.Base64;
import java.util.Base64.Encoder; import java.util.Base64.Encoder;
import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.crypto.Mac; import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec; import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
@ -29,8 +33,11 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService;
import ch.ethz.seb.sebserver.gbl.Constants; import ch.ethz.seb.sebserver.gbl.Constants;
@ -41,20 +48,27 @@ import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException;
import ch.ethz.seb.sebserver.gbl.api.JSONMapper; import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService; import ch.ethz.seb.sebserver.gbl.async.AsyncService;
import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker; import ch.ethz.seb.sebserver.gbl.async.CircuitBreaker;
import ch.ethz.seb.sebserver.gbl.client.ClientCredentials;
import ch.ethz.seb.sebserver.gbl.model.exam.Exam; import ch.ethz.seb.sebserver.gbl.model.exam.Exam;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
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;
import ch.ethz.seb.sebserver.gbl.model.session.RemoteProctoringRoom;
import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile; import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor; import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result; import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Tuple; import ch.ethz.seb.sebserver.gbl.util.Tuple;
import ch.ethz.seb.sebserver.gbl.util.Utils; import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
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.ExamProctoringService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.CreateMeetingRequest;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.CreateUserRequest; import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.CreateUserRequest;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.UserPageResponse; import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.MeetingResponse;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.impl.proctoring.ZoomRoomRequestResponse.UserResponse;
@Lazy @Lazy
@Service @Service
@ -67,8 +81,10 @@ public class ZoomProctoringService implements ExamProctoringService {
private static final String ZOOM_ACCESS_TOKEN_HEADER = private static final String ZOOM_ACCESS_TOKEN_HEADER =
"{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; "{\"alg\":\"HS256\",\"typ\":\"JWT\"}";
private static final String ZOOM_ACCESS_TOKEN_PAYLOAD = private static final String ZOOM_API_ACCESS_TOKEN_PAYLOAD =
"{\"iss\":\"%s\",\"exp\":%s}"; "{\"iss\":\"%s\",\"exp\":%s}";
private static final String ZOOM_MEETING_ACCESS_TOKEN_PAYLOAD =
"{\"app_key\":\"%s\",\"iat\":%s,\"exp\":%s,\"tpc\":\"%s\",\"pwd\":\"%s\"}";
private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList( private static final Map<String, String> SEB_API_NAME_INSTRUCTION_NAME_MAPPING = Utils.immutableMapOf(Arrays.asList(
new Tuple<>( new Tuple<>(
@ -100,20 +116,26 @@ public class ZoomProctoringService implements ExamProctoringService {
private final AsyncService asyncService; private final AsyncService asyncService;
private final JSONMapper jsonMapper; private final JSONMapper jsonMapper;
private final ZoomRestTemplate zoomRestTemplate; private final ZoomRestTemplate zoomRestTemplate;
private final RemoteProctoringRoomDAO remoteProctoringRoomDAO;
private final AuthorizationService authorizationService;
public ZoomProctoringService( public ZoomProctoringService(
final ExamSessionService examSessionService, final ExamSessionService examSessionService,
final ClientHttpRequestFactoryService clientHttpRequestFactoryService, final ClientHttpRequestFactoryService clientHttpRequestFactoryService,
final Cryptor cryptor, final Cryptor cryptor,
final AsyncService asyncService, final AsyncService asyncService,
final JSONMapper jsonMapper) { final JSONMapper jsonMapper,
final RemoteProctoringRoomDAO remoteProctoringRoomDAO,
final AuthorizationService authorizationService) {
this.examSessionService = examSessionService; this.examSessionService = examSessionService;
this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; this.clientHttpRequestFactoryService = clientHttpRequestFactoryService;
this.cryptor = cryptor; this.cryptor = cryptor;
this.asyncService = asyncService; this.asyncService = asyncService;
this.jsonMapper = jsonMapper; this.jsonMapper = jsonMapper;
this.zoomRestTemplate = new ZoomRestTemplate(); this.zoomRestTemplate = new ZoomRestTemplate(this);
this.remoteProctoringRoomDAO = remoteProctoringRoomDAO;
this.authorizationService = authorizationService;
} }
@Override @Override
@ -132,25 +154,32 @@ public class ZoomProctoringService implements ExamProctoringService {
try { try {
final ClientCredentials credentials = new ClientCredentials(
proctoringSettings.appKey,
proctoringSettings.appSecret);
final ResponseEntity<String> result = this.zoomRestTemplate final ResponseEntity<String> result = this.zoomRestTemplate
.testServiceConnection(proctoringSettings); .testServiceConnection(
proctoringSettings.serverURL,
credentials);
if (result.getStatusCode() != HttpStatus.OK) { if (result.getStatusCode() != HttpStatus.OK) {
throw new APIMessageException( throw new APIMessageException(
APIMessage.ErrorMessage.BINDING_ERROR, APIMessage.ErrorMessage.BINDING_ERROR,
String.valueOf(result.getStatusCode())); String.valueOf(result.getStatusCode()));
} else {
final UserPageResponse response = this.jsonMapper.readValue(
result.getBody(),
UserPageResponse.class);
System.out.println(response);
final ResponseEntity<String> createUser = this.zoomRestTemplate
.createUser(proctoringSettings);
System.out.println(response);
} }
// else {
// final UserPageResponse response = this.jsonMapper.readValue(
// result.getBody(),
// UserPageResponse.class);
//
// System.out.println(response);
//
// final ResponseEntity<String> createUser = this.zoomRestTemplate
// .createUser(credentials, "TestRoom");
//
// System.out.println(response);
// }
} catch (final Exception e) { } catch (final Exception e) {
log.error("Failed to access Zoom service at: {}", proctoringSettings.serverURL, e); log.error("Failed to access Zoom service at: {}", proctoringSettings.serverURL, e);
throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR, e.getMessage()); throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR, e.getMessage());
@ -160,32 +189,34 @@ public class ZoomProctoringService implements ExamProctoringService {
}); });
} }
@Override
public Result<ProctoringRoomConnection> getClientBreakOutRoomConnection(
final ProctoringServiceSettings proctoringSettings,
final String connectionToken,
final String roomName,
final String subject) {
// TODO Auto-generated method stub
return null;
}
@Override
public Result<ProctoringRoomConnection> getClientCollectingRoomConnection(
final ProctoringServiceSettings proctoringSettings,
final String connectionToken,
final String roomName,
final String subject) {
// TODO Auto-generated method stub
return null;
}
@Override @Override
public Map<String, String> createJoinInstructionAttributes(final ProctoringRoomConnection proctoringConnection) { public Map<String, String> createJoinInstructionAttributes(final ProctoringRoomConnection proctoringConnection) {
// TODO Auto-generated method stub final Map<String, String> attributes = new HashMap<>();
return null; attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.SERVICE_TYPE,
ProctoringServiceSettings.ProctoringServerType.ZOOM.name());
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.METHOD,
ClientInstruction.ProctoringInstructionMethod.JOIN.name());
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_URL,
proctoringConnection.serverURL);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_ROOM,
proctoringConnection.roomName);
if (StringUtils.isNotBlank(proctoringConnection.subject)) {
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_ROOM_SUBJECT,
proctoringConnection.subject);
}
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_TOKEN,
proctoringConnection.accessToken);
attributes.put(
ClientInstruction.SEB_INSTRUCTION_ATTRIBUTES.SEB_PROCTORING.ZOOM_USER_NAME,
proctoringConnection.userName);
return attributes;
} }
@Override @Override
@ -194,33 +225,136 @@ public class ZoomProctoringService implements ExamProctoringService {
final String roomName, final String roomName,
final String subject) { final String subject) {
// TODO Auto-generated method stub return Result.tryCatch(() -> {
return null;
final RemoteProctoringRoom remoteProctoringRoom = this.remoteProctoringRoomDAO
.getRoom(proctoringSettings.examId, roomName)
.getOrThrow();
final AdditionalZoomRoomData additionalZoomRoomData = this.jsonMapper.readValue(
remoteProctoringRoom.additionalRoomData,
AdditionalZoomRoomData.class);
final ClientCredentials credentials = new ClientCredentials(
proctoringSettings.appKey,
this.cryptor.decrypt(proctoringSettings.appSecret),
this.cryptor.decrypt(remoteProctoringRoom.joinKey));
final String jwt = this.createJWTForMeetingAccess(credentials, subject);
return new ProctoringRoomConnection(
ProctoringServerType.ZOOM,
null,
proctoringSettings.serverURL,
additionalZoomRoomData.join_url,
roomName,
subject,
jwt,
this.authorizationService.getUserService().getCurrentUser().getUsername());
});
}
@Override
public Result<ProctoringRoomConnection> getClientRoomConnection(
final ProctoringServiceSettings proctoringSettings,
final String connectionToken,
final String roomName,
final String subject) {
return Result.tryCatch(() -> {
final RemoteProctoringRoom remoteProctoringRoom = this.remoteProctoringRoomDAO
.getRoom(proctoringSettings.examId, roomName)
.getOrThrow();
final AdditionalZoomRoomData additionalZoomRoomData = this.jsonMapper.readValue(
remoteProctoringRoom.additionalRoomData,
AdditionalZoomRoomData.class);
final ClientCredentials credentials = new ClientCredentials(
proctoringSettings.appKey,
this.cryptor.decrypt(proctoringSettings.appSecret),
this.cryptor.decrypt(remoteProctoringRoom.joinKey));
final String jwt = this.createJWTForMeetingAccess(credentials, subject);
final ClientConnectionData clientConnection = this.examSessionService
.getConnectionData(connectionToken)
.getOrThrow();
return new ProctoringRoomConnection(
ProctoringServerType.ZOOM,
connectionToken,
proctoringSettings.serverURL,
additionalZoomRoomData.join_url,
roomName,
subject,
jwt,
clientConnection.clientConnection.userSessionId);
});
} }
@Override @Override
public Result<Void> disposeServiceRoomsForExam(final Exam exam) { public Result<Void> disposeServiceRoomsForExam(final Exam exam) {
// TODO get all rooms of the exam
// close the rooms on Zoom service return Result.tryCatch(() -> {
return null; //this.remoteProctoringRoomDAO.getRoomsOfExam(exam.id);
});
// Get all rooms of the exam
} }
@Override @Override
public Result<NewRoom> newCollectingRoom(final Long roomNumber) { public Result<NewRoom> newCollectingRoom(
// TODO create new room on zoom side and use the id as room name final ProctoringServiceSettings proctoringSettings,
return null; final Long roomNumber) {
return createAdHocMeeting(
UUID.randomUUID().toString(),
"Proctoring Room " + (roomNumber + 1),
proctoringSettings);
} }
@Override @Override
public Result<NewRoom> newBreakOutRoom(final String subject) { public Result<NewRoom> newBreakOutRoom(
// TODO create new room on zoom side and use the id as room name final ProctoringServiceSettings proctoringSettings,
return null; final String subject) {
return createAdHocMeeting(
UUID.randomUUID().toString(),
subject,
proctoringSettings);
} }
@Override @Override
public Result<Void> disposeBreakOutRoom(final String roomName) { public Result<Void> disposeBreakOutRoom(
// TODO close the room with specified roomName on zoom side final ProctoringServiceSettings proctoringSettings,
return null; final String roomName) {
return Result.tryCatch(() -> {
try {
final RemoteProctoringRoom roomData = this.remoteProctoringRoomDAO
.getRoom(proctoringSettings.examId, roomName)
.getOrThrow();
AdditionalZoomRoomData additionalZoomRoomData;
additionalZoomRoomData = this.jsonMapper.readValue(
roomData.getAdditionalRoomData(),
AdditionalZoomRoomData.class);
this.deleteAdHocMeeting(
proctoringSettings,
roomName,
additionalZoomRoomData.user_id)
.getOrThrow();
} catch (final Exception e) {
throw new RuntimeException("Unexpected error while trying to dispose ad-hoc room for zoom proctoring");
}
});
} }
@Override @Override
@ -237,82 +371,298 @@ public class ZoomProctoringService implements ExamProctoringService {
.collect(Collectors.toMap(Tuple::get_1, Tuple::get_2)); .collect(Collectors.toMap(Tuple::get_1, Tuple::get_2));
} }
protected String createPayload( private Result<NewRoom> createAdHocMeeting(
final String clientKey, final String roomName,
final Long expTime) { final String subject,
final ProctoringServiceSettings proctoringSettings) {
return String.format( return Result.tryCatch(() -> {
ZOOM_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""), final ClientCredentials credentials = new ClientCredentials(
clientKey, proctoringSettings.appKey,
expTime); this.cryptor.decrypt(proctoringSettings.appSecret));
// First create a new user/host for the new room
final ResponseEntity<String> createUser = this.zoomRestTemplate.createUser(
proctoringSettings.serverURL,
credentials,
roomName);
final UserResponse userResponse = this.jsonMapper.readValue(
createUser.getBody(),
UserResponse.class);
// Then create new meeting with the ad-hoc user/host
final CharSequence meetingPwd = UUID.randomUUID().toString();
final ResponseEntity<String> createMeeting = this.zoomRestTemplate.createMeeting(
roomName,
credentials,
userResponse.id,
subject,
meetingPwd);
final MeetingResponse meetingResponse = this.jsonMapper.readValue(
createMeeting.getBody(),
MeetingResponse.class);
// TODO start the meeting automatically ???
// Create NewRoom data with all needed information to store persistent
final AdditionalZoomRoomData additionalZoomRoomData = new AdditionalZoomRoomData(
userResponse.id,
meetingResponse.start_url,
meetingResponse.join_url);
final String additionalZoomRoomDataString = this.jsonMapper
.writeValueAsString(additionalZoomRoomData);
return new NewRoom(
roomName,
subject,
this.cryptor.encrypt(meetingPwd),
additionalZoomRoomDataString);
});
} }
// private long forExam(final ProctoringServiceSettings examProctoring) { private Result<Void> deleteAdHocMeeting(
// if (examProctoring.examId == null) { final ProctoringServiceSettings proctoringSettings,
// throw new IllegalStateException("Missing exam identifier from ExamProctoring data"); final String meetingId,
// } final String userId) {
//
// long expTime = System.currentTimeMillis() + Constants.DAY_IN_MILLIS;
// if (this.examSessionService.isExamRunning(examProctoring.examId)) {
// final Exam exam = this.examSessionService.getRunningExam(examProctoring.examId)
// .getOrThrow();
// if (exam.endTime != null) {
// expTime = exam.endTime.getMillis();
// }
// }
// return expTime;
// }
private class ZoomRestTemplate { return Result.tryCatch(() -> {
final ClientCredentials credentials = new ClientCredentials(
proctoringSettings.appKey,
this.cryptor.decrypt(proctoringSettings.appSecret));
this.zoomRestTemplate.deleteMeeting(proctoringSettings.serverURL, credentials, meetingId);
this.zoomRestTemplate.deleteUser(proctoringSettings.serverURL, credentials, userId);
});
}
private String createJWTForAPIAccess(
final ClientCredentials credentials,
final Long expTime) {
try {
final CharSequence decryptedSecret = this.cryptor.decrypt(credentials.secret);
final StringBuilder builder = new StringBuilder();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String jwtHeaderPart = urlEncoder.encodeToString(
ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
final String jwtPayload = String.format(
ZOOM_API_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""),
credentials.clientIdAsString(),
expTime);
final String jwtPayloadPart = urlEncoder.encodeToString(
jwtPayload.getBytes(StandardCharsets.UTF_8));
final String message = jwtHeaderPart + "." + jwtPayloadPart;
final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG);
final SecretKeySpec secret_key = new SecretKeySpec(
Utils.toByteArray(decryptedSecret),
TOKEN_ENCODE_ALG);
sha256_HMAC.init(secret_key);
final String hash = urlEncoder.encodeToString(
sha256_HMAC.doFinal(Utils.toByteArray(message)));
builder.append(message)
.append(".")
.append(hash);
return builder.toString();
} catch (final Exception e) {
throw new RuntimeException("Failed to create JWT for Zoom API access: ", e);
}
}
private String createJWTForMeetingAccess(
final ClientCredentials credentials,
final String subject) {
try {
final long iat = Utils.getMillisecondsNow() / 1000;
final long exp = iat + 7200;
final CharSequence decryptedSecret = this.cryptor.decrypt(credentials.secret);
final StringBuilder builder = new StringBuilder();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String jwtHeaderPart = urlEncoder.encodeToString(
ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8));
final String jwtPayload = String.format(
ZOOM_API_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""),
credentials.clientIdAsString(),
iat,
exp,
subject,
credentials.accessTokenAsString());
final String jwtPayloadPart = urlEncoder.encodeToString(
jwtPayload.getBytes(StandardCharsets.UTF_8));
final String message = jwtHeaderPart + "." + jwtPayloadPart;
final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG);
final SecretKeySpec secret_key = new SecretKeySpec(
Utils.toByteArray(decryptedSecret),
TOKEN_ENCODE_ALG);
sha256_HMAC.init(secret_key);
final String hash = urlEncoder.encodeToString(
sha256_HMAC.doFinal(Utils.toByteArray(message)));
builder.append(message)
.append(".")
.append(hash);
return builder.toString();
} catch (final Exception e) {
throw new RuntimeException("Failed to create JWT for Zoom meeting access: ", e);
}
}
private final static class ZoomRestTemplate {
private static final String API_TEST_ENDPOINT = private static final String API_TEST_ENDPOINT =
"v2/users?status=active&page_size=30&page_number=1&data_type=Json"; "v2/users?status=active&page_size=30&page_number=1&data_type=Json";
private static final String API_CREATE_USER_ENDPOINT = "v2/users";
private static final String API_DELETE_USER_ENDPOINT = "v2/users/{userid}?action=delete";
private static final String API_USER_CUST_CREATE = "custCreate";
private static final String API_ZOOM_ROOM_USER = "ZoomRoomUser";
private static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings";
private static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}";
private final ZoomProctoringService zoomProctoringService;
private final RestTemplate restTemplate; private final RestTemplate restTemplate;
private final CircuitBreaker<ResponseEntity<String>> circuitBreaker; private final CircuitBreaker<ResponseEntity<String>> circuitBreaker;
public ZoomRestTemplate() { public ZoomRestTemplate(final ZoomProctoringService zoomProctoringService) {
this.restTemplate = new RestTemplate(ZoomProctoringService.this.clientHttpRequestFactoryService this.zoomProctoringService = zoomProctoringService;
this.restTemplate = new RestTemplate(zoomProctoringService.clientHttpRequestFactoryService
.getClientHttpRequestFactory() .getClientHttpRequestFactory()
.getOrThrow()); .getOrThrow());
this.circuitBreaker = ZoomProctoringService.this.asyncService.createCircuitBreaker( this.circuitBreaker = zoomProctoringService.asyncService.createCircuitBreaker(
2, 2,
10 * Constants.SECOND_IN_MILLIS, 10 * Constants.SECOND_IN_MILLIS,
10 * Constants.SECOND_IN_MILLIS); 10 * Constants.SECOND_IN_MILLIS);
} }
public ResponseEntity<String> testServiceConnection(final ProctoringServiceSettings proctoringSettings) { public ResponseEntity<String> testServiceConnection(
final String url = proctoringSettings.serverURL.endsWith(Constants.SLASH.toString()) final String zoomServerUrl,
? proctoringSettings.serverURL + API_TEST_ENDPOINT final ClientCredentials credentials) {
: proctoringSettings.serverURL + "/" + API_TEST_ENDPOINT;
return exchange(url, HttpMethod.GET, proctoringSettings); final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.path(API_TEST_ENDPOINT)
.toString();
return exchange(url, HttpMethod.GET, credentials);
} }
public ResponseEntity<String> createUser(final ProctoringServiceSettings proctoringSettings) public ResponseEntity<String> createUser(
throws JsonProcessingException { final String zoomServerUrl,
final String url = proctoringSettings.serverURL.endsWith(Constants.SLASH.toString()) final ClientCredentials credentials,
? proctoringSettings.serverURL + "v2/users" final String roomName) {
: proctoringSettings.serverURL + "/" + "v2/users";
final CreateUserRequest createUserRequest = new CreateUserRequest( try {
"custCreate", final String url = UriComponentsBuilder
new CreateUserRequest.UserInfo( .fromUriString(zoomServerUrl)
"andreas.hefti@let.ethz.ch", .path(API_CREATE_USER_ENDPOINT)
1, .toString();
"Andreas", final String host = new URL(zoomServerUrl).getHost();
"Hefti")); final CreateUserRequest createUserRequest = new CreateUserRequest(
final String body = ZoomProctoringService.this.jsonMapper.writeValueAsString(createUserRequest); API_USER_CUST_CREATE,
final HttpHeaders headers = getHeaders(proctoringSettings); new CreateUserRequest.UserInfo(
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); roomName + "@" + host,
return exchange(url, HttpMethod.POST, body, headers, null); 1,
roomName,
API_ZOOM_ROOM_USER));
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createUserRequest);
final HttpHeaders headers = getHeaders(credentials);
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return exchange(url, HttpMethod.POST, body, headers);
} catch (final Exception e) {
log.error("Failed to create Zoom ad-hoc user for room: {}", roomName, e);
throw new RuntimeException("Failed to create Zoom ad-hoc user", e);
}
} }
private HttpHeaders getHeaders(final ProctoringServiceSettings proctoringSettings) { public ResponseEntity<String> createMeeting(
final String jwt = createJWT( final String zoomServerUrl,
proctoringSettings.appKey, final ClientCredentials credentials,
proctoringSettings.appSecret, final String userId,
System.currentTimeMillis() + Constants.MINUTE_IN_MILLIS); final String topic,
final CharSequence password) {
try {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.path(API_CREATE_MEETING_ENDPOINT)
.buildAndExpand(userId)
.toString();
final CreateMeetingRequest createRoomRequest = new CreateMeetingRequest(topic, password);
final String body = this.zoomProctoringService.jsonMapper.writeValueAsString(createRoomRequest);
final HttpHeaders headers = getHeaders(credentials);
headers.set(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
return exchange(url, HttpMethod.POST, body, headers);
} catch (final Exception e) {
log.error("Failed to create Zoom ad-hoc meeting: {}", topic, e);
throw new RuntimeException("Failed to create Zoom ad-hoc meeting", e);
}
}
public ResponseEntity<String> deleteMeeting(
final String zoomServerUrl,
final ClientCredentials credentials,
final String meetingId) {
try {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.path(API_DELETE_MEETING_ENDPOINT)
.buildAndExpand(meetingId)
.toString();
return exchange(url, HttpMethod.DELETE, credentials);
} catch (final Exception e) {
log.error("Failed to delete Zoom ad-hoc meeting: {}", meetingId, e);
throw new RuntimeException("Failed to delete Zoom ad-hoc meeting", e);
}
}
public ResponseEntity<String> deleteUser(
final String zoomServerUrl,
final ClientCredentials credentials,
final String userId) {
try {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.path(API_DELETE_USER_ENDPOINT)
.buildAndExpand(userId)
.toString();
return exchange(url, HttpMethod.DELETE, credentials);
} catch (final Exception e) {
log.error("Failed to delete Zoom ad-hoc user with id: {}", userId, e);
throw new RuntimeException("Failed to delete Zoom ad-hoc user", e);
}
}
private HttpHeaders getHeaders(final ClientCredentials credentials) {
final String jwt = this.zoomProctoringService
.createJWTForAPIAccess(
credentials,
System.currentTimeMillis() + Constants.MINUTE_IN_MILLIS);
final HttpHeaders httpHeaders = new HttpHeaders(); final HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt); httpHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt);
@ -323,38 +673,31 @@ public class ZoomProctoringService implements ExamProctoringService {
private ResponseEntity<String> exchange( private ResponseEntity<String> exchange(
final String url, final String url,
final HttpMethod method, final HttpMethod method,
final ProctoringServiceSettings proctoringSettings) { final ClientCredentials credentials) {
return exchange(url, HttpMethod.GET, null, getHeaders(proctoringSettings), null); return exchange(url, HttpMethod.GET, null, getHeaders(credentials));
} }
private ResponseEntity<String> exchange( private ResponseEntity<String> exchange(
final String url, final String url,
final HttpMethod method, final HttpMethod method,
final Object body, final Object body,
final HttpHeaders httpHeaders, final HttpHeaders httpHeaders) {
final Map<String, ?> uriVariables) {
final Result<ResponseEntity<String>> protectedRunResult = this.circuitBreaker.protectedRun(() -> { final Result<ResponseEntity<String>> protectedRunResult = this.circuitBreaker.protectedRun(() -> {
final HttpEntity<Object> httpEntity = (body != null) final HttpEntity<Object> httpEntity = (body != null)
? new HttpEntity<>(body, httpHeaders) ? new HttpEntity<>(body, httpHeaders)
: new HttpEntity<>(httpHeaders); : new HttpEntity<>(httpHeaders);
final ResponseEntity<String> result = (uriVariables != null) final ResponseEntity<String> result = this.restTemplate.exchange(
? this.restTemplate.exchange( url,
url, method,
method, httpEntity,
httpEntity, String.class);
String.class,
uriVariables)
: this.restTemplate.exchange(
url,
method,
httpEntity,
String.class);
if (result.getStatusCode() != HttpStatus.OK) { if (result.getStatusCode() != HttpStatus.OK) {
log.warn("Zoom API call to {} respond not 200 -> {}", url, result.getStatusCode()); log.warn("Zoom API call to {} respond not 200 -> {}", url, result.getStatusCode());
throw new RuntimeException("Error Response: " + result.getStatusCode());
} }
return result; return result;
@ -362,39 +705,29 @@ public class ZoomProctoringService implements ExamProctoringService {
return protectedRunResult.getOrThrow(); return protectedRunResult.getOrThrow();
} }
private String createJWT( }
final String appKey,
final CharSequence appSecret,
final Long expTime) {
try { @JsonIgnoreProperties(ignoreUnknown = true)
final StringBuilder builder = new StringBuilder(); public static final class AdditionalZoomRoomData {
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String jwtHeaderPart = urlEncoder.encodeToString( @JsonProperty("user_id")
ZOOM_ACCESS_TOKEN_HEADER.getBytes(StandardCharsets.UTF_8)); public final String user_id;
final String jwtPayload = createPayload(appKey, expTime); @JsonProperty("start_url")
final String jwtPayloadPart = urlEncoder.encodeToString( public final String start_url;
jwtPayload.getBytes(StandardCharsets.UTF_8)); @JsonProperty("join_url")
final String message = jwtHeaderPart + "." + jwtPayloadPart; public final String join_url;
final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG); @JsonCreator
final SecretKeySpec secret_key = new SecretKeySpec( public AdditionalZoomRoomData(
Utils.toByteArray(appSecret), @JsonProperty("user_id") final String user_id,
TOKEN_ENCODE_ALG); @JsonProperty("start_url") final String start_url,
sha256_HMAC.init(secret_key); @JsonProperty("join_url") final String join_url) {
final String hash = urlEncoder.encodeToString(
sha256_HMAC.doFinal(Utils.toByteArray(message)));
builder.append(message) this.user_id = user_id;
.append(".") this.start_url = start_url;
.append(hash); this.join_url = join_url;
return builder.toString();
} catch (final Exception e) {
throw new RuntimeException("Failed to create JWT for Zoom API access: ", e);
}
} }
} }
} }

View file

@ -88,14 +88,14 @@ public interface ZoomRoomRequestResponse {
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static class CreateUserResponse { static class UserResponse {
final String id; final String id;
final String email; final String email;
final int type; final int type;
final String first_name; final String first_name;
final String lasr_name; final String lasr_name;
@JsonCreator @JsonCreator
public CreateUserResponse( public UserResponse(
@JsonProperty("id") final String id, @JsonProperty("id") final String id,
@JsonProperty("email") final String email, @JsonProperty("email") final String email,
@JsonProperty("type") final int type, @JsonProperty("type") final int type,
@ -111,34 +111,34 @@ public interface ZoomRoomRequestResponse {
// https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate // https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static class NewRoomRequest { static class CreateMeetingRequest {
@JsonProperty final String topic; @JsonProperty final String topic;
@JsonProperty final int type = 1; // Instant meeting @JsonProperty final int type = 1; // Instant meeting
@JsonProperty final String start_time = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd`T`HH:mm:ssZ"); @JsonProperty final String start_time = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd`T`HH:mm:ssZ");
@JsonProperty final String password; @JsonProperty final CharSequence password;
@JsonProperty final Settings settings; @JsonProperty final Settings settings;
public NewRoomRequest(final String topic, final String password, final Settings settings) { public CreateMeetingRequest(final String topic, final CharSequence password) {
this.topic = topic; this.topic = topic;
this.password = password; this.password = password;
this.settings = settings; this.settings = new Settings();
} }
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static class Settings { static class Settings {
final boolean host_video = false; @JsonProperty final boolean host_video = false;
final boolean participant_video = true; @JsonProperty final boolean participant_video = true;
final boolean join_before_host = true; @JsonProperty final boolean join_before_host = true;
final int jbh_time = 0; @JsonProperty final int jbh_time = 0;
final boolean use_pmi = false; @JsonProperty final boolean use_pmi = false;
final String audio = "voip"; @JsonProperty final String audio = "voip";
} }
} }
// https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate // https://marketplace.zoom.us/docs/api-reference/zoom-api/meetings/meetingcreate
@JsonIgnoreProperties(ignoreUnknown = true) @JsonIgnoreProperties(ignoreUnknown = true)
static class NewRoomResponse { static class MeetingResponse {
final Integer id; final Long id;
final String join_url; final String join_url;
final String start_url; final String start_url;
final String start_time; final String start_time;
@ -148,8 +148,8 @@ public interface ZoomRoomRequestResponse {
final String host_id; final String host_id;
@JsonCreator @JsonCreator
public NewRoomResponse( public MeetingResponse(
@JsonProperty("id") final Integer id, @JsonProperty("id") final Long id,
@JsonProperty("join_url") final String join_url, @JsonProperty("join_url") final String join_url,
@JsonProperty("start_url") final String start_url, @JsonProperty("start_url") final String start_url,
@JsonProperty("start_time") final String start_time, @JsonProperty("start_time") final String start_time,

View file

@ -9,6 +9,6 @@ ADD COLUMN IF NOT EXISTS `vdi_pair_token` VARCHAR(255) NULL AFTER `vdi`;
-- Alter Table `remote_proctoring_room` -- Alter Table `remote_proctoring_room`
-- ----------------------------------------------------- -- -----------------------------------------------------
ALTER TABLE `remote_proctoring_room` ALTER TABLE `remote_proctoring_room`
ADD COLUMN IF NOT EXISTS `break_out_connections` VARCHAR(10000) NULL, ADD COLUMN IF NOT EXISTS `break_out_connections` VARCHAR(4000) NULL,
ADD COLUMN IF NOT EXISTS `join_key` VARCHAR(255) NULL, ADD COLUMN IF NOT EXISTS `join_key` VARCHAR(255) NULL,
ADD COLUMN IF NOT EXISTS `room_data` VARCHAR(10000) NULL; ADD COLUMN IF NOT EXISTS `room_data` VARCHAR(4000) NULL;

View file

@ -18,7 +18,6 @@ import org.junit.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection; import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringRoomConnection;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.util.Cryptor; import ch.ethz.seb.sebserver.gbl.util.Cryptor;
public class ExamJITSIProctoringServiceTest { public class ExamJITSIProctoringServiceTest {
@ -64,7 +63,6 @@ public class ExamJITSIProctoringServiceTest {
final JitsiProctoringService examJITSIProctoringService = final JitsiProctoringService examJITSIProctoringService =
new JitsiProctoringService(null, null, cryptorMock, null); new JitsiProctoringService(null, null, cryptorMock, null);
final ProctoringRoomConnection data = examJITSIProctoringService.createProctoringConnection( final ProctoringRoomConnection data = examJITSIProctoringService.createProctoringConnection(
ProctoringServerType.JITSI_MEET,
"connectionToken", "connectionToken",
"https://seb-jitsi.example.ch", "https://seb-jitsi.example.ch",
"test-app", "test-app",

View file

@ -95,9 +95,9 @@ CREATE TABLE IF NOT EXISTS `remote_proctoring_room` (
`size` INT NULL, `size` INT NULL,
`subject` VARCHAR(255) NULL, `subject` VARCHAR(255) NULL,
`townhall_room` INT(1) NOT NULL DEFAULT 0, `townhall_room` INT(1) NOT NULL DEFAULT 0,
`break_out_connections` VARCHAR(10000) NULL, `break_out_connections` VARCHAR(4000) NULL,
`join_key` VARCHAR(255) NULL, `join_key` VARCHAR(255) NULL,
`room_data` VARCHAR(10000) NULL, `room_data` VARCHAR(4000) NULL,
PRIMARY KEY (`id`), PRIMARY KEY (`id`),
INDEX `proctor_room_exam_id_idx` (`exam_id` ASC), INDEX `proctor_room_exam_id_idx` (`exam_id` ASC),
CONSTRAINT `proctorRoomExamRef` CONSTRAINT `proctorRoomExamRef`