mirror of
https://github.com/michalcz10/USB-RAID-Array.git
synced 2026-11-11 07:59:04 +00:00
revise backend
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../Web/includes/auth.php';
|
||||
|
||||
function expectBackend(bool $condition, string $message): void {
|
||||
if (!$condition) throw new RuntimeException($message);
|
||||
}
|
||||
|
||||
// Run handlers in separate processes because redirects and permission checks exit.
|
||||
if (($argv[1] ?? '') === '--handler') {
|
||||
session_set_save_handler(new class implements SessionHandlerInterface {
|
||||
public function open(string $path, string $name): bool { return true; }
|
||||
public function close(): bool { return true; }
|
||||
public function read(string $id): string|false { return ''; }
|
||||
public function write(string $id, string $data): bool { return true; }
|
||||
public function destroy(string $id): bool { return true; }
|
||||
public function gc(int $maxLifetime): int|false { return 0; }
|
||||
}, true);
|
||||
session_start(['use_cookies' => false, 'cache_limiter' => '']);
|
||||
http_response_code(200);
|
||||
$scenario = json_decode($argv[3], true, 512, JSON_THROW_ON_ERROR);
|
||||
$_SESSION = $scenario['session'] ?? [];
|
||||
$_POST = $scenario['post'] ?? [];
|
||||
$_GET = $scenario['get'] ?? [];
|
||||
$_FILES = $scenario['files'] ?? [];
|
||||
$_SERVER['REQUEST_METHOD'] = $scenario['method'] ?? 'POST';
|
||||
$defaultPath = '/mnt/raid';
|
||||
$calls = [];
|
||||
|
||||
function initializeSFTP() {
|
||||
global $calls;
|
||||
$calls[] = ['connect'];
|
||||
return new class {
|
||||
function put($path, $data, $mode = null) {
|
||||
global $calls;
|
||||
$calls[] = $mode === null ? ['put', $path, $data] : ['put', $path, $data, $mode];
|
||||
return true;
|
||||
}
|
||||
function is_dir($path) { return true; }
|
||||
function mkdir($path) {
|
||||
global $calls;
|
||||
$calls[] = ['mkdir', $path];
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Minimal database double exercises login without a live database or network.
|
||||
function getDatabaseConnection() {
|
||||
return new class {
|
||||
function prepare($sql) {
|
||||
return new class {
|
||||
function bind_param($types, &...$values) {}
|
||||
function execute() {}
|
||||
function get_result() {
|
||||
return new class {
|
||||
private $fetched = false;
|
||||
function fetch_assoc() {
|
||||
if ($this->fetched) return null;
|
||||
$this->fetched = true;
|
||||
return ['uname' => 'member', 'pswd' => password_hash(' Secret1<&" ', PASSWORD_BCRYPT),
|
||||
'admin' => 0, 'defPath' => '/mnt/raid', 'downPer' => 1, 'delPer' => 0, 'upPer' => 1];
|
||||
}
|
||||
function free() {}
|
||||
};
|
||||
}
|
||||
function close() {}
|
||||
};
|
||||
}
|
||||
function close() {}
|
||||
};
|
||||
}
|
||||
|
||||
register_shutdown_function(function () use (&$calls) {
|
||||
echo "\n__RESULT__" . json_encode(['status' => http_response_code(), 'calls' => $calls, 'session' => $_SESSION]);
|
||||
});
|
||||
$path = realpath(__DIR__ . '/../Web/content/' . $argv[2] . '.php');
|
||||
$source = file_get_contents($path);
|
||||
// Replace only configuration loading, retaining the real guards and handlers.
|
||||
$source = preg_replace("~require_once __DIR__ \\. '/(?:\\.\\./)+config/config\\.php';~", '', $source);
|
||||
$source = str_replace('__DIR__', var_export(dirname($path), true), $source);
|
||||
$source = str_replace('session_start();', '', $source);
|
||||
require_once __DIR__ . '/../Web/includes/remotepaths.php';
|
||||
require_once __DIR__ . '/../Web/vendor/autoload.php';
|
||||
eval('?>' . $source);
|
||||
exit;
|
||||
}
|
||||
|
||||
function runBackendHandler(string $handler, array $scenario): array {
|
||||
$process = proc_open([PHP_BINARY, __FILE__, '--handler', $handler, json_encode($scenario)],
|
||||
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']], $pipes);
|
||||
fclose($pipes[0]);
|
||||
$output = stream_get_contents($pipes[1]);
|
||||
$errors = stream_get_contents($pipes[2]);
|
||||
fclose($pipes[1]);
|
||||
fclose($pipes[2]);
|
||||
$exitCode = proc_close($process);
|
||||
expectBackend($exitCode === 0 && $errors === '', $handler . ' failed: ' . $errors . $output);
|
||||
[$body, $result] = explode("\n__RESULT__", $output, 2);
|
||||
return ['body' => $body] + json_decode($result, true, 512, JSON_THROW_ON_ERROR);
|
||||
}
|
||||
|
||||
foreach (['short1A' => false, 'lowercase1' => false, 'UppercaseOnly' => false, 'Password1' => true, ' Secret1<&" ' => true] as $password => $expected) {
|
||||
expectBackend(isPasswordValid($password) === $expected, 'Incorrect password validation');
|
||||
}
|
||||
|
||||
$member = ['uname' => 'member', 'upPer' => true, 'downPer' => true, 'delPer' => true];
|
||||
foreach (['upload' => 'upPer', 'createdir' => 'upPer', 'createfile' => 'upPer', 'rename' => 'upPer', 'extract' => 'upPer', 'delete' => 'delPer', 'download' => 'downPer'] as $handler => $permission) {
|
||||
$result = runBackendHandler('ftp/' . $handler, []);
|
||||
expectBackend($result['calls'] === [] && $result['body'] === '', $handler . ' must require login before connecting');
|
||||
$result = runBackendHandler('ftp/' . $handler, ['session' => ['uname' => 'member', $permission => false]]);
|
||||
expectBackend($result['status'] === 403 && $result['calls'] === [], $handler . ' must enforce permissions before connecting');
|
||||
}
|
||||
|
||||
$result = runBackendHandler('ftp/createfile', ['session' => $member,
|
||||
'post' => ['action' => 'createFile', 'currentPath' => '/mnt/raid', 'fileName' => 'empty.txt']]);
|
||||
expectBackend($result['status'] === 200 && $result['calls'][1] === ['put', '/mnt/raid/empty.txt', ''], 'New files must contain zero bytes');
|
||||
expectBackend(json_decode($result['body'], true)['success'] === true, 'Create file response must retain its API');
|
||||
|
||||
foreach (['/mnt/raid-other', '/mnt/raid/../private'] as $path) {
|
||||
$result = runBackendHandler('ftp/createdir', ['session' => $member,
|
||||
'post' => ['action' => 'createDir', 'currentPath' => $path, 'dirName' => 'new']]);
|
||||
expectBackend($result['status'] === 403 && count($result['calls']) === 1, 'Reject writes outside workspace');
|
||||
}
|
||||
$result = runBackendHandler('ftp/createdir', ['session' => $member, 'post' => ['action' => 'createDir', 'dirName' => '..']]);
|
||||
expectBackend($result['status'] === 400 && count($result['calls']) === 1, 'Reject parent directory names');
|
||||
$result = runBackendHandler('ftp/delete', ['session' => $member, 'post' => ['delete' => '/mnt/raid']]);
|
||||
expectBackend($result['status'] === 400 && count($result['calls']) === 1, 'Protect workspace root from deletion');
|
||||
$result = runBackendHandler('ftp/rename', ['session' => $member, 'post' => ['path' => '/mnt/raid/item', 'newName' => '../escape']]);
|
||||
expectBackend($result['status'] === 400 && count($result['calls']) === 1, 'Rename must accept a single filename');
|
||||
|
||||
$upload = ['session' => $member, 'post' => ['paths' => ['folder/ok.txt', 'bad.txt']],
|
||||
'files' => ['files' => ['name' => ['ok.txt', 'bad.txt'], 'tmp_name' => ['local-upload', ''],
|
||||
'error' => [UPLOAD_ERR_OK, UPLOAD_ERR_PARTIAL]]]];
|
||||
$result = runBackendHandler('ftp/upload', $upload);
|
||||
expectBackend($result['status'] === 200 && $result['calls'][1] === ['put', '/mnt/raid/folder/ok.txt', 'local-upload', 1], 'Upload must use local file mode and preserve relative directories');
|
||||
expectBackend(strpos($result['body'], 'File was only partially uploaded') !== false, 'Report errors for each uploaded file');
|
||||
$upload['post']['paths'][0] = '../private.txt';
|
||||
$result = runBackendHandler('ftp/upload', $upload);
|
||||
expectBackend($result['status'] === 403 && count($result['calls']) === 1, 'Upload paths must stay in the selected directory');
|
||||
|
||||
$result = runBackendHandler('login', ['post' => ['uname' => 'member', 'pswd' => ' Secret1<&" ']]);
|
||||
expectBackend(($result['session']['uname'] ?? '') === 'member', 'Login must preserve special characters and whitespace in passwords');
|
||||
expectBackend($result['session']['admin'] === false && $result['session']['delPer'] === false && $result['session']['upPer'] === true, 'Login must retain boolean permissions');
|
||||
$result = runBackendHandler('login', ['post' => ['uname' => 'member', 'pswd' => 'wrong']]);
|
||||
expectBackend(!isset($result['session']['uname']) && isset($result['session']['login_error']), 'Reject incorrect passwords');
|
||||
|
||||
echo "Backend: handler permissions, path boundaries, empty files and login checks passed.\n";
|
||||
@@ -28,4 +28,18 @@ foreach (['/etc' => '/etc/config.txt', '/mnt/raid' => '/mnt/raid/config.txt'] as
|
||||
$relative = substr($file, strlen(rtrim(remoteDirname($base), '/')) + 1);
|
||||
if ($relative !== ($base === '/etc' ? 'etc/config.txt' : 'raid/config.txt')) throw new RuntimeException('Incorrect ZIP member path');
|
||||
}
|
||||
echo "Remote paths: POSIX parents, normalization, root navigation and ZIP paths passed.\n";
|
||||
foreach ([
|
||||
['/mnt/raid', '/mnt/raid', true],
|
||||
['/mnt/raid/folder', '/mnt/raid/', true],
|
||||
['/mnt/raid-other/file', '/mnt/raid', false],
|
||||
['/mnt/raid/../private', '/mnt/raid', false],
|
||||
['/mnt/raid/sub/../../private', '/mnt/raid', false],
|
||||
['/mnt/raid/sub/../file', '/mnt/raid', true],
|
||||
['/etc', '/', true],
|
||||
] as [$path, $basePath, $expected]) {
|
||||
if (isRemotePathWithin($path, $basePath) !== $expected) throw new RuntimeException('Incorrect path boundary: ' . $path);
|
||||
}
|
||||
foreach (['' => false, '.' => false, '..' => false, 'a/b' => false, "a\0b" => false, 'file.txt' => true, '0' => true, 'a\\b' => true] as $name => $expected) {
|
||||
if (isRemoteNameValid((string) $name) !== $expected) throw new RuntimeException('Incorrect filename validation');
|
||||
}
|
||||
echo "Remote paths: POSIX parents, normalization, workspace boundaries, names and ZIP paths passed.\n";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../Web/includes/workspace.php';
|
||||
require_once __DIR__ . '/../Web/includes/auth.php';
|
||||
require_once __DIR__ . '/../Web/includes/remotepaths.php';
|
||||
require_once __DIR__ . '/../Web/content/ftp/lib/serverstats.php';
|
||||
set_error_handler(function ($severity, $message, $file, $line) {
|
||||
@@ -11,11 +12,11 @@ function expectWorkspace($condition, $message) {
|
||||
|
||||
function renderWorkspaceFixture($name, $admin = true, $defaultPath = '/mnt/raid', $currentPath = '/mnt/raid/Workspace') {
|
||||
$_SESSION = ['uname' => 'michal', 'admin' => $admin, 'upPer' => true, 'downPer' => true, 'delPer' => true];
|
||||
$filePath = $currentPath . '/Example.pdf'; $fileName = basename($filePath); $parentDir = $currentPath;
|
||||
$filePath = $currentPath . '/Example.pdf'; $fileName = basename($filePath); $parentPath = $currentPath;
|
||||
$fileExtension = 'png'; $fileSize = 245760; $mimeType = 'image/png';
|
||||
$isImage = true; $isVideo = false; $isAudio = false;
|
||||
$editable = true; $showLineNumbers = true; $content = "{\n \"workspace\": \"USB RAID\",\n \"status\": \"connected\"\n}\n";
|
||||
$message = ''; $messageType = ''; $message_type = ''; $shareCreated = false; $shareError = '';
|
||||
$message = ''; $messageType = ''; $shareCreated = false; $shareError = '';
|
||||
$share = ['expiration' => '2030-01-01']; $downloadAllowed = false; $token = 'test-only';
|
||||
$items = ['Documents', 'Photos', 'README.md', 'Example.pdf'];
|
||||
$imageExtensions = ['png']; $videoExtensions = []; $audioExtensions = []; $editableExtensions = ['md']; $archiveExtensions = [];
|
||||
|
||||
Reference in New Issue
Block a user