/* * Copyright (c) 2024 ETH Zürich, IT Services * * 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/. */ using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using SafeExamBrowser.Logging.Contracts; using SafeExamBrowser.Server.Contracts.Data; using SafeExamBrowser.Server.Contracts.Events.Proctoring; using SafeExamBrowser.Server.Data; using SafeExamBrowser.Server.Requests; namespace SafeExamBrowser.Server { internal class Parser { private readonly ILogger logger; internal Parser(ILogger logger) { this.logger = logger; } internal bool IsTokenExpired(HttpContent content) { var isExpired = false; try { var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; var error = json["error"].Value(); isExpired = error?.Equals("invalid_token", StringComparison.OrdinalIgnoreCase) == true; } catch (Exception e) { logger.Error("Failed to parse token expiration content!", e); } return isExpired; } internal bool TryParseApi(HttpContent content, out ApiVersion1 api) { var success = false; api = new ApiVersion1(); try { var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; var apisJson = json["api-versions"]; foreach (var apiJson in apisJson.AsJEnumerable()) { if (apiJson["name"].Value().Equals("v1")) { foreach (var endpoint in apiJson["endpoints"].AsJEnumerable()) { var name = endpoint["name"].Value(); var location = endpoint["location"].Value(); switch (name) { case "access-token-endpoint": api.AccessTokenEndpoint = location; break; case "seb-configuration-endpoint": api.ConfigurationEndpoint = location; break; case "seb-handshake-endpoint": api.HandshakeEndpoint = location; break; case "seb-log-endpoint": api.LogEndpoint = location; break; case "seb-ping-endpoint": api.PingEndpoint = location; break; } } success = true; } if (!success) { logger.Error("The selected SEB server instance does not support the required API version!"); } } } catch (Exception e) { logger.Error("Failed to parse server API!", e); } return success; } internal bool TryParseAppSignatureKeySalt(HttpResponseMessage response, out string salt) { salt = default; try { var hasHeader = response.Headers.TryGetValues(Header.APP_SIGNATURE_KEY_SALT, out var values); if (hasHeader) { salt = values.First(); } } catch (Exception e) { logger.Error("Failed to parse app signature key salt!", e); } return salt != default; } internal bool TryParseBrowserExamKey(HttpResponseMessage response, out string browserExamKey) { browserExamKey = default; try { var hasHeader = response.Headers.TryGetValues(Header.BROWSER_EXAM_KEY, out var values); if (hasHeader) { browserExamKey = values.First(); } } catch (Exception e) { logger.Error("Failed to parse browser exam key!", e); } return browserExamKey != default; } internal bool TryParseConnectionToken(HttpResponseMessage response, out string connectionToken) { connectionToken = default; try { var hasHeader = response.Headers.TryGetValues(Header.CONNECTION_TOKEN, out var values); if (hasHeader) { connectionToken = values.First(); } else { logger.Error("Failed to retrieve connection token!"); } } catch (Exception e) { logger.Error("Failed to parse connection token!", e); } return connectionToken != default; } internal bool TryParseExams(HttpContent content, out IEnumerable exams) { var list = new List(); try { var json = JsonConvert.DeserializeObject(Extract(content)) as JArray; foreach (var exam in json.AsJEnumerable()) { list.Add(new Exam { Id = exam["examId"].Value(), LmsName = exam["lmsType"].Value(), Name = exam["name"].Value(), Url = exam["url"].Value() }); } } catch (Exception e) { logger.Error("Failed to parse exams!", e); } exams = list; return exams.Any(); } internal bool TryParseInstruction(HttpContent content, out Attributes attributes, out string instruction, out string instructionConfirmation) { attributes = new Attributes(); instruction = default; instructionConfirmation = default; try { var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; if (json != default(JObject)) { instruction = json["instruction"].Value(); if (json.ContainsKey("attributes")) { var attributesJson = json["attributes"] as JObject; if (attributesJson.ContainsKey("instruction-confirm")) { instructionConfirmation = attributesJson["instruction-confirm"].Value(); } attributes = ParseAttributes(attributesJson, instruction); } } } catch (Exception e) { logger.Error("Failed to parse instruction!", e); } return instruction != default; } internal bool TryParseOauth2Token(HttpContent content, out string oauth2Token) { oauth2Token = default; try { var json = JsonConvert.DeserializeObject(Extract(content)) as JObject; oauth2Token = json["access_token"].Value(); } catch (Exception e) { logger.Error("Failed to parse Oauth2 token!", e); } return oauth2Token != default; } private Attributes ParseAttributes(JObject attributesJson, string instruction) { var attributes = new Attributes(); switch (instruction) { case Instructions.LOCK_SCREEN: ParseLockScreenInstruction(attributes, attributesJson); break; case Instructions.NOTIFICATION_CONFIRM: ParseNotificationConfirmation(attributes, attributesJson); break; case Instructions.PROCTORING: ParseProctoringInstruction(attributes, attributesJson); break; case Instructions.PROCTORING_RECONFIGURATION: ParseReconfigurationInstruction(attributes, attributesJson); break; } return attributes; } private void ParseLockScreenInstruction(Attributes attributes, JObject attributesJson) { if (attributesJson.ContainsKey("message")) { attributes.Message = attributesJson["message"].Value(); } } private void ParseNotificationConfirmation(Attributes attributes, JObject attributesJson) { if (attributesJson.ContainsKey("id")) { attributes.Id = attributesJson["id"].Value(); } if (attributesJson.ContainsKey("type")) { switch (attributesJson["type"].Value()) { case "lockscreen": attributes.Type = AttributeType.LockScreen; break; case "raisehand": attributes.Type = AttributeType.Hand; break; } } } private void ParseProctoringInstruction(Attributes attributes, JObject attributesJson) { var provider = attributesJson["service-type"].Value(); switch (provider) { case "JITSI_MEET": attributes.Instruction = ParseJitsiMeetInstruction(attributesJson); break; case "SCREEN_PROCTORING": attributes.Instruction = ParseScreenProctoringInstruction(attributesJson); break; case "ZOOM": attributes.Instruction = ParseZoomInstruction(attributesJson); break; } if (attributes.Instruction != default) { attributes.Instruction.Method = attributesJson["method"].Value() == "JOIN" ? InstructionMethod.Join : InstructionMethod.Leave; } } private JitsiMeetInstruction ParseJitsiMeetInstruction(JObject attributesJson) { return new JitsiMeetInstruction { RoomName = attributesJson["jitsiMeetRoom"].Value(), ServerUrl = attributesJson["jitsiMeetServerURL"].Value(), Token = attributesJson["jitsiMeetToken"].Value() }; } private ScreenProctoringInstruction ParseScreenProctoringInstruction(JObject attributesJson) { return new ScreenProctoringInstruction { ClientId = attributesJson["screenProctoringClientId"].Value(), ClientSecret = attributesJson["screenProctoringClientSecret"].Value(), GroupId = attributesJson["screenProctoringGroupId"].Value(), ServiceUrl = attributesJson["screenProctoringServiceURL"].Value(), SessionId = attributesJson["screenProctoringClientSessionId"].Value() }; } private ZoomInstruction ParseZoomInstruction(JObject attributesJson) { return new ZoomInstruction { MeetingNumber = attributesJson["zoomRoom"].Value(), Password = attributesJson["zoomMeetingKey"].Value(), SdkKey = attributesJson["zoomAPIKey"].Value(), Signature = attributesJson["zoomToken"].Value(), Subject = attributesJson["zoomSubject"].Value(), UserName = attributesJson["zoomUserName"].Value() }; } private void ParseReconfigurationInstruction(Attributes attributes, JObject attributesJson) { if (attributesJson.ContainsKey("jitsiMeetFeatureFlagChat")) { attributes.AllowChat = attributesJson["jitsiMeetFeatureFlagChat"].Value(); } if (attributesJson.ContainsKey("zoomFeatureFlagChat")) { attributes.AllowChat = attributesJson["zoomFeatureFlagChat"].Value(); } if (attributesJson.ContainsKey("jitsiMeetReceiveAudio")) { attributes.ReceiveAudio = attributesJson["jitsiMeetReceiveAudio"].Value(); } if (attributesJson.ContainsKey("zoomReceiveAudio")) { attributes.ReceiveAudio = attributesJson["zoomReceiveAudio"].Value(); } if (attributesJson.ContainsKey("jitsiMeetReceiveVideo")) { attributes.ReceiveVideo = attributesJson["jitsiMeetReceiveVideo"].Value(); } if (attributesJson.ContainsKey("zoomReceiveVideo")) { attributes.ReceiveVideo = attributesJson["zoomReceiveVideo"].Value(); } } private string Extract(HttpContent content) { var task = Task.Run(async () => { return await content.ReadAsStreamAsync(); }); var stream = task.GetAwaiter().GetResult(); var reader = new StreamReader(stream); return reader.ReadToEnd(); } } }