diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java index 5e72f04b..84d3d16d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamJITSIProctoringService.java @@ -58,8 +58,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructio @WebServiceProfile public class ExamJITSIProctoringService implements ExamProctoringService { + private static final String TOKEN_ENCODE_ALG = "HmacSHA256"; private static final String SEB_SERVER_KEY = "seb-server"; - private static final String SEB_CLIENT_KEY = "seb-client"; private static final Logger log = LoggerFactory.getLogger(ExamJITSIProctoringService.class); @@ -384,9 +384,9 @@ public class ExamJITSIProctoringService implements ExamProctoringService { .encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8)); final String message = jwtHeaderPart + "." + jwtPayloadPart; - final Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); + final Mac sha256_HMAC = Mac.getInstance(TOKEN_ENCODE_ALG); final SecretKeySpec secret_key = - new SecretKeySpec(Utils.toByteArray(appSecret), "HmacSHA256"); + new SecretKeySpec(Utils.toByteArray(appSecret), TOKEN_ENCODE_ALG); sha256_HMAC.init(secret_key); final String hash = urlEncoder.encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message))); diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamZOOMProctoringService.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamZOOMProctoringService.java index eb0eb608..e73e6c9d 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamZOOMProctoringService.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/session/impl/ExamZOOMProctoringService.java @@ -8,32 +8,119 @@ package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; +import java.nio.charset.StandardCharsets; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.util.Base64; +import java.util.Base64.Encoder; import java.util.Collection; +import java.util.Map; +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Lazy; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import ch.ethz.seb.sebserver.ClientHttpRequestFactoryService; +import ch.ethz.seb.sebserver.gbl.Constants; +import ch.ethz.seb.sebserver.gbl.api.APIMessage; +import ch.ethz.seb.sebserver.gbl.api.APIMessage.APIMessageException; +import ch.ethz.seb.sebserver.gbl.api.APIMessage.FieldValidationException; +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.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; 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.session.ExamProctoringService; +import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; @Lazy @Service @WebServiceProfile public class ExamZOOMProctoringService implements ExamProctoringService { + private static final Logger log = LoggerFactory.getLogger(ExamZOOMProctoringService.class); + + private static final String API_TEST_ENDPOINT = "v2/users?status=active&page_size=30&page_number=1&data_type=Json"; + private static final String TOKEN_ENCODE_ALG = "HmacSHA256"; + + private static final String ZOOM_ACCESS_TOKEN_HEADER = + "{\"alg\":\"HS256\",\"typ\":\"JWT\"}"; + private static final String ZOOM_ACCESS_TOKEN_PAYLOAD = + "{\"iss\":\"%s\",\"exp\":%s}"; + + private final ExamSessionService examSessionService; + private final ClientHttpRequestFactoryService clientHttpRequestFactoryService; + private final Cryptor cryptor; + private final AsyncService asyncService; + private final JSONMapper jsonMapper; + + public ExamZOOMProctoringService( + final ExamSessionService examSessionService, + final ClientHttpRequestFactoryService clientHttpRequestFactoryService, + final Cryptor cryptor, + final AsyncService asyncService, + final JSONMapper jsonMapper) { + + this.examSessionService = examSessionService; + this.clientHttpRequestFactoryService = clientHttpRequestFactoryService; + this.cryptor = cryptor; + this.asyncService = asyncService; + this.jsonMapper = jsonMapper; + } + @Override public ProctoringServerType getType() { return ProctoringServerType.ZOOM; } @Override - public Result testExamProctoring(final ProctoringServiceSettings examProctoring) { - // TODO Auto-generated method stub - return null; + public Result testExamProctoring(final ProctoringServiceSettings proctoringSettings) { + return Result.tryCatch(() -> { + if (proctoringSettings.serverURL != null && proctoringSettings.serverURL.contains("?")) { + throw new FieldValidationException( + "serverURL", + "proctoringSettings:serverURL:invalidURL"); + } + + try { + + final String url = proctoringSettings.serverURL.endsWith(Constants.SLASH.toString()) + ? proctoringSettings.serverURL + API_TEST_ENDPOINT + : proctoringSettings.serverURL + "/" + API_TEST_ENDPOINT; + + final ResponseEntity result = new ZoomRestCallTemplate(proctoringSettings) + .callGET(url); + + if (result.getStatusCode() != HttpStatus.OK) { + throw new APIMessageException( + APIMessage.ErrorMessage.BINDING_ERROR, + String.valueOf(result.getStatusCode())); + } else { + System.out.println(result.getBody()); + } + } catch (final Exception e) { + throw new APIMessageException(APIMessage.ErrorMessage.BINDING_ERROR, e.getMessage()); + } + + return true; + }); } @Override @@ -65,4 +152,132 @@ public class ExamZOOMProctoringService implements ExamProctoringService { return null; } + protected String createJWT( + final String appKey, + final CharSequence appSecret, + final Long expTime) throws NoSuchAlgorithmException, InvalidKeyException { + + 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 = createPayload(appKey, 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(appSecret), + 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(); + } + + protected String createPayload( + final String clientKey, + final Long expTime) { + + return String.format( + ZOOM_ACCESS_TOKEN_PAYLOAD.replaceAll(" ", "").replaceAll("\n", ""), + clientKey, + expTime); + } + + private long forExam(final ProctoringServiceSettings examProctoring) { + if (examProctoring.examId == null) { + throw new IllegalStateException("Missing exam identifier from ExamProctoring data"); + } + + 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 ZoomRestCallTemplate { + + final ProctoringServiceSettings proctoringSettings; + final RestTemplate restTemplate; + final HttpHeaders httpHeaders; + + private final CircuitBreaker> circuitBreaker; + + public ZoomRestCallTemplate(final ProctoringServiceSettings proctoringSettings) + throws InvalidKeyException, NoSuchAlgorithmException { + + this.proctoringSettings = proctoringSettings; + this.restTemplate = new RestTemplate(ExamZOOMProctoringService.this.clientHttpRequestFactoryService + .getClientHttpRequestFactory() + .getOrThrow()); + + final String jwt = createJWT( + proctoringSettings.appKey, + proctoringSettings.appSecret, + System.currentTimeMillis() + Constants.MINUTE_IN_MILLIS); + + this.httpHeaders = new HttpHeaders(); + this.httpHeaders.set(HttpHeaders.AUTHORIZATION, "Bearer " + jwt); + this.httpHeaders.set(HttpHeaders.ACCEPT, MediaType.APPLICATION_JSON_VALUE); + + this.circuitBreaker = ExamZOOMProctoringService.this.asyncService.createCircuitBreaker( + 2, + 10 * Constants.SECOND_IN_MILLIS, + 10 * Constants.SECOND_IN_MILLIS); + } + + public ResponseEntity callGET(final String url) { + return exchange(null, url, null); + } + + public ResponseEntity callGET(final String url, final Map uriVariables) { + return exchange(null, url, uriVariables); + } + + private ResponseEntity exchange( + final Object body, + final String url, + final Map uriVariables) { + + final Result> protectedRunResult = this.circuitBreaker.protectedRun(() -> { + final HttpEntity httpEntity = (body != null) + ? new HttpEntity<>(body, this.httpHeaders) + : new HttpEntity<>(this.httpHeaders); + + final ResponseEntity result = (uriVariables != null) + ? this.restTemplate.exchange( + url, + HttpMethod.GET, + httpEntity, + String.class, + uriVariables) + : this.restTemplate.exchange( + url, + HttpMethod.GET, + httpEntity, + String.class); + + if (result.getStatusCode() != HttpStatus.OK) { + log.warn("Zoom API call to {} respond not 200 -> {}", url, result.getStatusCode()); + } + + return result; + }); + return protectedRunResult.getOrThrow(); + } + } + }