feat: wire the front controller and harden userToken path
This commit is contained in:
+121
-5
@@ -2,8 +2,124 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
// Front controller. The router and handler dispatch land in Task 13.
|
use BattleForge\Http\CsrfToken;
|
||||||
// For now this returns a 200 with a stub so the dev server is reachable.
|
use BattleForge\Http\Handlers\GetAssets;
|
||||||
http_response_code(200);
|
use BattleForge\Http\Handlers\GetBattlefieldEditor;
|
||||||
header('Content-Type: text/plain; charset=utf-8');
|
use BattleForge\Http\Handlers\GetHomePage;
|
||||||
echo "BattleForge dev server up.\n";
|
use BattleForge\Http\Handlers\GetTeamEditor;
|
||||||
|
use BattleForge\Http\Handlers\PostBattlefieldEditor;
|
||||||
|
use BattleForge\Http\Handlers\PostImageUpload;
|
||||||
|
use BattleForge\Http\Handlers\PostStartMatch;
|
||||||
|
use BattleForge\Http\Handlers\PostTeamEditor;
|
||||||
|
use BattleForge\Http\Request;
|
||||||
|
use BattleForge\Http\Response;
|
||||||
|
use BattleForge\Http\Router;
|
||||||
|
|
||||||
|
require __DIR__ . '/../vendor/autoload.php';
|
||||||
|
|
||||||
|
// 1. Read the app secret.
|
||||||
|
$secret = getenv('BATTLEFORGE_SECRET');
|
||||||
|
if ($secret === false || $secret === '') {
|
||||||
|
$secretFile = __DIR__ . '/../var/secret.key';
|
||||||
|
if (!is_file($secretFile)) {
|
||||||
|
if (!is_dir(dirname($secretFile))) {
|
||||||
|
mkdir(dirname($secretFile), 0700, true);
|
||||||
|
}
|
||||||
|
file_put_contents($secretFile, random_bytes(32));
|
||||||
|
chmod($secretFile, 0600);
|
||||||
|
}
|
||||||
|
$secret = file_get_contents($secretFile);
|
||||||
|
if ($secret === false) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo "Failed to read app secret.\n";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Ensure the __csrf cookie is set. If absent, issue a fresh token.
|
||||||
|
$cookies = $_COOKIE;
|
||||||
|
$existingCookie = (string) ($cookies['__csrf'] ?? '');
|
||||||
|
if ($existingCookie === '') {
|
||||||
|
[$token, $cookie] = CsrfToken::issue($secret);
|
||||||
|
setcookie('__csrf', $cookie, [
|
||||||
|
'expires' => time() + 86400,
|
||||||
|
'path' => '/',
|
||||||
|
'secure' => isset($_SERVER['HTTPS']),
|
||||||
|
'httponly' => true,
|
||||||
|
'samesite' => 'Lax',
|
||||||
|
]);
|
||||||
|
$existingCookie = $cookie;
|
||||||
|
$cookies['__csrf'] = $cookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Compute the upload-endpoint user token (derived from the same secret).
|
||||||
|
$uploadsToken = hash_hmac('sha256', $secret, 'bf-uploads');
|
||||||
|
|
||||||
|
// 4. Determine the request method, path, and content type.
|
||||||
|
$method = strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET'));
|
||||||
|
$uri = (string) ($_SERVER['REQUEST_URI'] ?? '/');
|
||||||
|
$path = parse_url($uri, PHP_URL_PATH) ?: '/';
|
||||||
|
$queryString = (string) (parse_url($uri, PHP_URL_QUERY) ?? '');
|
||||||
|
$contentType = isset($_SERVER['CONTENT_TYPE']) ? (string) $_SERVER['CONTENT_TYPE'] : null;
|
||||||
|
if ($contentType !== null) {
|
||||||
|
$semicolon = strpos($contentType, ';');
|
||||||
|
if ($semicolon !== false) {
|
||||||
|
$contentType = substr($contentType, 0, $semicolon);
|
||||||
|
}
|
||||||
|
$contentType = trim($contentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. Read the raw body for fetch POSTs that submit JSON.
|
||||||
|
$rawBody = (string) file_get_contents('php://input');
|
||||||
|
|
||||||
|
// 6. Build a Request with the request data, the CSRF secret, and the upload token.
|
||||||
|
$server = $_SERVER;
|
||||||
|
$server['__csrf_secret'] = $secret;
|
||||||
|
$server['__uploads_token'] = $uploadsToken;
|
||||||
|
|
||||||
|
$request = new Request(
|
||||||
|
get: $_GET,
|
||||||
|
post: $_POST,
|
||||||
|
files: $_FILES,
|
||||||
|
cookies: $cookies,
|
||||||
|
server: $server,
|
||||||
|
queryString: $queryString,
|
||||||
|
method: $method,
|
||||||
|
path: $path,
|
||||||
|
contentType: $contentType,
|
||||||
|
rawBody: $rawBody,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 7. Configure the router with the eight routes.
|
||||||
|
$uploadsRoot = __DIR__ . '/../var/uploads';
|
||||||
|
$placeholderDir = __DIR__ . '/assets/placeholders';
|
||||||
|
|
||||||
|
$homePage = static fn (Request $r, array $p): Response => (new GetHomePage())->handle($r, $p);
|
||||||
|
$getTeam = static fn (Request $r, array $p): Response => (new GetTeamEditor())->handle($r, $p);
|
||||||
|
$postTeam = static fn (Request $r, array $p): Response => (new PostTeamEditor())->handle($r, $p);
|
||||||
|
$getBattlefield = static fn (Request $r, array $p): Response => (new GetBattlefieldEditor())->handle($r, $p);
|
||||||
|
$postBattlefield = static fn (Request $r, array $p): Response => (new PostBattlefieldEditor())->handle($r, $p);
|
||||||
|
$postStart = static fn (Request $r, array $p): Response => (new PostStartMatch())->handle($r, $p);
|
||||||
|
$postUpload = static fn (Request $r, array $p): Response => (new PostImageUpload($uploadsRoot))->handle($r, $p);
|
||||||
|
$getAssets = static function (Request $r, array $p) use ($placeholderDir, $uploadsRoot): Response {
|
||||||
|
return (new GetAssets($placeholderDir, $uploadsRoot))->handle($r, $p);
|
||||||
|
};
|
||||||
|
|
||||||
|
$router = new Router();
|
||||||
|
$router->add('GET', '/', $homePage);
|
||||||
|
$router->add('GET', '/scenarios/{id}/edit/team', $getTeam);
|
||||||
|
$router->add('POST', '/scenarios/{id}/edit/team', $postTeam);
|
||||||
|
$router->add('GET', '/scenarios/{id}/edit/battlefield', $getBattlefield);
|
||||||
|
$router->add('POST', '/scenarios/{id}/edit/battlefield', $postBattlefield);
|
||||||
|
$router->add('POST', '/scenarios/{id}/start', $postStart);
|
||||||
|
$router->add('POST', '/assets/upload', $postUpload);
|
||||||
|
$router->add('GET', '/assets/{kind}/{filename}', $getAssets);
|
||||||
|
|
||||||
|
// 8. Dispatch and emit.
|
||||||
|
$response = $router->dispatch($request);
|
||||||
|
|
||||||
|
http_response_code($response->status);
|
||||||
|
foreach ($response->headers as $name => $value) {
|
||||||
|
header($name . ': ' . $value);
|
||||||
|
}
|
||||||
|
echo $response->body;
|
||||||
|
|||||||
@@ -27,9 +27,13 @@ final class GetAssets
|
|||||||
|
|
||||||
if ($kind === 'uploads') {
|
if ($kind === 'uploads') {
|
||||||
$userToken = $params['userToken'] ?? '';
|
$userToken = $params['userToken'] ?? '';
|
||||||
|
if (!preg_match('/^[a-f0-9]{32,}$/', $userToken)) {
|
||||||
|
return Response::html(404, '<h1>Not found</h1>');
|
||||||
|
}
|
||||||
|
|
||||||
$requestToken = $request->cookies['__uploads_token'] ?? '';
|
$requestToken = $request->cookies['__uploads_token'] ?? '';
|
||||||
|
|
||||||
if ($userToken === '' || $userToken !== $requestToken) {
|
if ($userToken !== $requestToken) {
|
||||||
return Response::html(404, '<h1>Not found</h1>');
|
return Response::html(404, '<h1>Not found</h1>');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user