Pārlūkot izejas kodu

登录页-远程桌面软件和虚拟摄像头检测

Michael Wang 3 gadi atpakaļ
vecāks
revīzija
289e00a8ac

+ 46 - 0
src/constants/constant-namelist.ts

@@ -0,0 +1,46 @@
+export const VCAM_LIST = [
+  "17GuaGua Cam",
+  "91KBOX",
+  // "ASUS Virtual Camera",
+  "e2eSoft iVCam",
+  "e2eSoft VCam",
+  "FaceRig Virtual Camera",
+  // "Lenovo Virtual Camera",
+  "MagicCamera Capture",
+  "MeiSe",
+  "Virtual Cam",
+  "YY伴侣",
+  "WebcamMax Capture",
+  "Wecam",
+  "Vcam ",
+  "softcam",
+  "Vandate Virtual Camera",
+  "video2webcam",
+  "VCDCut Pro",
+  "9158虚拟视频",
+  "9158Capture",
+  "Insta360 Virtual Camera",
+  "无他直播伴侣PC客户端",
+  "无他相机电脑版",
+  "mobiola webcamera",
+  "艾米秀宝(ImiShowBox)",
+  "video2Webcam",
+  "飞翔虚拟视频",
+  "魔力秀",
+  "YY开播",
+  "无他伴侣",
+  "视频连麦",
+  "酷狗直播伴侣",
+  "screen-capture-recorder",
+  "OBS Virtual Camera",
+  "OBS-Camera",
+  "ManyCam Virtual Webcam",
+  "小葫芦直播助手",
+  "yyplayer",
+  "秀色直播伴侣",
+  "秀色直播助手",
+  "Citrix HDX Web Camera",
+];
+
+export const REMOTE_APP_NAME =
+  "qq;teamviewer;lookmypc;xt;winaw32;pcaquickconnect;sessioncontroller;sunloginclient;sunloginremote;wemeetapp;wechat";

+ 11 - 4
src/features/UserLogin/UserLogin.vue

@@ -21,9 +21,12 @@ import GlobalNotice from "./GlobalNotice.vue";
 import { useAppVersion } from "./useAppVersion";
 import { getElectronConfig } from "./useElectronConfig";
 import { checkExamInProgress } from "./useExamInProgress";
+import { useExamShellStats } from "./useExamShellStats";
 import { getGeeTestConfig } from "./useGeeTestConfig";
 import { limitLogin } from "./useLimitLogin";
 import { useNewVersion } from "./useNewVersion";
