diff --git a/Dockerfile b/Dockerfile index 73b77f2..7788c4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,9 @@ FROM dunglas/frankenphp:1-php8.3 -RUN install-php-extensions curl yaml +RUN install-php-extensions yaml WORKDIR /app COPY app ./app +COPY conf ./conf +COPY manifest.yaml ./manifest.yaml COPY Caddyfile /etc/frankenphp/Caddyfile diff --git a/app/public/index.php b/app/public/index.php index a98f8fa..a42fcca 100644 --- a/app/public/index.php +++ b/app/public/index.php @@ -2,143 +2,22 @@ declare(strict_types=1); -const MANIFEST_PATH = 'manifest.yaml'; -const TEMPLATES_DIRECTORY = __DIR__ . '/../templates'; -const FETCH_TIMEOUT_SECONDS = 5; +require __DIR__ . '/../src/functions.php'; -function sendPlainTextError(int $statusCode, string $message): never -{ - http_response_code($statusCode); - header('Content-Type: text/plain; charset=utf-8'); - echo $message . "\n"; - exit; -} +define('PUBLIC_BASE_URL', resolvePublicBaseUrl()); -function fetchRaw(string $relativePath): string|false -{ - $url = REPO_RAW_BASE_URL . ltrim($relativePath, '/'); - - $curl = curl_init($url); - curl_setopt_array($curl, [ - CURLOPT_RETURNTRANSFER => true, - CURLOPT_FAILONERROR => true, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_MAXREDIRS => 3, - CURLOPT_TIMEOUT => FETCH_TIMEOUT_SECONDS, - ]); - - $content = curl_exec($curl); - - return is_string($content) ? $content : false; -} - -function escapeForDoubleQuotedShellString(string $value): string -{ - return str_replace(['\\', '"', '$', '`'], ['\\\\', '\\"', '\\$', '\\`'], $value); -} - -function resolveHomePath(string $path): string -{ - if ($path === '~' || str_starts_with($path, '~/')) { - return '$HOME' . escapeForDoubleQuotedShellString(substr($path, 1)); - } - - return escapeForDoubleQuotedShellString($path); -} - -function renderScriptTemplate(string $templateName, array $placeholders): string|false -{ - $path = TEMPLATES_DIRECTORY . '/' . $templateName . '.sh.tpl'; - - if (!is_file($path)) { - return false; - } - - $script = file_get_contents($path); - - foreach ($placeholders as $key => $value) { - $script = str_replace('{{' . $key . '}}', $value, $script); - } - - return $script; -} - -$repoRawBaseUrl = getenv('REPO_RAW_BASE_URL'); - -if ($repoRawBaseUrl === false || $repoRawBaseUrl === '') { - sendPlainTextError(500, 'REPO_RAW_BASE_URL is not configured'); -} - -define('REPO_RAW_BASE_URL', $repoRawBaseUrl); - -$publicBaseUrl = getenv('PUBLIC_BASE_URL'); - -if ($publicBaseUrl === false || $publicBaseUrl === '') { - sendPlainTextError(500, 'PUBLIC_BASE_URL is not configured'); -} - -define('PUBLIC_BASE_URL', $publicBaseUrl); - -$requestPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/'; -$name = trim(rawurldecode($requestPath), '/'); +$name = resolveRequestedName(); if ($name === '') { - sendPlainTextError(400, 'Missing name of the installable item'); + handleIndexRequest(); } -$isRawRequest = str_starts_with($name, 'raw/'); - -if ($isRawRequest) { - $name = substr($name, strlen('raw/')); +if ($name === 'list') { + handleListRequest(); } -$repoHost = parse_url(REPO_RAW_BASE_URL, PHP_URL_HOST); - -$manifestYaml = fetchRaw(MANIFEST_PATH); - -if ($manifestYaml === false) { - sendPlainTextError(502, "Failed to fetch the manifest from $repoHost"); +if (str_starts_with($name, 'raw/')) { + handleRawRequest(substr($name, strlen('raw/'))); } -$manifest = yaml_parse($manifestYaml); - -if ($manifest === false) { - sendPlainTextError(502, "Failed to parse the manifest from $repoHost"); -} - -$entry = $manifest[$name] ?? null; - -if ($entry === null) { - sendPlainTextError(404, "Unknown item '$name'"); -} - -if (!is_string($entry['source'] ?? null) || !is_string($entry['target'] ?? null) || !is_string($entry['template'] ?? null)) { - sendPlainTextError(500, "Malformed manifest entry for item '$name'"); -} - -if ($isRawRequest) { - $content = fetchRaw($entry['source']); - - if ($content === false) { - sendPlainTextError(502, "Failed to fetch the file from $repoHost"); - } - - header('Content-Type: application/octet-stream'); - echo $content; - exit; -} - -$variables = [ - 'SOURCE_URL' => rtrim(PUBLIC_BASE_URL, '/') . '/raw/' . rawurlencode($name), - 'TARGET_PATH' => resolveHomePath($entry['target']), - 'POST_INSTALL' => $entry['post_install'] ?? '', -]; - -$script = renderScriptTemplate($entry['template'], $variables); - -if ($script === false) { - sendPlainTextError(500, 'Template for this item is missing'); -} - -header('Content-Type: text/x-shellscript; charset=utf-8'); -echo $script; +handleInstallRequest($name); diff --git a/app/src/functions.php b/app/src/functions.php new file mode 100644 index 0000000..4870415 --- /dev/null +++ b/app/src/functions.php @@ -0,0 +1,175 @@ + $value) { + $content = str_replace('{{' . $key . '}}', $value, $content); + } + + return $content; +} + +function readRepoFile(string $relativePath): string|false +{ + $path = REPO_ROOT_DIRECTORY . '/' . ltrim($relativePath, '/'); + + if (!is_file($path)) { + return false; + } + + $content = file_get_contents($path); + + return is_string($content) ? $content : false; +} + +function loadManifest(): array +{ + $manifestYaml = readRepoFile(MANIFEST_PATH); + + if ($manifestYaml === false) { + sendPlainTextError(500, 'Failed to read the manifest'); + } + + $manifest = yaml_parse($manifestYaml); + + if ($manifest === false) { + sendPlainTextError(500, 'Failed to parse the manifest'); + } + + return $manifest; +} + +function findManifestEntry(string $name): array +{ + $manifest = loadManifest(); + + $entry = $manifest[$name] ?? null; + + if ($entry === null) { + sendPlainTextError(404, "Unknown item '$name'"); + } + + if (!is_string($entry['source'] ?? null) || !is_string($entry['target'] ?? null) || !is_string($entry['template'] ?? null)) { + sendPlainTextError(500, "Malformed manifest entry for item '$name'"); + } + + return $entry; +} + +function resolvePublicBaseUrl(): string +{ + $publicBaseUrl = getenv('PUBLIC_BASE_URL'); + + if ($publicBaseUrl === false || $publicBaseUrl === '') { + sendPlainTextError(500, 'PUBLIC_BASE_URL is not configured'); + } + + return $publicBaseUrl; +} + +function resolveRequestedName(): string +{ + $requestPath = parse_url($_SERVER['REQUEST_URI'] ?? '/', PHP_URL_PATH) ?? '/'; + + return trim(rawurldecode($requestPath), '/'); +} + +function handleIndexRequest(): never +{ + $text = renderTemplate(TEMPLATES_DIRECTORY . '/index.tpl', [ + 'PUBLIC_BASE_URL' => rtrim(PUBLIC_BASE_URL, '/'), + ]); + + if ($text === false) { + sendPlainTextError(500, 'Index template is missing'); + } + + header('Content-Type: text/plain; charset=utf-8'); + echo $text; + exit; +} + +function handleListRequest(): never +{ + $names = array_keys(loadManifest()); + sort($names); + + header('Content-Type: text/plain; charset=utf-8'); + echo "Available configurations:\n"; + + foreach ($names as $name) { + echo " $name\n"; + } + + exit; +} + +function handleRawRequest(string $name): never +{ + $entry = findManifestEntry($name); + + $content = readRepoFile($entry['source']); + + if ($content === false) { + sendPlainTextError(500, "Failed to read the file for item '$name'"); + } + + header('Content-Type: application/octet-stream'); + echo $content; + exit; +} + +function handleInstallRequest(string $name): never +{ + $entry = findManifestEntry($name); + + $variables = [ + 'SOURCE_URL' => rtrim(PUBLIC_BASE_URL, '/') . '/raw/' . rawurlencode($name), + 'TARGET_PATH' => resolveHomePath($entry['target']), + 'POST_INSTALL' => $entry['post_install'] ?? '', + ]; + + $script = renderTemplate(TEMPLATES_DIRECTORY . '/' . $entry['template'] . '.sh.tpl', $variables); + + if ($script === false) { + sendPlainTextError(500, 'Template for this item is missing'); + } + + header('Content-Type: text/x-shellscript; charset=utf-8'); + echo $script; + exit; +} diff --git a/app/templates/index.tpl b/app/templates/index.tpl new file mode 100644 index 0000000..b5b1735 --- /dev/null +++ b/app/templates/index.tpl @@ -0,0 +1,17 @@ +# GET fnet.cz + +Fast access to configurations and installation scripts. + +## Usage + +Install a configuration: + + curl -fsSL {{PUBLIC_BASE_URL}}/ | bash + +List all available configurations: + + curl -fsSL {{PUBLIC_BASE_URL}}/list + +Download the raw source file instead of the install script: + + curl -fsSL {{PUBLIC_BASE_URL}}/raw/ diff --git a/compose.yaml b/compose.yaml index 5b3a6da..ee72dd5 100644 --- a/compose.yaml +++ b/compose.yaml @@ -2,7 +2,6 @@ services: get: build: . environment: - REPO_RAW_BASE_URL: https://git.hell/lachtan/get/raw/branch/master/ PUBLIC_BASE_URL: https://get.fnet.cz/ ports: - "8082:80"