SEBSERV-139 Jitsi Javascript API integration

This commit is contained in:
anhefti 2020-08-20 08:28:31 +02:00
parent 59e3d29e3d
commit 0e293c7602
7 changed files with 195 additions and 61 deletions

View file

@ -15,11 +15,15 @@ import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class SEBClientProctoringConnectionData {
public static final String ATTR_SERVER_HOST = "serverHost";
public static final String ATTR_SERVER_URL = "serverURL";
public static final String ATTR_ROOM_NAME = "roomName";
public static final String ATTR_ACCESS_TOKEN = "accessToken";
public static final String ATTR_CONNECTION_URL = "connectionURL";
@JsonProperty(ATTR_SERVER_HOST)
public final String serverHost;
@JsonProperty(ATTR_SERVER_URL)
public final String serverURL;
@ -34,17 +38,23 @@ public class SEBClientProctoringConnectionData {
@JsonCreator
public SEBClientProctoringConnectionData(
@JsonProperty(ATTR_SERVER_HOST) final String serverHost,
@JsonProperty(ATTR_SERVER_URL) final String serverURL,
@JsonProperty(ATTR_ROOM_NAME) final String roomName,
@JsonProperty(ATTR_ACCESS_TOKEN) final String accessToken,
@JsonProperty(ATTR_CONNECTION_URL) final String connectionURL) {
this.serverHost = serverHost;
this.serverURL = serverURL;
this.roomName = roomName;
this.accessToken = accessToken;
this.connectionURL = connectionURL;
}
public String getServerHost() {
return this.serverHost;
}
public String getAccessToken() {
return this.accessToken;
}
@ -64,7 +74,9 @@ public class SEBClientProctoringConnectionData {
@Override
public String toString() {
final StringBuilder builder = new StringBuilder();
builder.append("SEBClientProctoringConnectionData [serverURL=");
builder.append("SEBClientProctoringConnectionData [serverHost=");
builder.append(this.serverHost);
builder.append(", serverURL=");
builder.append(this.serverURL);
builder.append(", roomName=");
builder.append(this.roomName);

View file

@ -0,0 +1,102 @@
/*
* Copyright (c) 2020 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;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import ch.ethz.seb.sebserver.gbl.model.exam.SEBClientProctoringConnectionData;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.AuthorizationContextHolder;
import ch.ethz.seb.sebserver.gui.service.remote.webservice.auth.SEBServerAuthorizationContext;
@Component
@GuiProfile
public class ProctoringServlet extends HttpServlet {
private static final long serialVersionUID = 3475978419653411800L;
public static final String SESSION_ATTR_PROCTORING_DATA = "SESSION_ATTR_PROCTORING_DATA";
// @formatter:off
private static final String HTML =
"<!DOCTYPE html>" +
"<html>" +
"<head>" +
" <title></title>" +
" <script src='https://%s/external_api.js'></script>" +
"</head>" +
"" +
"<body>" +
"<div id=\"proctoring\"></div> " +
"</body>" +
"<script>" +
" const options = {\r\n" +
" parentNode: document.querySelector('#proctoring'),\r\n" +
" roomName: '%s',\r\n" +
" width: 600,\r\n" +
" height: 400,\r\n" +
" jwt: '%s'\r\n" +
" }\r\n" +
" meetAPI = new JitsiMeetExternalAPI(\"%s\", options);\r\n" +
"</script>" +
"</html>";
// @formatter:on
@Override
protected void doGet(
final HttpServletRequest req,
final HttpServletResponse resp)
throws ServletException, IOException {
final HttpSession httpSession = req.getSession();
final ServletContext servletContext = httpSession.getServletContext();
final WebApplicationContext webApplicationContext = WebApplicationContextUtils
.getRequiredWebApplicationContext(servletContext);
final boolean authenticated = isAuthenticated(httpSession, webApplicationContext);
if (!authenticated) {
resp.setStatus(HttpStatus.FORBIDDEN.value());
return;
}
final SEBClientProctoringConnectionData proctoringConnectionData =
(SEBClientProctoringConnectionData) httpSession.getAttribute(SESSION_ATTR_PROCTORING_DATA);
final String accessToken = proctoringConnectionData.getAccessToken();
final String roomName = proctoringConnectionData.roomName;
final String server = "seb-jitsi.ethz.ch";
final String script = String.format(HTML, server, roomName, accessToken, server);
resp.getOutputStream().println(script);
}
private boolean isAuthenticated(
final HttpSession httpSession,
final WebApplicationContext webApplicationContext) {
final AuthorizationContextHolder authorizationContextHolder = webApplicationContext
.getBean(AuthorizationContextHolder.class);
final SEBServerAuthorizationContext authorizationContext = authorizationContextHolder
.getAuthorizationContext(httpSession);
return authorizationContext.isValid() && authorizationContext.isLoggedIn();
}
}

View file

@ -8,14 +8,8 @@
package ch.ethz.seb.sebserver.gui;
import java.io.IOException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextListener;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.eclipse.rap.rwt.engine.RWTServlet;
import org.eclipse.rap.rwt.engine.RWTServletContextListener;
@ -25,6 +19,7 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.ServletContextInitializer;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -63,25 +58,12 @@ public class RAPSpringConfig {
}
@Bean
public ServletRegistrationBean<ProctoringServlet> servletProctoringRegistrationBean() {
return new ServletRegistrationBean<>(new ProctoringServlet(), "/proctoring/*");
}
private static class ProctoringServlet extends HttpServlet {
private static final long serialVersionUID = 3475978419653411800L;
@Override
protected void doGet(
final HttpServletRequest req,
final HttpServletResponse resp)
throws ServletException, IOException {
System.out.println("********************************");
resp.getOutputStream().println("Hello");
}
public ServletRegistrationBean<ProctoringServlet> servletProctoringRegistrationBean(
final ApplicationContext applicationContext) {
final ProctoringServlet proctoringServlet = applicationContext
.getBean(ProctoringServlet.class);
return new ServletRegistrationBean<>(proctoringServlet, "/proctoring/*");
}
@Bean

View file

@ -8,6 +8,8 @@
package ch.ethz.seb.sebserver.gui.content;
import java.util.Base64;
import java.util.Base64.Encoder;
import java.util.Collection;
import org.eclipse.rap.rwt.RWT;
@ -33,6 +35,7 @@ import ch.ethz.seb.sebserver.gbl.model.session.ExtendedClientEvent;
import ch.ethz.seb.sebserver.gbl.model.user.UserRole;
import ch.ethz.seb.sebserver.gbl.profile.GuiProfile;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.gui.ProctoringServlet;
import ch.ethz.seb.sebserver.gui.content.action.ActionDefinition;
import ch.ethz.seb.sebserver.gui.service.ResourceService;
import ch.ethz.seb.sebserver.gui.service.i18n.I18nSupport;
@ -276,16 +279,18 @@ public class MonitoringClientConnection implements TemplateComposer {
}
private PageAction openProctorScreen(final PageAction action, final String connectionToken) {
//
// final ProctorDialog dialog = new ProctorDialog(action.pageContext().getParent().getShell());
// dialog.open(EVENT_LIST_TITLE_KEY,
// "https://seb-jitsi.ethz.ch/TestRoomABC?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsiYXZhdGFyIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9qb2huLWRvZSIsIm5hbWUiOiJEaXNwbGF5IE5hbWUiLCJlbWFpbCI6Im5hbWVAZXhhbXBsZS5jb20ifX0sImF1ZCI6InNlYi1qaXRzaSIsImlzcyI6InNlYi1qaXRzaSIsInN1YiI6Im1lZXQuaml0c2kiLCJyb29tIjoiKiJ9.SD9Zs78mMFqxS1tpalPTykYYaubIYsj_406WAOhcqxQ");
//
// final UrlLauncher urlLauncher = RWT.getClient().getService(UrlLauncher.class);
// urlLauncher.openURL(
// "https://seb-jitsi.ethz.ch/TestRoomABC?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjb250ZXh0Ijp7InVzZXIiOnsiYXZhdGFyIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9qb2huLWRvZSIsIm5hbWUiOiJEaXNwbGF5IE5hbWUiLCJlbWFpbCI6Im5hbWVAZXhhbXBsZS5jb20ifX0sImF1ZCI6InNlYi1qaXRzaSIsImlzcyI6InNlYi1qaXRzaSIsInN1YiI6Im1lZXQuaml0c2kiLCJyb29tIjoiKiJ9.SD9Zs78mMFqxS1tpalPTykYYaubIYsj_406WAOhcqxQ");
// @formatter:off
private static final String OPEN_SINGEL_ROOM_SCRIPT =
"var existingWin = window.open('', '%s', 'height=420,width=620,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes');\n" +
"if(existingWin.location.href === 'about:blank'){\n" +
" existingWin.location.href = '%s/proctoring/%s';\n" +
" existingWin.focus();\n" +
"} else {\n" +
" existingWin.focus();\n" +
"}";
// @formatter:on
private PageAction openProctorScreen(final PageAction action, final String connectionToken) {
final SEBClientProctoringConnectionData proctoringConnectionData =
this.pageService.getRestService().getBuilder(GetProctorURLForClient.class)
.withURIVariable(API.PARAM_MODEL_ID, action.getEntityKey().modelId)
@ -293,20 +298,20 @@ public class MonitoringClientConnection implements TemplateComposer {
.call()
.getOrThrow();
final Encoder urlEncoder = Base64.getUrlEncoder().withoutPadding();
final String roomName = urlEncoder.encodeToString(Utils.toByteArray(connectionToken));
RWT.getUISession().getHttpSession().setAttribute(
ProctoringServlet.SESSION_ATTR_PROCTORING_DATA,
proctoringConnectionData);
final String webserviceServerAddress = this.pageService.getAuthorizationContextHolder()
.getWebserviceURIService()
.getWebserviceServerAddress();
final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
javaScriptExecutor.execute(
"window.open("
+ "'" + proctoringConnectionData.connectionURL + "',"
+ "'proctoring',"
+ "'height=400,width=800,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes');");
// final JavaScriptExecutor javaScriptExecutor = RWT.getClient().getService(JavaScriptExecutor.class);
// javaScriptExecutor.execute(
// "window.open("
// + "'http://localhost:8080/proctoring',"
// + "'proctoring',"
// + "'height=400,width=800,location=no,scrollbars=yes,status=no,menubar=yes,toolbar=yes,titlebar=yes');");
final String script = String.format(OPEN_SINGEL_ROOM_SCRIPT, roomName, webserviceServerAddress, roomName);
javaScriptExecutor.execute(script);
return action;
}

View file

@ -31,6 +31,7 @@ import ch.ethz.seb.sebserver.gbl.profile.WebServiceProfile;
import ch.ethz.seb.sebserver.gbl.util.Cryptor;
import ch.ethz.seb.sebserver.gbl.util.Result;
import ch.ethz.seb.sebserver.gbl.util.Utils;
import ch.ethz.seb.sebserver.webservice.servicelayer.authorization.AuthorizationService;
import ch.ethz.seb.sebserver.webservice.servicelayer.exam.ExamProctoringService;
import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService;
@ -45,13 +46,16 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
private static final String JITSI_ACCESS_TOKEN_PAYLOAD =
"{\"context\":{\"user\":{\"name\":\"%s\"}},\"iss\":\"%s\",\"aud\":\"%s\",\"sub\":\"%s\",\"room\":\"%s\"%s}";
private final AuthorizationService authorizationService;
private final ExamSessionService examSessionService;
private final Cryptor cryptor;
protected ExamJITSIProctoringService(
final AuthorizationService authorizationService,
final ExamSessionService examSessionService,
final Cryptor cryptor) {
this.authorizationService = authorizationService;
this.examSessionService = examSessionService;
this.cryptor = cryptor;
}
@ -113,7 +117,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
examProctoring.serverURL,
examProctoring.appKey,
examProctoring.getAppSecret(),
clientConnection.userSessionId,
this.authorizationService.getUserService().getCurrentUser().getUsername(),
(server) ? "seb-server" : "seb-client",
roomName,
expTime)
@ -154,6 +158,7 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
.append(token).toString();
return new SEBClientProctoringConnectionData(
host,
roomUrl,
roomName,
token,
@ -166,9 +171,12 @@ public class ExamJITSIProctoringService implements ExamProctoringService {
final String roomName) {
final StringBuilder builder = new StringBuilder();
return builder.append(url)
.append("/")
.append(roomName)
builder.append(url);
if (!url.endsWith(Constants.URL_PATH_SEPARATOR)) {
builder.append(Constants.URL_PATH_SEPARATOR);
}
return builder.append(roomName)
.toString();
}

View file

@ -16,6 +16,8 @@ import java.util.concurrent.ConcurrentHashMap;
import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.DateTimeZone;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Lazy;
@ -49,6 +51,8 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
private final WebserviceInfo webserviceInfo;
private final ClientConnectionDAO clientConnectionDAO;
private final ClientInstructionDAO clientInstructionDAO;
private final JSONMapper jsonMapper;
private final Map<String, ClientInstructionRecord> instructions;
private long lastRefresh = 0;
@ -56,11 +60,13 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
public SEBInstructionServiceImpl(
final WebserviceInfo webserviceInfo,
final ClientConnectionDAO clientConnectionDAO,
final ClientInstructionDAO clientInstructionDAO) {
final ClientInstructionDAO clientInstructionDAO,
final JSONMapper jsonMapper) {
this.webserviceInfo = webserviceInfo;
this.clientConnectionDAO = clientConnectionDAO;
this.clientInstructionDAO = clientInstructionDAO;
this.jsonMapper = jsonMapper;
this.instructions = new ConcurrentHashMap<>();
}
@ -101,15 +107,13 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
final boolean isActive = this.clientConnectionDAO
.isActiveConnection(examId, connectionToken)
.getOr(false);
if (isActive) {
try {
final String attributesString = new JSONMapper().writeValueAsString(attributes);
final String attributesString = this.jsonMapper.writeValueAsString(attributes);
this.clientInstructionDAO
.insert(examId, type, attributesString, connectionToken, needsConfirm)
.map(inst -> {
this.instructions.putIfAbsent(inst.getConnectionToken(), inst);
return inst;
})
.map(this::chacheInstruction)
.onError(error -> log.error("Failed to put instruction: ", error))
.getOrThrow();
} catch (final Exception e) {
@ -139,10 +143,10 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
.filter(activeConnections::contains)
.map(token -> this.clientInstructionDAO.insert(examId, type, attributesString, token, needsConfirm))
.map(result -> result.get(
error -> log.error("Failed to put instruction: ", error),
error -> log.error("Failed to register instruction: ", error),
() -> null))
.filter(Objects::nonNull)
.forEach(inst -> this.instructions.putIfAbsent(inst.getConnectionToken(), inst));
.forEach(this::chacheInstruction);
});
}
@ -229,4 +233,24 @@ public class SEBInstructionServiceImpl implements SEBInstructionService {
.forEach(inst -> this.instructions.putIfAbsent(inst.getConnectionToken(), inst)));
}
private ClientInstructionRecord chacheInstruction(final ClientInstructionRecord instruction) {
final String connectionToken = instruction.getConnectionToken();
if (this.instructions.containsKey(connectionToken)) {
// check if previous instruction is still valid
final ClientInstructionRecord clientInstructionRecord = this.instructions.get(connectionToken);
if (BooleanUtils.toBoolean(BooleanUtils.toBooleanObject(clientInstructionRecord.getNeedsConfirmation()))) {
// check if time is out
final long now = DateTime.now(DateTimeZone.UTC).getMillis();
final Long timestamp = clientInstructionRecord.getTimestamp();
if (timestamp != null && now - timestamp > Constants.MINUTE_IN_MILLIS) {
// remove old instruction and add new one
this.instructions.put(connectionToken, instruction);
}
}
} else {
this.instructions.put(connectionToken, instruction);
}
return instruction;
}
}

View file

@ -21,9 +21,10 @@ public class ExamJITSIProctoringServiceTest {
@Test
public void testCreateProctoringURL() {
Cryptor cryptorMock = Mockito.mock(Cryptor.class);
final Cryptor cryptorMock = Mockito.mock(Cryptor.class);
Mockito.when(cryptorMock.decrypt(Mockito.any())).thenReturn("fbvgeghergrgrthrehreg123");
final ExamJITSIProctoringService examJITSIProctoringService = new ExamJITSIProctoringService(null, cryptorMock);
final ExamJITSIProctoringService examJITSIProctoringService =
new ExamJITSIProctoringService(null, null, cryptorMock);
final SEBClientProctoringConnectionData data = examJITSIProctoringService.createProctoringConnectionData(
"https://seb-jitsi.example.ch",
"test-app",