Zoom integration
This commit is contained in:
		
							parent
							
								
									3dddaf9051
								
							
						
					
					
						commit
						b0dd0e1afc
					
				
					 2 changed files with 221 additions and 6 deletions
				
			
		|  | @ -58,8 +58,8 @@ import ch.ethz.seb.sebserver.webservice.servicelayer.session.SEBClientInstructio | ||||||
| @WebServiceProfile | @WebServiceProfile | ||||||
| public class ExamJITSIProctoringService implements ExamProctoringService { | 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_SERVER_KEY = "seb-server"; | ||||||
| 
 |  | ||||||
|     private static final String SEB_CLIENT_KEY = "seb-client"; |     private static final String SEB_CLIENT_KEY = "seb-client"; | ||||||
| 
 | 
 | ||||||
|     private static final Logger log = LoggerFactory.getLogger(ExamJITSIProctoringService.class); |     private static final Logger log = LoggerFactory.getLogger(ExamJITSIProctoringService.class); | ||||||
|  | @ -384,9 +384,9 @@ public class ExamJITSIProctoringService implements ExamProctoringService { | ||||||
|                 .encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8)); |                 .encodeToString(jwtPayload.getBytes(StandardCharsets.UTF_8)); | ||||||
|         final String message = jwtHeaderPart + "." + jwtPayloadPart; |         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 = |         final SecretKeySpec secret_key = | ||||||
|                 new SecretKeySpec(Utils.toByteArray(appSecret), "HmacSHA256"); |                 new SecretKeySpec(Utils.toByteArray(appSecret), TOKEN_ENCODE_ALG); | ||||||
|         sha256_HMAC.init(secret_key); |         sha256_HMAC.init(secret_key); | ||||||
|         final String hash = urlEncoder.encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message))); |         final String hash = urlEncoder.encodeToString(sha256_HMAC.doFinal(Utils.toByteArray(message))); | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -8,32 +8,119 @@ | ||||||
| 
 | 
 | ||||||
| package ch.ethz.seb.sebserver.webservice.servicelayer.session.impl; | 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.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.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.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.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.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.Result; | 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.ExamProctoringService; | ||||||
|  | import ch.ethz.seb.sebserver.webservice.servicelayer.session.ExamSessionService; | ||||||
| 
 | 
 | ||||||
| @Lazy | @Lazy | ||||||
| @Service | @Service | ||||||
| @WebServiceProfile | @WebServiceProfile | ||||||
| public class ExamZOOMProctoringService implements ExamProctoringService { | 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 |     @Override | ||||||
|     public ProctoringServerType getType() { |     public ProctoringServerType getType() { | ||||||
|         return ProctoringServerType.ZOOM; |         return ProctoringServerType.ZOOM; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     @Override |     @Override | ||||||
|     public Result<Boolean> testExamProctoring(final ProctoringServiceSettings examProctoring) { |     public Result<Boolean> testExamProctoring(final ProctoringServiceSettings proctoringSettings) { | ||||||
|         // TODO Auto-generated method stub |         return Result.tryCatch(() -> { | ||||||
|         return null; |             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<String> 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 |     @Override | ||||||
|  | @ -65,4 +152,132 @@ public class ExamZOOMProctoringService implements ExamProctoringService { | ||||||
|         return null; |         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<ResponseEntity<String>> 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<String> callGET(final String url) { | ||||||
|  |             return exchange(null, url, null); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         public ResponseEntity<String> callGET(final String url, final Map<String, ?> uriVariables) { | ||||||
|  |             return exchange(null, url, uriVariables); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private ResponseEntity<String> exchange( | ||||||
|  |                 final Object body, | ||||||
|  |                 final String url, | ||||||
|  |                 final Map<String, ?> uriVariables) { | ||||||
|  | 
 | ||||||
|  |             final Result<ResponseEntity<String>> protectedRunResult = this.circuitBreaker.protectedRun(() -> { | ||||||
|  |                 final HttpEntity<Object> httpEntity = (body != null) | ||||||
|  |                         ? new HttpEntity<>(body, this.httpHeaders) | ||||||
|  |                         : new HttpEntity<>(this.httpHeaders); | ||||||
|  | 
 | ||||||
|  |                 final ResponseEntity<String> 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(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		
		Reference in a new issue
	
	 anhefti
						anhefti