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";