initial import
This commit is contained in:
+476
@@ -0,0 +1,476 @@
|
||||
<?php
|
||||
/*
|
||||
* package.php
|
||||
*
|
||||
* Vereist:
|
||||
*
|
||||
* gitfetcher.php
|
||||
* b64parts.php
|
||||
*
|
||||
* Routes:
|
||||
*
|
||||
* /package?name=<package>&next=...
|
||||
* HTML-pagina met base64 part-links voor data/<package>.zip.
|
||||
*
|
||||
* /package-part?name=<package>&part=000001&next=...
|
||||
* text/plain met base64-inhoud van één part.
|
||||
*
|
||||
* Regels:
|
||||
*
|
||||
* - HTML voor index/pagina's.
|
||||
* - text/plain voor payload.
|
||||
* - Payload is altijd base64.
|
||||
* - Eén next-id per gegenereerde HTML-pagina.
|
||||
*/
|
||||
|
||||
ini_set('display_errors', '1');
|
||||
ini_set('display_startup_errors', '1');
|
||||
ini_set('log_errors', '1');
|
||||
error_reporting(E_ALL);
|
||||
|
||||
require_once __DIR__ . '/nexttoken.php';
|
||||
|
||||
$TOKENS = new NextTokenStore(__DIR__ . '/data/racket-sandbox.sqlite');
|
||||
|
||||
@set_time_limit(300);
|
||||
ignore_user_abort(false);
|
||||
|
||||
require_once __DIR__ . '/gitfetcher.php';
|
||||
require_once __DIR__ . '/b64parts.php';
|
||||
require_once __DIR__ . '/base64config.php';
|
||||
require_once __DIR__ . '/lib/catalog-http.php';
|
||||
require_once __DIR__ . '/lib/racket-data.php';
|
||||
|
||||
define('DATA_DIR', __DIR__ . '/data');
|
||||
define('CATALOG_PACKAGE_BASE', 'https://pkgs.racket-lang.org/pkg/');
|
||||
define('CATALOG_CACHE_TTL', 3600);
|
||||
|
||||
$chunkConfig = load_base64_chunk_config();
|
||||
$packageZipMaxBase64Kb = (int)($chunkConfig['package_zip_max_base64_kb'] ?? 2048);
|
||||
|
||||
if ($packageZipMaxBase64Kb < 1) {
|
||||
$packageZipMaxBase64Kb = 1;
|
||||
}
|
||||
|
||||
define('PACKAGE_ZIP_MAX_BASE64_KB', $packageZipMaxBase64Kb);
|
||||
define('PACKAGE_ZIP_MAX_BASE64_BYTES', PACKAGE_ZIP_MAX_BASE64_KB * 1024);
|
||||
|
||||
$NEXT_ID = $TOKENS->create();
|
||||
|
||||
function path_only()
|
||||
{
|
||||
$p = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH);
|
||||
$p = '/' . trim($p ?: '/', '/');
|
||||
return $p === '/' ? '/' : $p;
|
||||
}
|
||||
|
||||
function h($s)
|
||||
{
|
||||
return htmlspecialchars((string)$s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
|
||||
}
|
||||
|
||||
function current_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 current_host()
|
||||
{
|
||||
return $_SERVER['HTTP_HOST'] ?? 'localhost';
|
||||
}
|
||||
|
||||
function make_url($path, $query = array())
|
||||
{
|
||||
global $NEXT_ID;
|
||||
|
||||
$query['next'] = $NEXT_ID;
|
||||
|
||||
return current_scheme() . '://' . current_host() . $path . '?' . http_build_query($query);
|
||||
}
|
||||
|
||||
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>Package error</title></head><body>' .
|
||||
'<h1>Package error</h1>' .
|
||||
'<pre>' . h($message) . '</pre>' .
|
||||
'</body></html>',
|
||||
$status
|
||||
);
|
||||
}
|
||||
|
||||
function fail_text($message, $status = 500)
|
||||
{
|
||||
text_response("error: " . $message . "\n", $status);
|
||||
}
|
||||
|
||||
function package_name_ok($name)
|
||||
{
|
||||
return rktd_package_name_ok($name);
|
||||
}
|
||||
|
||||
function ensure_data_dir()
|
||||
{
|
||||
try {
|
||||
catalog_http_ensure_dir(DATA_DIR);
|
||||
} catch (Throwable $e) {
|
||||
fail_html($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
function catalog_cache_file($package)
|
||||
{
|
||||
ensure_data_dir();
|
||||
return DATA_DIR . '/catalog-' . sha1($package) . '.rktd';
|
||||
}
|
||||
|
||||
function catalog_meta_file($package)
|
||||
{
|
||||
ensure_data_dir();
|
||||
return DATA_DIR . '/catalog-' . sha1($package) . '.meta.json';
|
||||
}
|
||||
|
||||
function get_catalog_package_text($package)
|
||||
{
|
||||
$cacheFile = catalog_cache_file($package);
|
||||
$url = CATALOG_PACKAGE_BASE . rawurlencode($package);
|
||||
|
||||
try {
|
||||
return catalog_http_fetch_cached(
|
||||
$url,
|
||||
$cacheFile,
|
||||
catalog_meta_file($package),
|
||||
CATALOG_CACHE_TTL,
|
||||
'rktsndbx-package-entry/1.0',
|
||||
180
|
||||
);
|
||||
} catch (Throwable $e) {
|
||||
fail_html($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
function extract_catalog_source($catalogText)
|
||||
{
|
||||
return rktd_extract_catalog_source($catalogText);
|
||||
}
|
||||
|
||||
function normalize_source_to_repo_url($source)
|
||||
{
|
||||
$source = trim((string)$source);
|
||||
|
||||
if ($source === '') {
|
||||
fail_html('Lege package source.');
|
||||
}
|
||||
|
||||
/*
|
||||
* github://github.com/owner/repo[/branch[/subdir]]
|
||||
* Voor nu gebruiken we owner/repo. Subdirs lossen we later op.
|
||||
*/
|
||||
$p = parse_url($source);
|
||||
|
||||
if ($p === false || empty($p['scheme'])) {
|
||||
fail_html('Kan package source niet parsen: ' . $source);
|
||||
}
|
||||
|
||||
$scheme = strtolower($p['scheme']);
|
||||
|
||||
if ($scheme === 'github') {
|
||||
$host = !empty($p['host']) ? strtolower($p['host']) : 'github.com';
|
||||
$path = trim($p['path'] ?? '', '/');
|
||||
$bits = explode('/', $path);
|
||||
|
||||
if ($host !== 'github.com' || count($bits) < 2) {
|
||||
fail_html('Ongeldige github package source: ' . $source);
|
||||
}
|
||||
|
||||
return 'https://github.com/' . $bits[0] . '/' . $bits[1];
|
||||
}
|
||||
|
||||
/*
|
||||
* git+https://... normaliseren. gitfetcher.php kan hier ook deels mee
|
||||
* omgaan, maar dit houdt metadata netter.
|
||||
*/
|
||||
if (strpos($source, 'git+https://') === 0) {
|
||||
return 'https://' . substr($source, strlen('git+https://'));
|
||||
}
|
||||
|
||||
if (strpos($source, 'git+http://') === 0) {
|
||||
return 'http://' . substr($source, strlen('git+http://'));
|
||||
}
|
||||
|
||||
/*
|
||||
* Verwijder query/fragment voor de repository-fetch.
|
||||
* ?path=... en #branch/subdir pakken we later apart aan.
|
||||
*/
|
||||
$source = preg_replace('/[?#].*$/', '', $source);
|
||||
|
||||
return $source;
|
||||
}
|
||||
|
||||
function ensure_package_zip_and_parts($package)
|
||||
{
|
||||
$catalogText = get_catalog_package_text($package);
|
||||
$source = extract_catalog_source($catalogText);
|
||||
|
||||
if ($source === null || $source === '') {
|
||||
fail_html('Geen source gevonden in Racket package catalogus voor package: ' . $package);
|
||||
}
|
||||
|
||||
$repoUrl = normalize_source_to_repo_url($source);
|
||||
|
||||
$fetcher = new GitFetcher(array(
|
||||
'data_dir' => DATA_DIR,
|
||||
));
|
||||
|
||||
try {
|
||||
$zipInfo = $fetcher->ensurePackageZip($package, $repoUrl);
|
||||
} catch (Throwable $e) {
|
||||
fail_html(
|
||||
"Kon package zip niet ophalen.\n\n" .
|
||||
"Package: " . $package . "\n" .
|
||||
"Catalog source: " . $source . "\n" .
|
||||
"Repo URL: " . $repoUrl . "\n\n" .
|
||||
$e->getMessage()
|
||||
);
|
||||
}
|
||||
|
||||
$zipFile = DATA_DIR . '/' . $package . '.zip';
|
||||
|
||||
if (!is_file($zipFile) || !is_readable($zipFile)) {
|
||||
fail_html('Zipbestand ontbreekt na fetch: ' . $zipFile);
|
||||
}
|
||||
|
||||
$parts = new Base64Parts(DATA_DIR, PACKAGE_ZIP_MAX_BASE64_BYTES);
|
||||
|
||||
try {
|
||||
$manifest = ensure_parts_for_zip($parts, $package, $zipFile);
|
||||
} catch (Throwable $e) {
|
||||
fail_html('Kon base64-parts niet maken: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
return array(
|
||||
'package' => $package,
|
||||
'source' => $source,
|
||||
'repo_url' => $repoUrl,
|
||||
'zip_info' => $zipInfo,
|
||||
'manifest' => $manifest,
|
||||
);
|
||||
}
|
||||
|
||||
function ensure_parts_for_zip($parts, $package, $zipFile)
|
||||
{
|
||||
$zipSha = hash_file('sha256', $zipFile);
|
||||
$zipSize = filesize($zipFile);
|
||||
|
||||
/*
|
||||
* Hergebruik bestaande parts als ze nog exact bij de zip horen.
|
||||
*/
|
||||
try {
|
||||
$manifest = $parts->loadManifest($package);
|
||||
|
||||
if (isset($manifest['source_sha256']) &&
|
||||
$manifest['source_sha256'] === $zipSha &&
|
||||
isset($manifest['source_bytes']) &&
|
||||
(int)$manifest['source_bytes'] === (int)$zipSize &&
|
||||
isset($manifest['max_base64_bytes']) &&
|
||||
(int)$manifest['max_base64_bytes'] === PACKAGE_ZIP_MAX_BASE64_BYTES &&
|
||||
isset($manifest['parts']) &&
|
||||
is_array($manifest['parts']) &&
|
||||
count($manifest['parts']) > 0) {
|
||||
|
||||
$ok = true;
|
||||
|
||||
foreach ($manifest['parts'] as $part) {
|
||||
if (empty($part['file']) || !is_file($part['file']) || !is_readable($part['file'])) {
|
||||
$ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($ok) {
|
||||
$manifest['parts_status'] = 'cached';
|
||||
return $manifest;
|
||||
}
|
||||
}
|
||||
} catch (Throwable $e) {
|
||||
/*
|
||||
* Geen manifest of ongeldig manifest: gewoon opnieuw maken.
|
||||
*/
|
||||
}
|
||||
|
||||
$manifest = $parts->splitFile($zipFile, $package, true);
|
||||
$manifest['parts_status'] = 'created';
|
||||
|
||||
return $manifest;
|
||||
}
|
||||
|
||||
function serve_package_page()
|
||||
{
|
||||
global $NEXT_ID;
|
||||
|
||||
$package = $_GET['name'] ?? '';
|
||||
|
||||
if (!package_name_ok($package)) {
|
||||
fail_html('Ongeldige of ontbrekende package naam.', 400);
|
||||
}
|
||||
|
||||
$info = ensure_package_zip_and_parts($package);
|
||||
|
||||
$manifest = $info['manifest'];
|
||||
$zipInfo = $info['zip_info'];
|
||||
|
||||
$rows = '';
|
||||
|
||||
foreach ($manifest['parts'] as $part) {
|
||||
$n = $part['number'];
|
||||
|
||||
$url = make_url('/package-part', array(
|
||||
'name' => $package,
|
||||
'part' => $n,
|
||||
));
|
||||
|
||||
$rows .=
|
||||
'<tr>' .
|
||||
'<td>' . h($n) . '</td>' .
|
||||
'<td>' . h((string)$part['base64_bytes']) . '</td>' .
|
||||
'<td><a href="' . h($url) . '">' . h($url) . '</a></td>' .
|
||||
'</tr>' . "\n";
|
||||
}
|
||||
|
||||
html_response('<!doctype html>
|
||||
<html lang="nl">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Package ' . h($package) . '</title>
|
||||
<link rel="stylesheet" href="/styles.css">
|
||||
</head>
|
||||
<body class="simple-doc">
|
||||
|
||||
<h1>Package ' . h($package) . '</h1>
|
||||
|
||||
<p>
|
||||
Deze pagina is HTML. Alle part-links hieronder geven <code>text/plain</code>
|
||||
met base64-inhoud terug. Dezelfde <code>next</code> wordt gebruikt voor alle
|
||||
part-links op deze pagina.
|
||||
</p>
|
||||
|
||||
<h2>Bron</h2>
|
||||
|
||||
<table>
|
||||
<tr><th>catalog source</th><td><code>' . h($info['source']) . '</code></td></tr>
|
||||
<tr><th>repo url</th><td><code>' . h($info['repo_url']) . '</code></td></tr>
|
||||
<tr><th>fetch status</th><td><code>' . h($zipInfo['status'] ?? '') . '</code></td></tr>
|
||||
<tr><th>default branch</th><td><code>' . h($zipInfo['default_branch'] ?? '') . '</code></td></tr>
|
||||
<tr><th>head sha</th><td><code>' . h($zipInfo['head_sha'] ?? '') . '</code></td></tr>
|
||||
<tr><th>zip file</th><td><code>' . h($zipInfo['zip_file'] ?? '') . '</code></td></tr>
|
||||
<tr><th>zip bytes</th><td><code>' . h((string)($zipInfo['zip_bytes'] ?? '')) . '</code></td></tr>
|
||||
<tr><th>zip sha256</th><td><code>' . h($zipInfo['zip_sha256'] ?? '') . '</code></td></tr>
|
||||
<tr><th>parts status</th><td><code>' . h($manifest['parts_status'] ?? '') . '</code></td></tr>
|
||||
<tr><th>max base64 part size</th><td><code>' . h((string)PACKAGE_ZIP_MAX_BASE64_KB) . '</code> KiB (<code>' . h((string)PACKAGE_ZIP_MAX_BASE64_BYTES) . '</code> bytes)</td></tr>
|
||||
<tr><th>binary chunk size</th><td><code>' . h((string)($manifest['binary_chunk_bytes'] ?? '')) . '</code> bytes</td></tr>
|
||||
<tr><th>part count</th><td><code>' . h((string)$manifest['part_count']) . '</code></td></tr>
|
||||
<tr><th>next id</th><td><code>' . h($NEXT_ID) . '</code></td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Base64 parts</h2>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>part</th>
|
||||
<th>base64 bytes</th>
|
||||
<th>text/plain URL</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
' . $rows . '
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<h2>Reconstructie in de sandbox</h2>
|
||||
|
||||
<pre>
|
||||
# download alle links als:
|
||||
# ' . h($package) . '.part.000001.b64
|
||||
# ' . h($package) . '.part.000002.b64
|
||||
# enz.
|
||||
|
||||
cat ' . h($package) . '.part.*.b64 > ' . h($package) . '.zip.b64
|
||||
base64 -d ' . h($package) . '.zip.b64 > ' . h($package) . '.zip
|
||||
raco pkg install --auto ./' . h($package) . '.zip
|
||||
</pre>
|
||||
|
||||
</body>
|
||||
</html>');
|
||||
}
|
||||
|
||||
function serve_package_part()
|
||||
{
|
||||
$package = $_GET['name'] ?? '';
|
||||
$part = $_GET['part'] ?? '';
|
||||
|
||||
if (!package_name_ok($package)) {
|
||||
fail_text('Ongeldige of ontbrekende package naam.', 400);
|
||||
}
|
||||
|
||||
if (!is_string($part) || !preg_match('/^[0-9]{6}$/', $part)) {
|
||||
fail_text('Ongeldig of ontbrekend partnummer.', 400);
|
||||
}
|
||||
|
||||
$parts = new Base64Parts(DATA_DIR, PACKAGE_ZIP_MAX_BASE64_BYTES);
|
||||
|
||||
try {
|
||||
$txt = $parts->readPart($package, $part);
|
||||
} catch (Throwable $e) {
|
||||
fail_text($e->getMessage(), 404);
|
||||
}
|
||||
|
||||
text_response($txt, 200);
|
||||
}
|
||||
|
||||
$path = path_only();
|
||||
|
||||
if ($path === '/package') {
|
||||
$TOKENS->check_valid_next('html');
|
||||
serve_package_page();
|
||||
}
|
||||
|
||||
if ($path === '/package-part') {
|
||||
$TOKENS->check_valid_next('text');
|
||||
serve_package_part();
|
||||
}
|
||||
|
||||
fail_html('Onbekende route: ' . $path, 404);
|
||||
Reference in New Issue
Block a user