Bearsampp 2026.9.23
Loading...
Searching...
No Matches
class.core.php
Go to the documentation of this file.
1<?php
2/*
3 *
4 * * Copyright (c) 2021-2026 Bearsampp
5 * * License: GNU General Public License version 3 or later; see LICENSE.txt
6 * * Website: https://bearsampp.com
7 * * Github: https://github.com/Bearsampp
8 *
9 */
10
18class Core
19{
20 // Constants for various file names and versions
21 const isRoot_FILE = 'root.php';
22 const PATH_WIN_PLACEHOLDER = '~BEARSAMPP_WIN_PATH~';
23 const PATH_LIN_PLACEHOLDER = '~BEARSAMPP_LIN_PATH~';
24
25 const PHP_EXE = 'php-win.exe';
26 const SETENV_EXE = 'SetEnv.exe';
27 const NSSM_EXE = 'nssm.exe';
28 const OPENSSL_EXE = 'openssl.exe';
29 const OPENSSL_CONF = 'openssl.cfg';
30 const HOSTSEDITOR_EXE = 'hEdit_x64.exe';
31 const LN_EXE = 'ln.exe';
32 const PWGEN_EXE = "PwTech.exe";
33
34 const APP_VERSION = 'version.dat';
35 const LAST_PATH = 'lastPath.dat';
36 const EXEC = 'exec.dat';
37 const LOADING_PID = 'loading.pid';
38
44 public function __construct()
45 {
46 if (extension_loaded('winbinder')) {
47 require_once Path::getLibsPath() . '/winbinder/winbinder.php';
48 }
49 }
50
56 public function getAppVersion()
57 {
58 global $bearsamppLang;
59
60 $filePath = Path::getResourcesPath() . '/' . self::APP_VERSION;
61 if (!is_file($filePath)) {
62 Log::error(sprintf($bearsamppLang->getValue(Lang::ERROR_CONF_NOT_FOUND), APP_TITLE, $filePath));
63
64 return null;
65 }
66
67 return trim(file_get_contents($filePath));
68 }
69
75 public function getLastPathContent()
76 {
77 return @file_get_contents(Path::getLastPath());
78 }
79
85 public function getPreviousExec()
86 {
87 $file = $this->getExec();
88 if (file_exists($file)) {
89 return trim(file_get_contents($file));
90 }
91
92 return false;
93 }
94
102 public function getExec($aetrayPath = false)
103 {
104 return Path::getTmpPath($aetrayPath) . '/' . self::EXEC;
105 }
106
112 public function setExec($action)
113 {
114 file_put_contents($this->getExec(), $action);
115 }
116
124 public function getLoadingPid($aetrayPath = false)
125 {
126 return Path::getResourcesPath($aetrayPath) . '/' . self::LOADING_PID;
127 }
128
134 public function addLoadingPid($pid)
135 {
136 file_put_contents($this->getLoadingPid(), $pid . PHP_EOL, FILE_APPEND);
137 }
138
144 public function __toString()
145 {
146 return 'core object';
147 }
148
170 public function unzipFile($filePath, $destination, $progressCallback = null)
171 {
172 global $bearsamppRoot;
173
174 $sevenZipPath = Path::getLibsPath() . '/7zip/7za.exe';
175
176 if (!file_exists($sevenZipPath)) {
177 Log::error('7za.exe not found at: ' . $sevenZipPath);
178
179 return false;
180 }
181
182 if ($progressCallback) {
183 call_user_func($progressCallback, 'Analyzing archive...');
184 }
185
186 // Defensive path-traversal scan: parse the structured `7z l -slt` output into
187 // each individual entry path and validate every one relative to the destination,
188 // so extraction can never escape the intended directory. This must fail closed:
189 // any listing/parse failure aborts the operation rather than proceeding unverified.
190 // The scan also returns the number of archive entries, used as the extraction
191 // file count. A standalone integrity test is not needed: the archive's SHA-256
192 // was already verified against its .sha256 sidecar upstream, and `7za x`
193 // CRC-checks every file as it is extracted.
194 $numFiles = self::isSafeDestinationFileList($sevenZipPath, $filePath);
195 if ($numFiles === false) {
196 Log::error('Archive path-traversal scan failed or rejected for: ' . $filePath);
197
198 return false;
199 }
200 Log::trace('Number of files to be extracted: ' . $numFiles);
201
202 if ($progressCallback) {
203 call_user_func($progressCallback, 'Initializing extraction...');
204 }
205 // Extract the archive, streaming progress line-by-line
206 $returnVar = CommandRunner::stream(
207 $sevenZipPath,
208 ['x', $filePath, '-y', '-bsp1', '-bb0', '-o' . $destination],
209 function (string $line) use ($progressCallback) {
210 Log::trace("Processing line: $line");
211 if ($line === 'Everything is Ok') {
212 if ($progressCallback) {
213 Log::trace('Extraction progress: 100%');
214 call_user_func($progressCallback, 100);
215 }
216 } elseif ($progressCallback && preg_match('/(?:^|\s)(\d+)%/', $line, $matches)) {
217 $currentPercentage = intval($matches[1]);
218 Log::trace("Extraction progress: $currentPercentage%");
219 call_user_func($progressCallback, $currentPercentage);
220 } else {
221 Log::trace("Line did not match pattern: $line");
222 }
223 }
224 );
225
226 if ($returnVar === false) {
227 Log::error('Failed to open process for: ' . $sevenZipPath);
228
229 return ['error' => 'Failed to open process', 'numFiles' => $numFiles];
230 }
231
232 Log::trace('Command return value: ' . $returnVar);
233
234 if ($returnVar === 0 && $progressCallback) {
235 Log::trace('Extraction completed successfully. Setting progress to 100%');
236 call_user_func($progressCallback, 100);
237 usleep(100000); // 100 milliseconds
238 }
239
240 if ($returnVar === 0) {
241 Log::debug('Successfully unzipped file to: ' . $destination);
242
243 return ['success' => true, 'numFiles' => $numFiles];
244 }
245
246 Log::error('Failed to unzip file. Command return value: ' . $returnVar);
247
248 return ['error' => 'Failed to unzip file', 'numFiles' => $numFiles];
249 }
250
272 private static function isSafeDestinationFileList($sevenZipPath, $filePath)
273 {
274 $listingFile = Path::getTmpPath() . '/7z-listing-' . uniqid('', true) . '.txt';
275 $stderrFile = Path::getTmpPath() . '/7z-listing-' . uniqid('', true) . '.err';
276
277 $returnVar = CommandRunner::execToFile($sevenZipPath, ['l', '-slt', $filePath], $listingFile, $stderrFile);
278 if ($returnVar === false) {
279 Log::error('Path-traversal scan: unable to list archive: ' . $filePath);
280
281 return false;
282 }
283
284 $listingOutput = @file_get_contents($listingFile);
285 @unlink($listingFile);
286 @unlink($stderrFile);
287
288 if (!is_string($listingOutput) || $listingOutput === '') {
289 Log::error('Path-traversal scan: unable to list archive: ' . $filePath);
290
291 return false;
292 }
293
294 // 7-Zip separates the `-slt` entry blocks with blank lines (a single dash
295 // line precedes the first entry, and the banner/header references the archive
296 // itself). Normalize line endings and split on blank lines so each block is
297 // one archive entry.
298 $listingOutput = str_replace("\r\n", "\n", $listingOutput);
299 $blocks = preg_split('/\n[ \t]*\n/', $listingOutput);
300 if (empty($blocks)) {
301 // No entries listed at all - cannot validate, fail closed.
302 Log::error('Path-traversal scan: no entries found in archive: ' . $filePath);
303
304 return false;
305 }
306
307 // The header block's `Path = <archive itself>` entry must not be validated
308 // or counted; skip any block that resolves to the archive file.
309 $archivePathNormalized = strtolower(str_replace('/', '\\', $filePath));
310
311 $numFiles = 0;
312 foreach ($blocks as $block) {
313 $block = trim($block);
314 if ($block === '') {
315 continue;
316 }
317
318 if (!preg_match('/^Path = (.*)$/m', $block, $match)) {
319 // Not an entry block (banner/scanning preamble lines) - skip.
320 continue;
321 }
322
323 $entryPath = trim($match[1]);
324 if (strtolower(str_replace('/', '\\', $entryPath)) === $archivePathNormalized) {
325 // Banner/header (and any trailing archive-summary) block.
326 continue;
327 }
328
329 if (self::isUnsafeArchiveEntryPath($entryPath)) {
330 Log::error('Archive contains an unsafe entry path ("' . $entryPath . '"): ' . $filePath);
331
332 return false;
333 }
334
335 // Directories are validated above but not counted as files, matching the
336 // file count previously reported by `7z t` (`Files:` excludes folders).
337 if (preg_match('/^Attributes = D\s*$/m', $block)) {
338 continue;
339 }
340
341 $numFiles++;
342 }
343
344 if ($numFiles === 0) {
345 // Nothing to extract - cannot validate, fail closed.
346 Log::error('Path-traversal scan: no file entries found in archive: ' . $filePath);
347
348 return false;
349 }
350
351 return $numFiles;
352 }
353
365 private static function isUnsafeArchiveEntryPath($entryPath)
366 {
367 if ($entryPath === '') {
368 return true;
369 }
370
371 // Parent-directory traversal (matches any path segment equal to '..').
372 if (preg_match('/(?:^|[\\/\\\\])\.\.(?:[\\/\\\\]|$)/', $entryPath)) {
373 return true;
374 }
375
376 // Absolute / rooted paths.
377 if (preg_match('#^(?:[A-Za-z]:[\\/\\\\]|[\\/\\\\])#', $entryPath)) {
378 return true;
379 }
380
381 return false;
382 }
383}
global $bearsamppLang
global $bearsamppRoot
static execToFile(string $executable, array $args, string $stdoutFile, string $stderrFile)
static stream(string $executable, array $args, callable $lineCallback)
const OPENSSL_CONF
unzipFile($filePath, $destination, $progressCallback=null)
__construct()
const PHP_EXE
const PWGEN_EXE
const isRoot_FILE
getAppVersion()
addLoadingPid($pid)
getPreviousExec()
static isSafeDestinationFileList($sevenZipPath, $filePath)
getExec($aetrayPath=false)
getLastPathContent()
__toString()
static isUnsafeArchiveEntryPath($entryPath)
const PATH_LIN_PLACEHOLDER
const EXEC
getLoadingPid($aetrayPath=false)
const NSSM_EXE
const SETENV_EXE
const LN_EXE
const PATH_WIN_PLACEHOLDER
const LOADING_PID
setExec($action)
const HOSTSEDITOR_EXE
const APP_VERSION
const OPENSSL_EXE
const LAST_PATH
const ERROR_CONF_NOT_FOUND
static debug($data, $file=null)
static trace($data, $file=null)
static error($data, $file=null)
static getResourcesPath($aetrayPath=false)
static getTmpPath($aetrayPath=false)
static getLibsPath($aetrayPath=false)
static getLastPath($aetrayPath=false)
const APP_TITLE
Definition root.php:19