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";
|
||||
Reference in New Issue
Block a user