+import { useRemoteAppChecker } from "./useRemoteAppChecker";
+import { useVCamChecker } from "./useVCamChecker";
 
 logger({
   cnl: ["console", "local", "server"],
@@ -31,6 +34,8 @@ logger({
   act: "首次渲染",
 });
 
+useExamShellStats();
+
 //#region cache faceapi json
 {
   const begin = Date.now();
@@ -81,13 +86,15 @@ const allowLoginType = $computed(() => QECSConfig.LOGIN_TYPE ?? []);
 const { newVersionAvailable, checkNewVersion } = useNewVersion();
 const { disableLoginBtnBecauseAppVersionChecker } =
   useAppVersion(newVersionAvailable);
+const { disableLoginBtnBecauseVCam } = useVCamChecker();
+const { disableLoginBtnBecauseRemoteApp } = useRemoteAppChecker();
 
 let disableLoginBtn = $computed(
   () =>
-    disableLoginBtnBecauseNotTimeout ||
-    (!import.meta.env.DEV &&
-      //   (this.disableLoginBtnBecauseRemoteApp ||
-      //     this.disableLoginBtnBecauseVCam)) ||
+    !import.meta.env.DEV &&
+    (disableLoginBtnBecauseNotTimeout ||
+      disableLoginBtnBecauseRemoteApp.value ||
+      disableLoginBtnBecauseVCam.value ||
       disableLoginBtnBecauseAppVersionChecker.value)
   // this.disableLoginBtnBecauseRefreshServiceWorker ||
   // this.disableLoginBtnBecauseNotAllowedNative

+ 2 - 2
src/features/UserLogin/useAppVersion.ts

@@ -1,6 +1,6 @@
 import { DOMAIN, STRICT_CHECK_HOSTS } from "@/constants/constants";
+import { checkMainExe, fileExists, isElectron } from "@/utils/nativeMethods";
 import ua from "@/utils/ua";
-import { checkMainExe, existsSync, isElectron } from "@/utils/utils";
 import { onMounted, Ref, watch } from "vue";
 
 export function useAppVersion(newVersionAvailable: Ref<boolean>) {
@@ -30,7 +30,7 @@ export function useAppVersion(newVersionAvailable: Ref<boolean>) {
         "swjtu.ecs.qmth.com.cn",
       ].includes(DOMAIN)
     ) {
-      if (!isElectron() || !existsSync("multiCamera.exe")) {
+      if (!isElectron() || !fileExists("multiCamera.exe")) {
         disableLoginBtnBecauseAppVersionChecker = true;
         $message.error("请与学校申请最新的客户端,进行考试!", {
           duration: 2 * 24 * 60 * 60,

+ 33 - 0
src/features/UserLogin/useExamShellStats.ts

@@ -0,0 +1,33 @@
+export function useExamShellStats() {
+  const shellVersion = window.navigator.userAgent
+    .split(" ")
+    .find((v) => v.startsWith("electron-exam-shell/"));
+  const chromeVersion = window.navigator.userAgent
+    .split(" ")
+    .find((v) => v.startsWith("Chrome/"));
+  if (shellVersion) {
+    _hmt.push([
+      "_trackEvent",
+      "登录页面",
+      "学生端版本",
+      shellVersion + "/" + chromeVersion,
+    ]);
+  } else {
+    _hmt.push(["_trackEvent", "登录页面", "浏览器登录"]);
+    if (chromeVersion) {
+      _hmt.push([
+        "_trackEvent",
+        "登录页面",
+        "学生端版本/chrome",
+        chromeVersion,
+      ]);
+    } else {
+      _hmt.push([
+        "_trackEvent",
+        "登录页面",
+        "学生端版本/其他浏览器",
+        window.navigator.userAgent,
+      ]);
+    }
+  }
+}

+ 116 - 0
src/features/UserLogin/useRemoteAppChecker.ts

@@ -0,0 +1,116 @@
+import { REMOTE_APP_NAME } from "@/constants/constant-namelist";
+import { store } from "@/store/store";
+import {
+  execLocal,
+  fileExists,
+  nodeCheckRemoteDesktop,
+} from "@/utils/nativeMethods";
+import { watch } from "vue";
+
+export function useRemoteAppChecker() {
+  /** 检测出错则提示;检测通过则将禁用登录按钮的flag置为false */
+  async function checkRemoteAppTxt() {
+    let applicationNames;
+    try {
+      const fs: typeof import("fs") = window.nodeRequire("fs");
+      try {
+        applicationNames = fs.readFileSync("remoteApplication.txt", "utf-8");
+      } catch (error) {
+        logger({
+          cnl: ["local", "server"],
+          key: "checkRemoteAppTxt",
+          pgu: "AUTO",
+          dtl: "remoteApplication.txt出错--0",
+          ejn: JSON.stringify(error),
+        });
+        await new Promise((resolve2) => setTimeout(resolve2, 3000));
+        applicationNames = fs.readFileSync("remoteApplication.txt", "utf-8");
+      }
+    } catch (error) {
+      logger({
+        cnl: ["local", "server"],
+        key: "checkRemoteAppTxt",
+        pgu: "AUTO",
+        stk: error instanceof Error ? error.message : JSON.stringify(error),
+        dtl: "读取remoteApplication.txt出错",
+        ext: { errorType: "e-01", applicationNames },
+      });
+      $message.error("系统检测出错(e-01),请退出程序后重试!", {
+        duration: 2 * 24 * 60 * 60,
+      });
+      return;
+    }
+
+    const hasSun = nodeCheckRemoteDesktop();
+    if (hasSun) {
+      if (applicationNames) {
+        applicationNames += ",sunloginclient";
+      } else {
+        applicationNames = "sunloginclient";
+      }
+    }
+    if (!applicationNames?.trim()) {
+      disableLoginBtnBecauseRemoteApp = false;
+    } else {
+      let names = applicationNames
+        .replace("qq", "QQ")
+        .replace("teamviewer", "TeamViewer")
+        .replace("lookmypc", "LookMyPC")
+        .replace("xt", "协通")
+        .replace("winaw32", "Symantec PCAnywhere")
+        .replace("pcaquickconnect", "Symantec PCAnywhere")
+        .replace("sessioncontroller", "Symantec PCAnywhere")
+        .replace(/sunloginclient/gi, "向日葵")
+        .replace(/sunloginremote/gi, "向日葵")
+        .replace(/选择免安装运行,截图识别/gi, "向日葵")
+        .replace("wemeetapp", "腾讯会议")
+        .replace("wechat", "微信");
+
+      names = [...new Set(names.split(",").map((v) => v.trim()))].join(",");
+      $message.info("在考试期间,请关掉" + names + "软件,诚信考试。", {
+        duration: 2 * 24 * 60 * 60,
+        closable: false,
+      });
+    }
+  }
+
+  let disableLoginBtnBecauseRemoteApp = $ref(true);
+
+  const QECSConfig = store.QECSConfig;
+  watch(QECSConfig, async () => {
+    if (
+      !QECSConfig.PREVENT_CHEATING_CONFIG.includes("DISABLE_REMOTE_ASSISTANCE")
+    ) {
+      disableLoginBtnBecauseRemoteApp = false;
+      return;
+    }
+
+    if (import.meta.env.DEV) return;
+
+    let exe = "Project1.exe";
+    if (fileExists("Project2.exe")) {
+      const remoteAppName = REMOTE_APP_NAME;
+      exe = `Project2.exe "${remoteAppName}" `;
+    }
+
+    const fs: typeof import("fs") = window.nodeRequire("fs");
+    try {
+      fileExists("remoteApplication.txt") &&
+        fs.unlinkSync("remoteApplication.txt");
+    } catch (error) {
+      console.log(error);
+      logger({
+        cnl: ["local", "server"],
+        key: "checkRemoteAppTxt",
+        dtl: "unlink remoteApplication.txt 失败",
+      });
+    }
+    await execLocal(exe);
+
+    await checkRemoteAppTxt();
+  });
+
+  return {
+    disableLoginBtnBecauseRemoteApp: $$(disableLoginBtnBecauseRemoteApp),
+  };
+}

+ 91 - 0
src/features/UserLogin/useVCamChecker.ts

@@ -0,0 +1,91 @@
+import { VCAM_LIST } from "@/constants/constant-namelist";
+import { store } from "@/store/store";
+import { execLocal, fileExists } from "@/utils/nativeMethods";
+import { watch } from "vue";
+
+export function useVCamChecker() {
+  /** 检测出错则提示;检测通过则将禁用登录按钮的flag置为false */
+  async function checkVCamTxt() {
+    let cameraInfo;
+    try {
+      const fs: typeof import("fs") = window.nodeRequire("fs");
+      try {
+        cameraInfo = fs.readFileSync("CameraInfo.txt", "utf-8");
+      } catch (error) {
+        logger({
+          cnl: ["local", "server"],
+          key: "checkVCamTxt",
+          pgu: "AUTO",
+          dtl: "CameraInfo.txt出错--0",
+          ejn: JSON.stringify(error),
+        });
+        await new Promise((resolve2) => setTimeout(resolve2, 3000));
+        cameraInfo = fs.readFileSync("CameraInfo.txt", "utf-8");
+      }
+    } catch (error) {
+      logger({
+        cnl: ["local", "server"],
+        key: "checkVCamTxt",
+        pgu: "AUTO",
+        stk: error instanceof Error ? error.message : JSON.stringify(error),
+        dtl: "读取CameraInfo.txt出错",
+        ext: { errorType: "e-02", applicationNames: cameraInfo },
+      });
+      $message.error("系统检测出错(e-02),请退出程序后重试!", {
+        duration: 2 * 24 * 60 * 60,
+      });
+      return;
+    }
+    let found = false;
+    if (cameraInfo && cameraInfo.trim()) {
+      for (const vc of VCAM_LIST) {
+        if (cameraInfo.toUpperCase().includes(vc.toUpperCase())) {
+          found = true;
+          $message.info("在考试期间,请关掉虚拟摄像头软件,诚信考试。", {
+            duration: 2 * 24 * 60 * 60,
+          });
+          logger({
+            cnl: ["local", "server"],
+            key: "checkVCamTxt",
+            pgu: "AUTO",
+            dtl: "提示CameraInfo.txt成功",
+            ext: { applicationNames: cameraInfo },
+          });
+        }
+      }
+    }
+    disableLoginBtnBecauseVCam = found;
+  }
+
+  let disableLoginBtnBecauseVCam = $ref(true);
+
+  const QECSConfig = store.QECSConfig;
+  watch(QECSConfig, async () => {
+    if (
+      !QECSConfig.PREVENT_CHEATING_CONFIG.includes("DISABLE_VIRTUAL_CAMERA")
+    ) {
+      disableLoginBtnBecauseVCam = false;
+      return;
+    }
+
+    if (import.meta.env.DEV) return;
+
+    const fs: typeof import("fs") = window.nodeRequire("fs");
+    try {
+      fileExists("CameraInfo.txt") && fs.unlinkSync("CameraInfo.txt");
+    } catch (error) {
+      console.log(error);
+      logger({
+        cnl: ["local", "server"],
+        key: "checkVCamTxt",
+        dtl: "unlink CameraInfo.txt 失败",
+      });
+    }
+
+    await execLocal("multiCamera.exe");
+
+    await checkVCamTxt();
+  });
+
+  return { disableLoginBtnBecauseVCam: $$(disableLoginBtnBecauseVCam) };
+}

+ 94 - 24
src/utils/nativeExe.ts → src/utils/nativeMethods.ts

@@ -1,10 +1,15 @@
+import { HOST_FILE_HASH_MAP } from "@/constants/constants";
 import { xor } from "lodash-es";
 
-export default function checkRemote(
-  exeName: string,
-  cb: () => Promise<void>
-): Promise<void> {
-  if (typeof window.nodeRequire == "undefined") {
+export function isElectron() {
+  return typeof window.nodeRequire != "undefined";
+}
+
+const fs: typeof import("fs") = isElectron() && window.nodeRequire("fs");
+
+/** 执行本地exe文件 */
+export function execLocal(exeName: string): Promise<void> {
+  if (isElectron()) {
     logger({
       pgu: "AUTO",
       cnl: ["local", "server"],
@@ -50,7 +55,7 @@ export default function checkRemote(
             );
             if (fs.existsSync(absPath)) {
               try {
-                await checkRemote([absPath, exeParams].join(" ").trim(), cb);
+                await execLocal([absPath, exeParams].join(" ").trim());
               } catch (error) {
                 console.log("second try error", absPath);
                 logger({
@@ -60,25 +65,13 @@ export default function checkRemote(
               }
             }
           }
-          await new Promise<void>((resolve2) =>
-            setTimeout(() => resolve2(), 1000)
-          );
-          try {
-            await cb();
-          } catch (e) {
-            console.log("call cb failed", e);
-            logger({
-              cnl: ["local", "server"],
-              dtl: "call cb failed: " + e,
-            });
-          } finally {
-            resolve();
-          }
+          resolve();
         }
       );
   });
 }
 
+/** 文件路径是否存在 */
 export function fileExists(file: string): boolean {
   if (typeof window.nodeRequire == "undefined") {
     logger({
@@ -88,11 +81,11 @@ export function fileExists(file: string): boolean {
     });
     throw new Error("不在Electron中,调用 fs 失败");
   }
-  // eslint-disable-next-line
-  return window.nodeRequire("fs").existsSync(file);
+  return fs.existsSync(file);
 }
 
-export function nodeCheckRemoteDesktop() {
+/** 检测当前运行的进程中是否有“向日葵” */
+export function nodeCheckRemoteDesktop(): boolean {
   logger({
     pgu: "AUTO",
     cnl: ["local", "server"],
@@ -111,7 +104,8 @@ export function nodeCheckRemoteDesktop() {
 }
 
 let previousAppList: string[] = [];
-export function nodeCheckProcess() {
+/** 将电脑运行的进程记录到日志 */
+export function nodeCheckProcess(): void {
   try {
     // eslint-disable-next-line @typescript-eslint/no-unsafe-call
     const appListCP: string = window
@@ -141,3 +135,79 @@ export function nodeCheckProcess() {
     });
   }
 }
+
+/** 检测当前程序包的完整性。 */
+export function checkMainExe(): boolean {
+  try {
+    let iid: string = window.nodeRequire("process").pid;
+    const cp: typeof import("child_process") =
+      window.nodeRequire("child_process");
+    iid = cp
+      .execSync(
+        `cmd /c chcp 65001>nul && C:\\Windows\\System32\\wbem\\wmic process where ^(processid^=${iid}^) get parentprocessid /value`
+      )
+      .toString();
+    iid = iid.replace("ParentProcessId=", "").trim();
+    console.log(iid);
+    logger({
+      cnl: ["local", "console", "server"],
+      key: "checkMainExe",
+      dtl: `iid1: ${iid}`,
+    });
+    iid = cp
+      .execSync(
+        `cmd /c chcp 65001>nul && C:\\Windows\\System32\\wbem\\wmic process where ^(processid^=${iid}^) get parentprocessid /value`
+      )
+      .toString();
+    iid = iid.replace("ParentProcessId=", "").trim();
+    logger({
+      cnl: ["local", "console", "server"],
+      key: "checkMainExe",
+      dtl: `iid2: ${iid}`,
+    });
+
+    const executablePathBuffer = cp.execSync(
+      `cmd /c chcp 65001>nul && C:\\Windows\\System32\\wbem\\wmic process where ^(processid^=${iid}^) get executablepath /value`
+    );
+    console.log(executablePathBuffer);
+    const encoding = window.nodeRequire("encoding");
+    // eslint-disable-next-line @typescript-eslint/no-unsafe-call
+    let executablePath: string = encoding
+      .convert(executablePathBuffer, "utf8", "gbk")
+      .toString();
+    logger({
+      cnl: ["local", "console", "server"],
+      key: "checkMainExe",
+      dtl: executablePath,
+    });
+    executablePath = executablePath
+      .replace("ExecutablePath=", "")
+      .trim()
+      .replace(/&amp;/g, "&");
+    if (executablePath === eval(`process.env.PORTABLE_EXECUTABLE_FILE`)) {
+      const crypto: typeof import("crypto") = window.nodeRequire("crypto");
+      const getHash = crypto
+        .createHmac("sha256", "abcdefg")
+        .update(fs.readFileSync(executablePath))
+        .digest("hex");
+      console.log("the hash: ", getHash);
+      logger({
+        cnl: ["local", "console", "server"],
+        key: "checkMainExe",
+        dtl: `the hash: ${getHash}`,
+      });
+      if (HOST_FILE_HASH_MAP.get(window.location.hostname) === getHash) {
+        return true;
+      }
+    }
+    // check filepath executablePath md5
+  } catch (error) {
+    console.log(error);
+    logger({
+      cnl: ["local", "console", "server"],
+      key: "checkMainExe",
+      ejn: JSON.stringify(error),
+    });
+  }
+  return false;
+}

+ 0 - 87
src/utils/utils.ts

@@ -1,93 +1,6 @@
-import { HOST_FILE_HASH_MAP } from "@/constants/constants";
-
 export function setUUID() {
   if (!localStorage.getItem("uuidForEcs")) {
     const uuidForEcs = "" + Date.now() + Math.random();
     localStorage.setItem("uuidForEcs", uuidForEcs);
   }
 }
-
-export function isElectron() {
-  return typeof window.nodeRequire != "undefined";
-}
-
-export function existsSync(path: string) {
-  const fs: typeof import("fs") = window.nodeRequire("fs");
-  return fs.existsSync(path);
-}
-
-export function checkMainExe() {
-  try {
-    let iid: string = window.nodeRequire("process").pid;
-    const cp: typeof import("child_process") =
-      window.nodeRequire("child_process");
-    iid = cp
-      .execSync(
-        `cmd /c chcp 65001>nul && C:\\Windows\\System32\\wbem\\wmic process where ^(processid^=${iid}^) get parentprocessid /value`
-      )
-      .toString();
-    iid = iid.replace("ParentProcessId=", "").trim();
-    console.log(iid);
-    logger({
-      cnl: ["local", "console", "server"],
-      key: "checkMainExe",
-      dtl: `iid1: ${iid}`,
-    });
-    iid = cp
-      .execSync(
-        `cmd /c chcp 65001>nul && C:\\Windows\\System32\\wbem\\wmic process where ^(processid^=${iid}^) get parentprocessid /value`
-      )
-      .toString();
-    iid = iid.replace("ParentProcessId=", "").trim();
-    logger({
-      cnl: ["local", "console", "server"],
-      key: "checkMainExe",
-      dtl: `iid2: ${iid}`,
-    });
-
-    const executablePathBuffer = cp.execSync(
-      `cmd /c chcp 65001>nul && C:\\Windows\\System32\\wbem\\wmic process where ^(processid^=${iid}^) get executablepath /value`
-    );
-    console.log(executablePathBuffer);
-    const encoding = window.nodeRequire("encoding");
-    // eslint-disable-next-line @typescript-eslint/no-unsafe-call
-    let executablePath: string = encoding
-      .convert(executablePathBuffer, "utf8", "gbk")
-      .toString();
-    logger({
-      cnl: ["local", "console", "server"],
-      key: "checkMainExe",
-      dtl: executablePath,
-    });
-    executablePath = executablePath
-      .replace("ExecutablePath=", "")
-      .trim()
-      .replace(/&amp;/g, "&");
-    if (executablePath === eval(`process.env.PORTABLE_EXECUTABLE_FILE`)) {
-      const crypto: typeof import("crypto") = window.nodeRequire("crypto");
-      const fs: typeof import("fs") = window.nodeRequire("fs");
-      const getHash = crypto
-        .createHmac("sha256", "abcdefg")
-        .update(fs.readFileSync(executablePath))
-        .digest("hex");
-      console.log("the hash: ", getHash);
-      logger({
-        cnl: ["local", "console", "server"],
-        key: "checkMainExe",
-        dtl: `the hash: ${getHash}`,
-      });
-      if (HOST_FILE_HASH_MAP.get(window.location.hostname) === getHash) {
-        return true;
-      }
-    }
-    // check filepath executablePath md5
-  } catch (error) {
-    console.log(error);
-    logger({
-      cnl: ["local", "console", "server"],
-      key: "checkMainExe",
-      ejn: JSON.stringify(error),
-    });
-  }
-  return false;
-}