|
2 | 2 | // Use of this source code is governed by the MIT license, a copy of which can |
3 | 3 | // be found in the LICENSE file. |
4 | 4 |
|
| 5 | +import { Detector } from 'features/sampcac/detector.js'; |
| 6 | +import { DetectorResults } from 'features/sampcac/detector_results.js'; |
| 7 | + |
| 8 | +import { equals } from 'base/equals.js'; |
| 9 | + |
| 10 | +// File (not checked in) in which detectors are located. Not required. |
| 11 | +const kDetectorConfiguration = 'detectors.json'; |
| 12 | + |
5 | 13 | // Time after which we consider a memory read to have timed out. |
6 | 14 | export const kMemoryReadTimeoutMs = 3500; |
7 | 15 |
|
8 | 16 | // Manages the SAMPCAC detectors that are available on the server. Will load its information from |
9 | 17 | // a configuration file, unless tests are active, in which case they can be injected. |
10 | 18 | export class DetectorManager { |
| 19 | + detectors_ = null; |
11 | 20 | natives_ = null; |
12 | 21 | responseResolvers_ = null; |
13 | 22 |
|
14 | 23 | constructor(natives) { |
| 24 | + this.detectors_ = null; |
15 | 25 | this.natives_ = natives; |
16 | 26 | this.responseResolvers_ = new Map(); |
17 | 27 |
|
18 | 28 | server.playerManager.addObserver(this); |
19 | 29 | } |
20 | 30 |
|
| 31 | + // Initializes the detectors from scratch. Will be lazily called the first time a detection run |
| 32 | + // is started for a particular player, or the detectors are being reloaded by management. |
| 33 | + initializeDetectors() { |
| 34 | + this.detectors_ = new Set(); |
| 35 | + |
| 36 | + let configuration = null; |
| 37 | + try { |
| 38 | + configuration = JSON.parse(readFile(kDetectorConfiguration)); |
| 39 | + } catch { |
| 40 | + return; // bail out, the file does not exist |
| 41 | + } |
| 42 | + |
| 43 | + if (!Array.isArray(configuration)) |
| 44 | + throw new Error(`Expected the detector configuration to be an array.`); |
| 45 | + |
| 46 | + for (const detectorConfiguration of configuration) |
| 47 | + this.detectors_.add(new Detector(detectorConfiguration)); |
| 48 | + } |
| 49 | + |
| 50 | + // --------------------------------------------------------------------------------------------- |
| 51 | + |
| 52 | + // Runs the necessary checks on the given |player|, and returns an instance of DetectorResults |
| 53 | + // to communicate back the |player|'s state. Could take multiple seconds. |
| 54 | + async detect(player) { |
| 55 | + if (this.detectors_ === null) |
| 56 | + this.initializeDetectors(); |
| 57 | + |
| 58 | + const results = new DetectorResults(); |
| 59 | + |
| 60 | + // (1) Populate the meta-data fields of the results. |
| 61 | + results.version = player.version; |
| 62 | + |
| 63 | + if (this.natives_.getStatus(player)) { |
| 64 | + results.sampcacVersion = this.natives_.getClientVersion(player).join('.'); |
| 65 | + results.sampcacHardwareId = this.natives_.getHardwareID(player); |
| 66 | + } |
| 67 | + |
| 68 | + results.minimized = player.isMinimized(); |
| 69 | + |
| 70 | + // (2) Run each of the detectors for the |player| in parallel and populate the results in |
| 71 | + // the |results.detectors| map. There is no expectation for that map to be sorted. |
| 72 | + results.detectors = new Map(); |
| 73 | + |
| 74 | + const tasks = []; |
| 75 | + |
| 76 | + for (const detector of this.detectors_) { |
| 77 | + tasks.push(this.requestDetection(player, detector).then(result => { |
| 78 | + results.detectors.set(detector.name, result); |
| 79 | + })); |
| 80 | + } |
| 81 | + |
| 82 | + // Wait for all the |tasks| to have been completed. |
| 83 | + await Promise.all(tasks); |
| 84 | + |
| 85 | + // (3) Return the |results| to the caller who requested this detection run. |
| 86 | + return results; |
| 87 | + } |
| 88 | + |
| 89 | + // Requests the |detector| to run for the given |player|. Will return the result of the request |
| 90 | + // as one of the DetectorResult.kResult* constants. |
| 91 | + async requestDetection(player, detector) { |
| 92 | + const response = await this.requestMemoryRead(player, detector.address, detector.bytes); |
| 93 | + if (response === null) |
| 94 | + return DetectorResults.kResultUnavailable; |
| 95 | + |
| 96 | + // (1) Determine whether the |response| is a checksum (true) or an array of bytes (false). |
| 97 | + const isChecksum = typeof response === 'number'; |
| 98 | + |
| 99 | + // (2) Consider the |response| based on the type of |detector| we're dealing with. |
| 100 | + switch (detector.type) { |
| 101 | + case Detector.kTypeAllowList: |
| 102 | + if (isChecksum && detector.resultChecksum === response) |
| 103 | + return DetectorResults.kResultClean; |
| 104 | + |
| 105 | + if (!isChecksum && equals(detector.resultBytes, response)) |
| 106 | + return DetectorResults.kResultClean; |
| 107 | + |
| 108 | + break; |
| 109 | + |
| 110 | + case Detector.kTypeBlockList: |
| 111 | + if (isChecksum && detector.resultChecksum === response) |
| 112 | + return DetectorResults.kResultDetected; |
| 113 | + |
| 114 | + if (!isChecksum && equals(detector.resultBytes, response)) |
| 115 | + return DetectorResults.kResultDetected; |
| 116 | + |
| 117 | + break; |
| 118 | + } |
| 119 | + |
| 120 | + return DetectorResults.kResultUndeterminable; |
| 121 | + } |
| 122 | + |
21 | 123 | // --------------------------------------------------------------------------------------------- |
22 | 124 |
|
23 | 125 | // Requests a memory read from the |player| at the given |address| (in GTA_SA.exe address space) |
@@ -53,8 +155,8 @@ export class DetectorManager { |
53 | 155 |
|
54 | 156 | // Request a checksum for GTA_SA.exe's .text section as the contents are well known, so we |
55 | 157 | // don't need the granularity and can reduce the data being transfered. |
56 | | - address < 0x856A00 ? this.natives_.readMemoryChecksum(player, address, bytes) |
57 | | - : this.natives_.readMemory(player, address, bytes); |
| 158 | + this.natives_.readMemory(player, address, bytes); |
| 159 | + this.natives_.readMemoryChecksum(player, address, bytes) |
58 | 160 |
|
59 | 161 | const result = await promise; |
60 | 162 |
|
|
0 commit comments