2f2e8869d6
Move shared PHP code into private/, move JavaScript files into js/, and block direct access to private/. Remove unused API key and cache artifacts from the working tree.
313 lines
7.8 KiB
PHP
313 lines
7.8 KiB
PHP
<?php
|
||
/*
|
||
* Simpele Racket bootstrap.
|
||
*
|
||
* .htaccess:
|
||
*
|
||
* FallbackResource /index.php
|
||
*
|
||
* Bestanden:
|
||
*
|
||
* config/racket.zip
|
||
* data/ bevat vooraf gemaakte parts
|
||
*
|
||
* Routes:
|
||
*
|
||
* /bootstrap-racket?next=...
|
||
* Leest de vooraf gemaakte parts in data/ en toont een HTML-index
|
||
* met links.
|
||
*
|
||
* /bootstrap-racket-part?n=000001&next=...
|
||
* Geeft text/plain met base64 van precies één binair part.
|
||
*
|
||
* Regels:
|
||
*
|
||
* - Indexen/lijsten zijn HTML.
|
||
* - Payload/downloads zijn text/plain.
|
||
* - Payload/downloads zijn altijd base64.
|
||
* - Geen .bin, .zip of bestandsnaam in download-URL’s.
|
||
* - next=... is alleen cache-busting.
|
||
* - Per gegenereerde HTML-pagina wordt één next-id gemaakt en voor alle
|
||
* links op die pagina gebruikt.
|
||
*/
|
||
|
||
ini_set('display_errors', '1');
|
||
ini_set('display_startup_errors', '1');
|
||
ini_set('log_errors', '1');
|
||
error_reporting(E_ALL);
|
||
|
||
require_once __DIR__ . '/private/nexttoken.php';
|
||
require_once __DIR__ . '/private/base64config.php';
|
||
require_once __DIR__ . '/private/racketzip.php';
|
||
|
||
$TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite');
|
||
|
||
@set_time_limit(300);
|
||
ignore_user_abort(false);
|
||
|
||
$chunkConfig = load_base64_chunk_config();
|
||
$racketZipMaxBase64Kb = (int)($chunkConfig['racket_zip_max_base64_kb'] ?? 8192);
|
||
|
||
if ($racketZipMaxBase64Kb < 1) {
|
||
$racketZipMaxBase64Kb = 1;
|
||
}
|
||
|
||
define('RACKET_ZIP_MAX_BASE64_KB', $racketZipMaxBase64Kb);
|
||
define('RACKET_ZIP_MAX_BASE64_BYTES', RACKET_ZIP_MAX_BASE64_KB * 1024);
|
||
define('RACKET_ZIP_BINARY_CHUNK_BYTES', racket_zip_binary_chunk_bytes_for_base64_kib(RACKET_ZIP_MAX_BASE64_KB));
|
||
|
||
if (RACKET_ZIP_BINARY_CHUNK_BYTES < 3) {
|
||
fail_html('Ongeldige racket.zip base64 chunk-instelling.');
|
||
}
|
||
|
||
/*
|
||
* Eén next-id voor alle URLs die deze request genereert.
|
||
*/
|
||
$NEXT_ID = $TOKENS->create();
|
||
|
||
function path_only()
|
||
{
|
||
$p = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
|
||
$p = '/' . trim($p ?: '/', '/');
|
||
return $p === '/' ? '/' : $p;
|
||
}
|
||
|
||
function scheme()
|
||
{
|
||
if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) {
|
||
return strtolower(trim(explode(',', $_SERVER['HTTP_X_FORWARDED_PROTO'])[0]));
|
||
}
|
||
|
||
return (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http';
|
||
}
|
||
|
||
function host_name()
|
||
{
|
||
return $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||
}
|
||
|
||
function make_url($path, $query = array())
|
||
{
|
||
global $NEXT_ID;
|
||
|
||
$query['next'] = $NEXT_ID;
|
||
|
||
return scheme() . '://' . host_name() . $path . '?' . http_build_query($query);
|
||
}
|
||
|
||
function h($s)
|
||
{
|
||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||
}
|
||
|
||
function html_response($html, $status = 200)
|
||
{
|
||
http_response_code($status);
|
||
header('Content-Type: text/html; charset=utf-8');
|
||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||
header('Pragma: no-cache');
|
||
echo $html;
|
||
exit;
|
||
}
|
||
|
||
function text_response($text, $status = 200)
|
||
{
|
||
http_response_code($status);
|
||
header('Content-Type: text/plain; charset=us-ascii');
|
||
header('Content-Disposition: inline');
|
||
header('X-Content-Type-Options: nosniff');
|
||
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
|
||
header('Pragma: no-cache');
|
||
|
||
echo $text;
|
||
|
||
if ($text === '' || substr($text, -1) !== "\n") {
|
||
echo "\n";
|
||
}
|
||
|
||
exit;
|
||
}
|
||
|
||
function fail_html($message, $status = 500)
|
||
{
|
||
html_response(
|
||
'<!doctype html>' .
|
||
'<html><head><meta charset="utf-8"><title>Error</title></head>' .
|
||
'<body><h1>Error</h1><pre>' . h($message) . '</pre></body></html>',
|
||
$status
|
||
);
|
||
}
|
||
|
||
function fail_text($message, $status = 500)
|
||
{
|
||
text_response("error: " . $message . "\n", $status);
|
||
}
|
||
|
||
function load_parts()
|
||
{
|
||
try {
|
||
$manifest = load_racket_zip_parts_manifest();
|
||
} catch (Throwable $e) {
|
||
fail_html($e->getMessage() . "\n\nSla de configuratiepagina op om racket.zip in parts te verdelen.");
|
||
}
|
||
|
||
if (!racket_zip_parts_current($manifest, RACKET_ZIP_MAX_BASE64_KB)) {
|
||
fail_html(
|
||
"racket.zip parts ontbreken of passen niet bij de huidige chunk-grootte.\n\n" .
|
||
"Sla de configuratiepagina op om de parts opnieuw te maken."
|
||
);
|
||
}
|
||
|
||
$parts = array();
|
||
|
||
foreach (($manifest['parts'] ?? array()) as $part) {
|
||
$number = (string)($part['number'] ?? '');
|
||
$parts[] = array(
|
||
'number' => $number,
|
||
'name' => (string)($part['name'] ?? racket_zip_part_name((int)$number)),
|
||
'size' => (int)($part['size'] ?? 0),
|
||
'url' => make_url('/bootstrap-racket-part', array('n' => $number)),
|
||
);
|
||
}
|
||
|
||
$manifest['parts'] = $parts;
|
||
|
||
return $manifest;
|
||
}
|
||
|
||
function serve_bootstrap()
|
||
{
|
||
global $NEXT_ID;
|
||
|
||
$manifest = load_parts();
|
||
$parts = $manifest['parts'];
|
||
|
||
$rows = '';
|
||
|
||
foreach ($parts as $i => $part) {
|
||
$rows .=
|
||
'<tr>' .
|
||
'<td>' . h((string)($i + 1)) . '</td>' .
|
||
'<td>' . h($part['number']) . '</td>' .
|
||
'<td>' . h((string)$part['size']) . '</td>' .
|
||
'<td><a href="' . h($part['url']) . '">' . h($part['url']) . '</a></td>' .
|
||
'</tr>' . "\n";
|
||
}
|
||
|
||
$zipSize = (int)($manifest['source_bytes'] ?? 0);
|
||
|
||
$pkg_url = make_url('/racket-pkg-index');
|
||
|
||
html_response('<!doctype html>
|
||
<html lang="nl">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<title>Racket bootstrap</title>
|
||
<link rel="stylesheet" href="/styles.css">
|
||
</head>
|
||
<body class="simple-doc">
|
||
|
||
<h1>Racket bootstrap</h1>
|
||
|
||
<p>
|
||
<code>racket.zip</code> is vooraf via de configuratiepagina gesplitst naar
|
||
parts in de map <code>data</code>.
|
||
</p>
|
||
|
||
<p>
|
||
Package index: <a href="' . $pkg_url . '"> Racket package index</a>
|
||
</p>
|
||
|
||
<p>
|
||
Bronbestand: <code>config/racket.zip</code><br>
|
||
Bronbestand bytes: <code>' . h((string)$zipSize) . '</code><br>
|
||
Maximale base64 part-grootte: <code>' . h((string)RACKET_ZIP_MAX_BASE64_KB) . '</code> KiB (<code>' . h((string)RACKET_ZIP_MAX_BASE64_BYTES) . '</code> bytes)<br>
|
||
Binaire chunk-grootte: <code>' . h((string)RACKET_ZIP_BINARY_CHUNK_BYTES) . '</code> bytes<br>
|
||
Aantal parts: <code>' . h((string)count($parts)) . '</code><br>
|
||
Parts gemaakt op: <code>' . h((string)($manifest['created_at'] ?? '')) . '</code><br>
|
||
next-id voor alle links op deze pagina: <code>' . h($NEXT_ID) . '</code>
|
||
</p>
|
||
|
||
<p>
|
||
Elke link hieronder geeft <strong>text/plain</strong> met de
|
||
<strong>base64-representatie van één binair part</strong>.
|
||
De URL bevat alleen een nummer, geen bestandsnaam en geen extensie.
|
||
</p>
|
||
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>#</th>
|
||
<th>partnummer</th>
|
||
<th>bytes</th>
|
||
<th>base64 text/plain URL</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
' . $rows . '
|
||
</tbody>
|
||
</table>
|
||
|
||
<h2>Reconstructie in de sandbox</h2>
|
||
|
||
<p>
|
||
Decodeer ieder base64-part afzonderlijk naar een binair part. Plak daarna de
|
||
binaire parts in numerieke volgorde aan elkaar.
|
||
</p>
|
||
|
||
<pre>
|
||
base64 -d part-000001.txt > part-000001
|
||
base64 -d part-000002.txt > part-000002
|
||
base64 -d part-000003.txt > part-000003
|
||
# enzovoort
|
||
|
||
cat part-* > racket.zip
|
||
unzip racket.zip -d /tmp/racket
|
||
</pre>
|
||
|
||
</body>
|
||
</html>');
|
||
}
|
||
|
||
function serve_part()
|
||
{
|
||
$n = $_GET['n'] ?? '';
|
||
|
||
if (!is_string($n) || !preg_match('/^[0-9]{6}$/', $n)) {
|
||
fail_text('ongeldig partnummer; gebruik bijvoorbeeld n=000001', 400);
|
||
}
|
||
|
||
$file = racket_zip_part_file($n);
|
||
|
||
if (!is_file($file) || !is_readable($file)) {
|
||
fail_text('part ontbreekt of is niet leesbaar: ' . $n, 404);
|
||
}
|
||
|
||
$bin = file_get_contents($file);
|
||
|
||
if ($bin === false) {
|
||
fail_text('kan part niet lezen: ' . $n, 500);
|
||
}
|
||
|
||
text_response(base64_encode($bin));
|
||
}
|
||
|
||
$path = path_only();
|
||
|
||
if ($path === '/' || $path === '/index.php') {
|
||
header('Location: ' . make_url('/bootstrap-racket'), true, 302);
|
||
exit;
|
||
}
|
||
|
||
if ($path === '/bootstrap-racket') {
|
||
$TOKENS->check_valid_next('html');
|
||
serve_bootstrap();
|
||
}
|
||
|
||
if ($path === '/bootstrap-racket-part') {
|
||
$TOKENS->check_valid_next('text');
|
||
serve_part();
|
||
}
|
||
|
||
fail_html('Onbekende route: ' . $path, 404);
|