SEBSERV-148 GUI impl

This commit is contained in:
anhefti 2021-03-25 16:56:39 +01:00
parent 70064206cf
commit ffe4b6301a
15 changed files with 505 additions and 162 deletions

View file

@ -27,6 +27,7 @@ public class ProctoringRoomConnection {
public static final String ATTR_USER_NAME = "userName";
public static final String ATTR_ROOM_KEY = "roomKey";
public static final String ATTR_API_KEY = "apiKey";
public static final String ATTR_MEETING_ID = "meetingId";
@JsonProperty(ProctoringServiceSettings.ATTR_SERVER_TYPE)
public final ProctoringServerType proctoringServerType;
@ -55,6 +56,9 @@ public class ProctoringRoomConnection {
@JsonProperty(ATTR_API_KEY)
public final CharSequence apiKey;
@JsonProperty(ATTR_MEETING_ID)
public final String meetingId;
@JsonProperty(ATTR_USER_NAME)
public final String userName;
@ -69,6 +73,7 @@ public class ProctoringRoomConnection {
@JsonProperty(ATTR_ACCESS_TOKEN) final CharSequence accessToken,
@JsonProperty(ATTR_ROOM_KEY) final CharSequence roomKey,
@JsonProperty(ATTR_API_KEY) final CharSequence apiKey,
@JsonProperty(ATTR_MEETING_ID) final String meetingId,
@JsonProperty(ATTR_USER_NAME) final String userName) {
this.proctoringServerType = proctoringServerType;
@ -80,6 +85,7 @@ public class ProctoringRoomConnection {
this.accessToken = accessToken;
this.roomKey = roomKey;
this.apiKey = apiKey;
this.meetingId = meetingId;
this.userName = userName;
}
@ -123,6 +129,10 @@ public class ProctoringRoomConnection {
return this.subject;
}
public String getMeetingId() {
return this.meetingId;
}
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();

View file

@ -16,4 +16,5 @@ public interface RemoteProctoringView extends TemplateComposer {
*
* @return the remote proctoring server type this remote proctoring view can handle. */
ProctoringServerType serverType();
}

View file

@ -75,6 +75,11 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
this.remoteProctoringViewServletEndpoint = remoteProctoringViewServletEndpoint;
}
@Override
public ProctoringServerType serverType() {
return ProctoringServerType.JITSI_MEET;
}
@Override
public void compose(final PageContext pageContext) {
@ -227,18 +232,6 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
sendReconfigurationAttributes(examId, roomName, state);
}
@Override
public ProctoringServerType serverType() {
return ProctoringServerType.JITSI_MEET;
}
private static final class BroadcastActionState {
public static final String KEY_NAME = "BroadcastActionState";
boolean audio = false;
boolean video = false;
boolean chat = false;
}
private void closeRoom(final ProctoringWindowData proctoringWindowData) {
this.pageService
.getCurrentUser()
@ -246,4 +239,11 @@ public class JitsiMeetProctoringView implements RemoteProctoringView {
.closeRoomWindow(proctoringWindowData.windowName);
}
static final class BroadcastActionState {
public static final String KEY_NAME = "BroadcastActionState";
boolean audio = false;
boolean video = false;
boolean chat = false;
}
}

View file

@ -0,0 +1,249 @@
/*
* Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gui.service.page.impl;
import org.eclipse.swt.SWT;
import org.eclipse.swt.browser.Browser;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowData;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import ch.ethz.seb.sebserver.gbl.Constants;
import ch.ethz.seb.sebserver.gbl.api.API;
import ch.ethz.seb.sebserver.gbl.model.Domain;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.GuiServiceInfo;
import ch.ethz.seb.sebserver.gui.service.i18n.LocTextKey;
import ch.ethz.seb.sebserver.gui.service.page.PageContext;
import ch.ethz.seb.sebserver.gui.service.page.PageService;
import ch.ethz.seb.sebserver.gui.service.page.RemoteProctoringView;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.api.session.SendProctoringReconfigurationAttributes;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService;
import ch.ethz.seb.sebserver.gui.service.session.proctoring.ProctoringGUIService.ProctoringWindowData;
import ch.ethz.seb.sebserver.gui.widget.WidgetFactory;
@Component
@GuiProfile
public class ZoomProctoringView implements RemoteProctoringView {
private static final Logger log = LoggerFactory.getLogger(ZoomProctoringView.class);
private static final LocTextKey CLOSE_WINDOW_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.action.close");
private static final LocTextKey BROADCAST_AUDIO_ON_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.audio");
private static final LocTextKey BROADCAST_AUDIO_OFF_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.audio");
private static final LocTextKey BROADCAST_VIDEO_ON_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.video");
private static final LocTextKey BROADCAST_VIDEO_OFF_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.video");
private static final LocTextKey CHAT_ON_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcaston.chat");
private static final LocTextKey CHAT_OFF_TEXT_KEY =
new LocTextKey("sebserver.monitoring.exam.proctoring.action.broadcastoff.chat");
private final PageService pageService;
private final GuiServiceInfo guiServiceInfo;
private final String remoteProctoringEndpoint;
private final String remoteProctoringViewServletEndpoint;
public ZoomProctoringView(
final PageService pageService,
final GuiServiceInfo guiServiceInfo,
@Value("${sebserver.gui.remote.proctoring.entrypoint:/remote-proctoring}") final String remoteProctoringEndpoint,
@Value("${sebserver.gui.remote.proctoring.api-servler.endpoint:/remote-view-servlet}") final String remoteProctoringViewServletEndpoint) {
this.pageService = pageService;
this.guiServiceInfo = guiServiceInfo;
this.remoteProctoringEndpoint = remoteProctoringEndpoint;
this.remoteProctoringViewServletEndpoint = remoteProctoringViewServletEndpoint;
}
@Override
public ProctoringServerType serverType() {
return ProctoringServerType.ZOOM;
}
@Override
public void compose(final PageContext pageContext) {
final ProctoringWindowData proctoringWindowData = ProctoringGUIService.getCurrentProctoringWindowData();
final Composite parent = pageContext.getParent();
final Composite content = new Composite(parent, SWT.NONE | SWT.NO_SCROLL);
final GridLayout gridLayout = new GridLayout();
content.setLayout(gridLayout);
final GridData headerCell = new GridData(SWT.FILL, SWT.FILL, true, true);
content.setLayoutData(headerCell);
parent.addListener(SWT.Dispose, event -> closeRoom(proctoringWindowData));
final String url = this.guiServiceInfo
.getExternalServerURIBuilder()
.toUriString()
+ this.remoteProctoringEndpoint
+ this.remoteProctoringViewServletEndpoint
+ Constants.SLASH;
if (log.isDebugEnabled()) {
log.debug("Open proctoring Servlet in IFrame with URL: {}", url);
}
final Browser browser = new Browser(content, SWT.NONE | SWT.NO_SCROLL);
browser.setLayout(new GridLayout());
final GridData gridData = new GridData(SWT.FILL, SWT.FILL, true, true);
browser.setLayoutData(gridData);
browser.setUrl(url);
browser.setBackground(new Color(parent.getDisplay(), 100, 100, 100));
final Composite footer = new Composite(content, SWT.NONE | SWT.NO_SCROLL);
footer.setLayout(new RowLayout());
final GridData footerLayout = new GridData(SWT.CENTER, SWT.BOTTOM, true, false);
footerLayout.heightHint = 40;
footer.setLayoutData(footerLayout);
final WidgetFactory widgetFactory = this.pageService.getWidgetFactory();
final Button closeAction = widgetFactory.buttonLocalized(footer, CLOSE_WINDOW_TEXT_KEY);
closeAction.setLayoutData(new RowData(150, 30));
closeAction.addListener(SWT.Selection, event -> closeRoom(proctoringWindowData));
final BroadcastActionState broadcastActionState = new BroadcastActionState();
final Button broadcastAudioAction = widgetFactory.buttonLocalized(footer, BROADCAST_AUDIO_ON_TEXT_KEY);
broadcastAudioAction.setLayoutData(new RowData(150, 30));
broadcastAudioAction.addListener(SWT.Selection, event -> toggleBroadcastAudio(
proctoringWindowData.examId,
proctoringWindowData.connectionData.roomName,
broadcastAudioAction));
broadcastAudioAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState);
final Button broadcastVideoAction = widgetFactory.buttonLocalized(footer, BROADCAST_VIDEO_ON_TEXT_KEY);
broadcastVideoAction.setLayoutData(new RowData(150, 30));
broadcastVideoAction.addListener(SWT.Selection, event -> toggleBroadcastVideo(
proctoringWindowData.examId,
proctoringWindowData.connectionData.roomName,
broadcastVideoAction,
broadcastAudioAction));
broadcastVideoAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState);
final Button chatAction = widgetFactory.buttonLocalized(footer, CHAT_ON_TEXT_KEY);
chatAction.setLayoutData(new RowData(150, 30));
chatAction.addListener(SWT.Selection, event -> toggleChat(
proctoringWindowData.examId,
proctoringWindowData.connectionData.roomName,
chatAction));
chatAction.setData(BroadcastActionState.KEY_NAME, broadcastActionState);
}
private void sendReconfigurationAttributes(
final String examId,
final String roomName,
final BroadcastActionState state) {
this.pageService.getRestService().getBuilder(SendProctoringReconfigurationAttributes.class)
.withURIVariable(API.PARAM_MODEL_ID, examId)
.withFormParam(Domain.REMOTE_PROCTORING_ROOM.ATTR_ID, roomName)
.withFormParam(
API.EXAM_PROCTORING_ATTR_RECEIVE_AUDIO,
state.audio ? Constants.TRUE_STRING : Constants.FALSE_STRING)
.withFormParam(
API.EXAM_PROCTORING_ATTR_RECEIVE_VIDEO,
state.video ? Constants.TRUE_STRING : Constants.FALSE_STRING)
.withFormParam(
API.EXAM_PROCTORING_ATTR_ALLOW_CHAT,
state.chat ? Constants.TRUE_STRING : Constants.FALSE_STRING)
.call()
.onError(error -> log.error("Failed to send broadcast attributes to clients in room: {} cause: {}",
roomName,
error.getMessage()));
}
private void toggleBroadcastAudio(
final String examId,
final String roomName,
final Button broadcastAction) {
final BroadcastActionState state =
(BroadcastActionState) broadcastAction.getData(BroadcastActionState.KEY_NAME);
this.pageService.getPolyglotPageService().injectI18n(
broadcastAction,
state.audio ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY);
state.audio = !state.audio;
sendReconfigurationAttributes(examId, roomName, state);
}
private void toggleBroadcastVideo(
final String examId,
final String roomName,
final Button videoAction,
final Button audioAction) {
final BroadcastActionState state =
(BroadcastActionState) videoAction.getData(BroadcastActionState.KEY_NAME);
this.pageService.getPolyglotPageService().injectI18n(
audioAction,
state.video ? BROADCAST_AUDIO_ON_TEXT_KEY : BROADCAST_AUDIO_OFF_TEXT_KEY);
this.pageService.getPolyglotPageService().injectI18n(
videoAction,
state.video ? BROADCAST_VIDEO_ON_TEXT_KEY : BROADCAST_VIDEO_OFF_TEXT_KEY);
state.video = !state.video;
state.audio = state.video;
sendReconfigurationAttributes(examId, roomName, state);
}
private void toggleChat(
final String examId,
final String roomName,
final Button broadcastAction) {
final BroadcastActionState state =
(BroadcastActionState) broadcastAction.getData(BroadcastActionState.KEY_NAME);
this.pageService.getPolyglotPageService().injectI18n(
broadcastAction,
state.chat ? CHAT_ON_TEXT_KEY : CHAT_OFF_TEXT_KEY);
state.chat = !state.chat;
sendReconfigurationAttributes(examId, roomName, state);
}
private void closeRoom(final ProctoringWindowData proctoringWindowData) {
this.pageService
.getCurrentUser()
.getProctoringGUIService()
.closeRoomWindow(proctoringWindowData.windowName);
}
static final class BroadcastActionState {
public static final String KEY_NAME = "BroadcastActionState";
boolean audio = false;
boolean video = false;
boolean chat = false;
}
}

View file

@ -64,16 +64,16 @@ public class ZoomWindowScriptResolver implements ProctoringWindowScriptResolver
+ " ZoomMtg.preLoadWasm();\n"
+ " ZoomMtg.prepareJssdk();\n"
+ "\n"
+ " const API_KEY = \"%%_\" + ATTR_API_KEY + \"_%%\";\n"
+ " const API_KEY = \"%%_" + ATTR_API_KEY + "_%%\";\n"
+ " const config = {\n"
+ " meetingNumber: %%_\" + ATTR_ROOM_NAME + \"_%%,\n"
+ " leaveUrl: '%%_\" + ATTR_HOST + \"_%%',\n"
+ " userName: '%%_\" + ATTR_USER_NAME + \"_%%',\n"
+ " passWord: '%%_\" + ATTR_ROOM_KEY + \"_%%',\n"
+ " role: 0 // 1 for host; 0 for attendee\n"
+ " meetingNumber: %%_" + ATTR_ROOM_NAME + "_%%,\n"
+ " leaveUrl: '%%_" + ATTR_HOST + "_%%',\n"
+ " userName: '%%_" + ATTR_USER_NAME + "_%%',\n"
+ " passWord: '%%_" + ATTR_ROOM_KEY + "_%%',\n"
+ " role: 1 // 1 for host; 0 for attendee\n"
+ " };\n"
+ "\n"
+ " const signature = '%%_\" + ATTR_ACCESS_TOKEN + \"_%%';\n"
+ " const signature = '%%_" + ATTR_ACCESS_TOKEN + "_%%';\n"
+ "\n"
+ " console.log(\"Initializing meeting...\");\n"
+ "\n"
@ -155,7 +155,7 @@ public class ZoomWindowScriptResolver implements ProctoringWindowScriptResolver
public String getProctoringWindowScript(final ProctoringWindowData data) {
final Map<String, String> args = new HashMap<>();
args.put(ATTR_HOST, data.connectionData.serverHost);
args.put(ATTR_ROOM_NAME, data.connectionData.roomName);
args.put(ATTR_ROOM_NAME, data.connectionData.meetingId);
args.put(ATTR_ACCESS_TOKEN, String.valueOf(data.connectionData.accessToken));
args.put(ATTR_API_KEY, String.valueOf(data.connectionData.apiKey));
if (StringUtils.isNotBlank(data.connectionData.roomKey)) {

View file

@ -41,16 +41,16 @@ public interface ExamAdminService {
* @return Result refer to the restriction flag or to an error when happened */
Result<Boolean> isRestricted(Exam exam);
/** Get the proctoring service settings for a certain exam to an error when happened.
*
* @param examId the exam instance
* @return Result refer to proctoring service settings for the exam. */
default Result<ProctoringServiceSettings> getProctoringServiceSettings(final Exam exam) {
if (exam == null || exam.id == null) {
return Result.ofRuntimeError("Invalid Exam model");
}
return getProctoringServiceSettings(exam.id);
}
// /** Get the proctoring service settings for a certain exam to an error when happened.
// *
// * @param examId the exam instance
// * @return Result refer to proctoring service settings for the exam. */
// default Result<ProctoringServiceSettings> getProctoringServiceSettings(final Exam exam) {
// if (exam == null || exam.id == null) {
// return Result.ofRuntimeError("Invalid Exam model");
// }
// return getProctoringServiceSettings(exam.id);
// }
/** Get proctoring service settings for a certain exam to an error when happened.
*
@ -90,29 +90,38 @@ public interface ExamAdminService {
* @return ExamProctoringService instance */
Result<ExamProctoringService> 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<ExamProctoringService> getExamProctoringService(final ProctoringServiceSettings settings) {
return Result.tryCatch(() -> getExamProctoringService(settings.serverType).getOrThrow());
}
/** Get the exam proctoring service implementation for specified exam.
*
* @param exam the exam instance
* @return ExamProctoringService instance */
default Result<ExamProctoringService> getExamProctoringService(final Exam exam) {
return Result.tryCatch(() -> getExamProctoringService(exam.id).getOrThrow());
}
/** Get the exam proctoring service implementation for specified exam.
*
* @param examId the exam identifier
* @return ExamProctoringService instance */
default Result<ExamProctoringService> getExamProctoringService(final Long examId) {
return getProctoringServiceSettings(examId)
.flatMap(this::getExamProctoringService);
.flatMap(settings -> getExamProctoringService(settings.serverType));
}
// /** Get the exam proctoring service implementation of specified type.
// *
// * @param settings the ProctoringSettings that defines the ProctoringServerType
// * @return ExamProctoringService instance */
// default Result<ExamProctoringService> getExamProctoringService(final ProctoringServiceSettings settings) {
// return Result.tryCatch(() -> getExamProctoringService(settings.serverType).getOrThrow());
// }
//
// /** Get the exam proctoring service implementation for specified exam.
// *
// * @param exam the exam instance
// * @return ExamProctoringService instance */
// default Result<ExamProctoringService> getExamProctoringService(final Exam exam) {
// return Result.tryCatch(() -> getExamProctoringService(exam.id).getOrThrow());
// }
//
// /** Get the exam proctoring service implementation for specified exam.
// *
// * @param examId the exam identifier
// * @return ExamProctoringService instance */
// default Result<ExamProctoringService> getExamProctoringService(final Long examId) {
// return getProctoringServiceSettings(examId)
// .flatMap(this::getExamProctoringService);
// }
}

View file

@ -12,7 +12,6 @@ import java.util.Map;
import org.apache.commons.lang3.StringUtils;
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.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
@ -52,7 +51,7 @@ public interface ExamProctoringService {
Map<String, String> createJoinInstructionAttributes(ProctoringRoomConnection proctoringConnection);
Result<Void> disposeServiceRoomsForExam(ProctoringServiceSettings proctoringSettings, Exam exam);
Result<Void> disposeServiceRoomsForExam(Long examId, ProctoringServiceSettings proctoringSettings);
default String verifyRoomName(final String requestedRoomName, final String connectionToken) {
if (StringUtils.isNotBlank(requestedRoomName)) {

View file

@ -115,14 +115,13 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
return Result.tryCatch(() -> {
final ProctoringServiceSettings settings = this.examSessionService
.getRunningExam(exam.id)
.flatMap(this.examAdminService::getProctoringServiceSettings)
final ProctoringServiceSettings proctoringSettings = this.examAdminService
.getProctoringServiceSettings(exam.id)
.getOrThrow();
this.examAdminService
.getExamProctoringService(exam)
.flatMap(service -> service.disposeServiceRoomsForExam(settings, exam))
.getExamProctoringService(proctoringSettings.serverType)
.flatMap(service -> service.disposeServiceRoomsForExam(exam.id, proctoringSettings))
.onError(error -> log.error("Failed to dispose proctoring service rooms for exam: {} / {}",
exam.name,
exam.externalId,
@ -143,9 +142,9 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
}
return Result.tryCatch(() -> {
final ProctoringServiceSettings settings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.examAdminService::getProctoringServiceSettings)
final ProctoringServiceSettings settings = this.examAdminService
.getProctoringServiceSettings(examId)
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
@ -182,9 +181,8 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
return Result.tryCatch(() -> {
final ProctoringServiceSettings settings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.examAdminService::getProctoringServiceSettings)
final ProctoringServiceSettings settings = this.examAdminService
.getProctoringServiceSettings(examId)
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
@ -213,13 +211,12 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
public Result<Void> closeProctoringRoom(final Long examId, final String roomName) {
return Result.tryCatch(() -> {
final ProctoringServiceSettings proctoringSettings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.examAdminService::getProctoringServiceSettings)
final ProctoringServiceSettings settings = this.examAdminService
.getProctoringServiceSettings(examId)
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
.getExamProctoringService(proctoringSettings.serverType)
.getExamProctoringService(settings.serverType)
.getOrThrow();
// Get room
@ -228,9 +225,9 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.getOrThrow();
if (!remoteProctoringRoom.breakOutConnections.isEmpty()) {
closeBreakOutRoom(examId, proctoringSettings, examProctoringService, remoteProctoringRoom);
closeBreakOutRoom(examId, settings, examProctoringService, remoteProctoringRoom);
} else if (remoteProctoringRoom.townhallRoom) {
closeTownhall(examId, proctoringSettings, examProctoringService);
closeTownhall(examId, settings, examProctoringService);
} else {
closeCollectingRoom(examId, roomName, examProctoringService);
}
@ -301,7 +298,7 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
.getExamProctoringService(examId)
.getExamProctoringService(proctoringSettings.serverType)
.getOrThrow();
return this.remoteProctoringRoomDAO.reservePlaceInCollectingRoom(
@ -415,13 +412,12 @@ public class ExamProctoringRoomServiceImpl implements ExamProctoringRoomService
return Result.tryCatch(() -> {
final ProctoringServiceSettings proctoringSettings = this.examSessionService
.getRunningExam(examId)
.flatMap(this.examAdminService::getProctoringServiceSettings)
final ProctoringServiceSettings settings = this.examAdminService
.getProctoringServiceSettings(examId)
.getOrThrow();
final ExamProctoringService examProctoringService = this.examAdminService
.getExamProctoringService(proctoringSettings.serverType)
.getExamProctoringService(settings.serverType)
.getOrThrow();
final RemoteProctoringRoom room = this.remoteProctoringRoomDAO

View file

@ -165,9 +165,8 @@ public class JitsiProctoringService implements ExamProctoringService {
@Override
public Result<Void> disposeServiceRoomsForExam(
final ProctoringServiceSettings proctoringSettings,
final Exam exam) {
final Long examId,
final ProctoringServiceSettings proctoringSettings) {
// NOTE: Since Jitsi rooms are generated and disposed automatically we don't need to do anything here
return Result.EMPTY;
}
@ -329,6 +328,7 @@ public class JitsiProctoringService implements ExamProctoringService {
token,
null,
null,
null,
clientName);
});
}

View file

@ -20,6 +20,7 @@ import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
@ -49,7 +50,6 @@ import ch.ethz.seb.sebserver.gbl.api.JSONMapper;
import ch.ethz.seb.sebserver.gbl.async.AsyncService;
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.ProctoringRoomConnection;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings;
import ch.ethz.seb.sebserver.gbl.model.exam.ProctoringServiceSettings.ProctoringServerType;
@ -167,19 +167,17 @@ public class ZoomProctoringService implements ExamProctoringService {
throw new APIMessageException(
APIMessage.ErrorMessage.BINDING_ERROR,
String.valueOf(result.getStatusCode()));
} else {
// TODO this is just for cleaning up along development process.
// Remove this before finish up the Zoom integration
try {
} catch (final Exception e) {
log.error("Failed to dev-cleanup rooms: ", e);
}
}
// 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) {
log.error("Failed to access Zoom service at: {}", proctoringSettings.serverURL, e);
throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR, e.getMessage());
@ -247,10 +245,13 @@ public class ZoomProctoringService implements ExamProctoringService {
final ClientCredentials credentials = new ClientCredentials(
proctoringSettings.appKey,
this.cryptor.decrypt(proctoringSettings.appSecret),
this.cryptor.decrypt(remoteProctoringRoom.joinKey));
proctoringSettings.appSecret,
remoteProctoringRoom.joinKey);
final String jwt = this.createJWTForMeetingAccess(credentials, subject);
final String jwt = this.createJWTForMeetingAccess(
credentials,
String.valueOf(additionalZoomRoomData.meeting_id),
true);
return new ProctoringRoomConnection(
ProctoringServerType.ZOOM,
@ -260,8 +261,9 @@ public class ZoomProctoringService implements ExamProctoringService {
roomName,
subject,
jwt,
credentials.accessToken,
this.cryptor.decrypt(credentials.accessToken),
credentials.clientId,
String.valueOf(additionalZoomRoomData.meeting_id),
this.authorizationService.getUserService().getCurrentUser().getUsername());
});
}
@ -285,10 +287,13 @@ public class ZoomProctoringService implements ExamProctoringService {
final ClientCredentials credentials = new ClientCredentials(
proctoringSettings.appKey,
this.cryptor.decrypt(proctoringSettings.appSecret),
this.cryptor.decrypt(remoteProctoringRoom.joinKey));
proctoringSettings.appSecret,
remoteProctoringRoom.joinKey);
final String jwt = this.createJWTForMeetingAccess(credentials, subject);
final String jwt = this.createJWTForMeetingAccess(
credentials,
String.valueOf(additionalZoomRoomData.meeting_id),
false);
final ClientConnectionData clientConnection = this.examSessionService
.getConnectionData(connectionToken)
@ -302,20 +307,22 @@ public class ZoomProctoringService implements ExamProctoringService {
roomName,
subject,
jwt,
credentials.accessToken,
this.cryptor.decrypt(credentials.accessToken),
credentials.clientId,
String.valueOf(additionalZoomRoomData.meeting_id),
clientConnection.clientConnection.userSessionId);
});
}
@Override
public Result<Void> disposeServiceRoomsForExam(
final ProctoringServiceSettings proctoringSettings,
final Exam exam) {
final Long examId,
final ProctoringServiceSettings proctoringSettings) {
return Result.tryCatch(() -> {
this.remoteProctoringRoomDAO
.getRooms(exam.id)
.getRooms(examId)
.getOrThrow()
.stream()
.forEach(room -> {
@ -361,9 +368,7 @@ public class ZoomProctoringService implements ExamProctoringService {
.getRoom(proctoringSettings.examId, roomName)
.getOrThrow();
AdditionalZoomRoomData additionalZoomRoomData;
additionalZoomRoomData = this.jsonMapper.readValue(
final AdditionalZoomRoomData additionalZoomRoomData = this.jsonMapper.readValue(
roomData.getAdditionalRoomData(),
AdditionalZoomRoomData.class);
@ -414,9 +419,9 @@ public class ZoomProctoringService implements ExamProctoringService {
UserResponse.class);
// Then create new meeting with the ad-hoc user/host
final CharSequence meetingPwd = UUID.randomUUID().toString();
final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9);
final ResponseEntity<String> createMeeting = this.zoomRestTemplate.createMeeting(
roomName,
proctoringSettings.serverURL,
credentials,
userResponse.id,
subject,
@ -429,6 +434,7 @@ public class ZoomProctoringService implements ExamProctoringService {
// Create NewRoom data with all needed information to store persistent
final AdditionalZoomRoomData additionalZoomRoomData = new AdditionalZoomRoomData(
meetingResponse.id,
userResponse.id,
meetingResponse.start_url,
meetingResponse.join_url);
@ -438,7 +444,7 @@ public class ZoomProctoringService implements ExamProctoringService {
return new NewRoom(
roomName,
subject,
this.cryptor.encrypt(meetingPwd),
this.cryptor.encrypt(meetingResponse.meetingPwd),
additionalZoomRoomDataString);
});
}
@ -506,43 +512,63 @@ public class ZoomProctoringService implements ExamProctoringService {
private String createJWTForMeetingAccess(
final ClientCredentials credentials,
final String subject) {
final String meetingId,
final boolean host) {
try {
final long iat = Utils.getMillisecondsNow() / 1000;
final long exp = iat + 7200;
final String apiKey = credentials.clientIdAsString();
final int status = host ? 1 : 0;
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_MEETING_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 hasher = Mac.getInstance("HmacSHA256");
final String ts = Long.toString(System.currentTimeMillis() - 30000);
final String msg = String.format("%s%s%s%d", apiKey, meetingId, ts, status);
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)));
hasher.init(new SecretKeySpec(decryptedSecret.toString().getBytes(), "HmacSHA256"));
builder.append(message)
.append(".")
.append(hash);
final String message = Base64.getEncoder().encodeToString(msg.getBytes());
final byte[] hash = hasher.doFinal(message.getBytes());
return builder.toString();
final String hashBase64Str = DatatypeConverter.printBase64Binary(hash);
final String tmpString = String.format("%s.%s.%s.%d.%s", apiKey, meetingId, ts, status, hashBase64Str);
final String encodedString = Base64.getEncoder().encodeToString(tmpString.getBytes());
return encodedString.replaceAll("\\=+$", "");
// final long iat = Utils.getMillisecondsNow() / 1000;
// final long exp = iat + 7200;
//
// final CharSequence decryptedSecret = this.cryptor.decrypt(credentials.secret);
// final CharSequence decryptedMeetingPWD = 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_MEETING_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""),
// credentials.clientIdAsString(),
// iat,
// exp,
// subject,
// decryptedMeetingPWD);
// 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);
}
@ -554,7 +580,7 @@ public class ZoomProctoringService implements ExamProctoringService {
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_ZOOM_ROOM_USER = "SEBProctoringRoomUser";
private static final String API_CREATE_MEETING_ENDPOINT = "v2/users/{userid}/meetings";
private static final String API_DELETE_MEETING_ENDPOINT = "v2/meetings/{meetingid}";
@ -579,16 +605,23 @@ public class ZoomProctoringService implements ExamProctoringService {
final String zoomServerUrl,
final ClientCredentials credentials) {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.path(API_TEST_ENDPOINT)
.queryParam("status", "active")
.queryParam("page_size", "10")
.queryParam("page_number", "1")
.queryParam("data_type", "Json")
.build()
.toUriString();
return exchange(url, HttpMethod.GET, credentials);
try {
final String url = UriComponentsBuilder
.fromUriString(zoomServerUrl)
.path(API_TEST_ENDPOINT)
.queryParam("status", "active")
.queryParam("page_size", "10")
.queryParam("page_number", "1")
.queryParam("data_type", "Json")
.build()
.toUriString();
return exchange(url, HttpMethod.GET, credentials);
} catch (final Exception e) {
log.error("Failed to test zoom service connection: ", e);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
public ResponseEntity<String> createUser(
@ -617,7 +650,7 @@ public class ZoomProctoringService implements ExamProctoringService {
} 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);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@ -645,7 +678,7 @@ public class ZoomProctoringService implements ExamProctoringService {
} 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);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@ -666,7 +699,7 @@ public class ZoomProctoringService implements ExamProctoringService {
} 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);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@ -687,7 +720,7 @@ public class ZoomProctoringService implements ExamProctoringService {
} 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);
return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@ -728,9 +761,8 @@ public class ZoomProctoringService implements ExamProctoringService {
httpEntity,
String.class);
if (result.getStatusCode() != HttpStatus.OK) {
log.warn("Zoom API call to {} respond not 200 -> {}", url, result.getStatusCode());
throw new RuntimeException("Error Response: " + result.getStatusCode());
if (result.getStatusCode() != HttpStatus.OK && result.getStatusCode() != HttpStatus.CREATED) {
log.warn("Error response on Zoom API call to {} response status: {}", url, result.getStatusCode());
}
return result;
@ -743,6 +775,8 @@ public class ZoomProctoringService implements ExamProctoringService {
@JsonIgnoreProperties(ignoreUnknown = true)
public static final class AdditionalZoomRoomData {
@JsonProperty("meeting_id")
private final Long meeting_id;
@JsonProperty("user_id")
public final String user_id;
@JsonProperty("start_url")
@ -752,15 +786,16 @@ public class ZoomProctoringService implements ExamProctoringService {
@JsonCreator
public AdditionalZoomRoomData(
@JsonProperty("meeting_id") final Long meeting_id,
@JsonProperty("user_id") final String user_id,
@JsonProperty("start_url") final String start_url,
@JsonProperty("join_url") final String join_url) {
this.meeting_id = meeting_id;
this.user_id = user_id;
this.start_url = start_url;
this.join_url = join_url;
}
}
}

View file

@ -77,12 +77,12 @@ public interface ZoomRoomRequestResponse {
@JsonProperty final String email;
@JsonProperty final int type;
@JsonProperty final String first_name;
@JsonProperty final String lasr_name;
public UserInfo(final String email, final int type, final String first_name, final String lasr_name) {
@JsonProperty final String last_name;
public UserInfo(final String email, final int type, final String first_name, final String last_name) {
this.email = email;
this.type = type;
this.first_name = first_name;
this.lasr_name = lasr_name;
this.last_name = last_name;
}
}
}
@ -113,8 +113,9 @@ public interface ZoomRoomRequestResponse {
@JsonIgnoreProperties(ignoreUnknown = true)
static class CreateMeetingRequest {
@JsonProperty final String topic;
@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 int type = 2; // Scheduled Meeting
@JsonProperty final String start_time = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss");
@JsonProperty final int duration = 60;
@JsonProperty final CharSequence password;
@JsonProperty final Settings settings;
@ -146,6 +147,8 @@ public interface ZoomRoomRequestResponse {
final String status;
final String uuid;
final String host_id;
final CharSequence meetingPwd;
final CharSequence encryptedPwd;
@JsonCreator
public MeetingResponse(
@ -156,7 +159,10 @@ public interface ZoomRoomRequestResponse {
@JsonProperty("duration") final Integer duration,
@JsonProperty("status") final String status,
@JsonProperty("uuid") final String uuid,
@JsonProperty("host_id") final String host_id) {
@JsonProperty("host_id") final String host_id,
@JsonProperty("password") final CharSequence meetingPwd,
@JsonProperty("encrypted_password") final CharSequence encryptedPwd) {
this.id = id;
this.join_url = join_url;
this.start_url = start_url;
@ -165,6 +171,8 @@ public interface ZoomRoomRequestResponse {
this.status = status;
this.uuid = uuid;
this.host_id = host_id;
this.meetingPwd = meetingPwd;
this.encryptedPwd = encryptedPwd;
}
}

View file

@ -396,7 +396,7 @@ public class ExamAdministrationController extends EntityController<Exam, Exam> {
checkReadPrivilege(institutionId);
return this.entityDAO.byPK(modelId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getProctoringServiceSettings)
.flatMap(exam -> this.examAdminService.getProctoringServiceSettings(exam.id))
.getOrThrow();
}

View file

@ -111,7 +111,7 @@ public class ExamProctoringController {
checkAccess(institutionId, examId);
return this.examSessionService.getRunningExam(examId)
.flatMap(this.authorization::checkRead)
.flatMap(this.examAdminService::getExamProctoringService)
.flatMap(exam -> this.examAdminService.getExamProctoringService(exam.id))
.flatMap(service -> service.getProctorRoomConnection(
this.examAdminService.getProctoringServiceSettings(examId).getOrThrow(),
roomName,

View file

@ -36,7 +36,7 @@ logging.level.ch=INFO
### spring actuator configuration
management.endpoints.web.base-path=/mprofile
management.endpoints.web.exposure.include=metrics,logfile,loggers,heapdump
management.endpoints.web.exposure.include=metrics,logfile,loggers,heapdump,health
##########################################################
### Overall Security Settings

View file

@ -0,0 +1,36 @@
/*
* Copyright (c) 2021 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.gbl.util;
import static org.junit.Assert.assertEquals;
import java.util.UUID;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.junit.Ignore;
import org.junit.jupiter.api.Test;
public class ReplTest {
@Test
@Ignore
public void testDateFormatting() {
final String datestring = DateTime.now(DateTimeZone.UTC).toString("yyyy-MM-dd'T'HH:mm:ss");
assertEquals("", datestring);
}
@Test
@Ignore
public void testGenPwd() {
final CharSequence meetingPwd = UUID.randomUUID().toString().subSequence(0, 9);
assertEquals("", meetingPwd);
}
}