-
Notifications
You must be signed in to change notification settings - Fork 11
feat: add custom plugin to generate index.php during build process #10
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,167 @@ | ||||||||||||||||||
| import fs from "fs"; | ||||||||||||||||||
| import path from "path"; | ||||||||||||||||||
|
|
||||||||||||||||||
| export function generateIndexPHP(buildDir) { | ||||||||||||||||||
| const htmlFile = path.join(buildDir, "index.html"); | ||||||||||||||||||
| const phpFile = path.join(buildDir, "index.php"); | ||||||||||||||||||
|
|
||||||||||||||||||
| try { | ||||||||||||||||||
| // Read the generated index.html file | ||||||||||||||||||
| const htmlContent = fs.readFileSync(htmlFile, "utf-8"); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Remove the Jinja2 template script block (will be replaced with PHP version) | ||||||||||||||||||
| const htmlWithoutJinja = htmlContent.replace( | ||||||||||||||||||
| /<!-- Jinja2 embedded initial data[\s\S]*?<\/script>/, | ||||||||||||||||||
| "<!-- PHP_INITIAL_DATA_PLACEHOLDER -->", | ||||||||||||||||||
| ); | ||||||||||||||||||
|
|
||||||||||||||||||
| // PHP Template with embedded HTML content and server-side data fetching | ||||||||||||||||||
| const phpTemplate = `<?php | ||||||||||||||||||
|
|
||||||||||||||||||
| // Ensure the HTTP_USER_AGENT is set; if not, redirect to the home page | ||||||||||||||||||
| if (empty($_SERVER['HTTP_USER_AGENT'])) { | ||||||||||||||||||
| header('Location: /'); | ||||||||||||||||||
| exit(); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Check PHP version for str_contains function | ||||||||||||||||||
| if (!function_exists('str_contains')) { | ||||||||||||||||||
| die('Please upgrade your PHP version to 8.0 or above'); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| $isHtmlRequest = str_contains($_SERVER['HTTP_ACCEPT'] ?? '', 'text/html'); | ||||||||||||||||||
| define('BASE_URL', 'https://yourdomain.com:443'); // Set the appropriate URL | ||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
♻️ Suggested change-export function generateIndexPHP(buildDir) {
+export function generateIndexPHP(buildDir, options = {}) {
+ const baseUrl = options.baseUrl ?? process.env.SUBSCRIPTION_BASE_URL;
+ if (!baseUrl) {
+ throw new Error(
+ "generateIndexPHP: baseUrl is required (set SUBSCRIPTION_BASE_URL env var or pass options.baseUrl).",
+ );
+ }
...
- define('BASE_URL', 'https://yourdomain.com:443'); // Set the appropriate URL
+ define('BASE_URL', '${baseUrl}'); // injected at build timeIf keeping the placeholder is intentional for downstream patching, document this clearly in the README and emit a build-time warning. 🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| // Generate the full URL with the request URI | ||||||||||||||||||
| $infoUrl = BASE_URL . ($_SERVER['REQUEST_URI'] ?? '') . '/info'; | ||||||||||||||||||
| $subUrl = BASE_URL . ($_SERVER['REQUEST_URI'] ?? ''); | ||||||||||||||||||
|
Comment on lines
+36
to
+37
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Use the path component only and re-attach the query where appropriate: 🛡️ Suggested fix- $infoUrl = BASE_URL . ($_SERVER['REQUEST_URI'] ?? '') . '/info';
- $subUrl = BASE_URL . ($_SERVER['REQUEST_URI'] ?? '');
+ $requestUri = $_SERVER['REQUEST_URI'] ?? '';
+ $requestPath = parse_url($requestUri, PHP_URL_PATH) ?? '';
+ $requestQuery = parse_url($requestUri, PHP_URL_QUERY);
+ $querySuffix = $requestQuery !== null ? ('?' . $requestQuery) : '';
+ $infoUrl = BASE_URL . rtrim($requestPath, '/') . '/info' . $querySuffix;
+ $subUrl = BASE_URL . $requestPath . $querySuffix;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| // For HTML requests, fetch user data and embed it for instant loading | ||||||||||||||||||
| if ($isHtmlRequest) { | ||||||||||||||||||
| // Fetch user info | ||||||||||||||||||
| $ch = curl_init(); | ||||||||||||||||||
| curl_setopt_array($ch, [ | ||||||||||||||||||
| CURLOPT_URL => $infoUrl, | ||||||||||||||||||
| CURLOPT_RETURNTRANSFER => true, | ||||||||||||||||||
| CURLOPT_TIMEOUT => 10, | ||||||||||||||||||
| CURLOPT_USERAGENT => $_SERVER['HTTP_USER_AGENT'], | ||||||||||||||||||
| CURLOPT_CUSTOMREQUEST => 'GET', | ||||||||||||||||||
| ]); | ||||||||||||||||||
|
|
||||||||||||||||||
| $infoResponse = curl_exec($ch); | ||||||||||||||||||
| $userData = null; | ||||||||||||||||||
|
|
||||||||||||||||||
| if ($infoResponse !== false) { | ||||||||||||||||||
| $userData = json_decode($infoResponse, true); | ||||||||||||||||||
| } | ||||||||||||||||||
| curl_close($ch); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Fetch subscription links | ||||||||||||||||||
| $ch = curl_init(); | ||||||||||||||||||
| curl_setopt_array($ch, [ | ||||||||||||||||||
| CURLOPT_URL => $subUrl, | ||||||||||||||||||
| CURLOPT_RETURNTRANSFER => true, | ||||||||||||||||||
| CURLOPT_TIMEOUT => 10, | ||||||||||||||||||
| CURLOPT_USERAGENT => 'V2rayNG', | ||||||||||||||||||
| CURLOPT_CUSTOMREQUEST => 'GET', | ||||||||||||||||||
| ]); | ||||||||||||||||||
|
|
||||||||||||||||||
| $linksResponse = curl_exec($ch); | ||||||||||||||||||
| $links = []; | ||||||||||||||||||
|
|
||||||||||||||||||
| if ($linksResponse !== false) { | ||||||||||||||||||
| // Try to decode base64, otherwise split by newlines | ||||||||||||||||||
| $decoded = @base64_decode($linksResponse, true); | ||||||||||||||||||
| $linksText = ($decoded !== false && preg_match('/^(vmess|vless|trojan|ss):\\/\\//', $decoded)) | ||||||||||||||||||
| ? $decoded | ||||||||||||||||||
| : $linksResponse; | ||||||||||||||||||
| $links = array_filter(explode("\\n", trim($linksText)), function($line) { | ||||||||||||||||||
| return !empty($line) && $line !== 'False'; | ||||||||||||||||||
| }); | ||||||||||||||||||
| } | ||||||||||||||||||
| curl_close($ch); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Build the initial data script | ||||||||||||||||||
| $initialDataScript = '<script> | ||||||||||||||||||
| try { | ||||||||||||||||||
| window.__INITIAL_DATA__ = { | ||||||||||||||||||
| user: ' . ($userData ? json_encode($userData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : 'null') . ', | ||||||||||||||||||
| links: ' . json_encode(array_values($links), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . ' | ||||||||||||||||||
| }; | ||||||||||||||||||
| } catch (e) { | ||||||||||||||||||
| console.warn("Failed to parse initial data:", e); | ||||||||||||||||||
| window.__INITIAL_DATA__ = null; | ||||||||||||||||||
| } | ||||||||||||||||||
| </script>'; | ||||||||||||||||||
|
|
||||||||||||||||||
| // Output HTML with embedded initial data | ||||||||||||||||||
| $html = str_replace( | ||||||||||||||||||
| '<!-- PHP_INITIAL_DATA_PLACEHOLDER -->', | ||||||||||||||||||
| $initialDataScript, | ||||||||||||||||||
| '${htmlWithoutJinja.replace(/'/g, "\\'")}' | ||||||||||||||||||
| ); | ||||||||||||||||||
| echo $html; | ||||||||||||||||||
| return; | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+98
to
+105
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unsafe escaping of HTML into a PHP single-quoted string — backslashes will corrupt output.
A safer approach is to embed the HTML via a NOWDOC (which has no escaping at all) using a delimiter unlikely to occur in the content: 🛡️ Suggested fix using NOWDOC- $html = str_replace(
- '<!-- PHP_INITIAL_DATA_PLACEHOLDER -->',
- $initialDataScript,
- '${htmlWithoutJinja.replace(/'/g, "\\'")}'
- );
- echo $html;
+ $html = <<<'CODERABBIT_HTML_EOT'
+${htmlWithoutJinja}
+CODERABBIT_HTML_EOT;
+ echo str_replace(
+ '<!-- PHP_INITIAL_DATA_PLACEHOLDER -->',
+ $initialDataScript,
+ $html
+ );If you keep the single-quoted approach, escape backslashes before quotes: 🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| // For non-HTML requests (subscription clients), proxy the request | ||||||||||||||||||
| $requestUrl = $subUrl; | ||||||||||||||||||
|
|
||||||||||||||||||
| // Initialize cURL session | ||||||||||||||||||
| $ch = curl_init(); | ||||||||||||||||||
| curl_setopt_array($ch, [ | ||||||||||||||||||
| CURLOPT_URL => $requestUrl, | ||||||||||||||||||
| CURLOPT_RETURNTRANSFER => true, | ||||||||||||||||||
| CURLOPT_HEADER => true, | ||||||||||||||||||
| CURLOPT_TIMEOUT => 17, | ||||||||||||||||||
| CURLOPT_USERAGENT => $_SERVER['HTTP_USER_AGENT'], | ||||||||||||||||||
| CURLOPT_CUSTOMREQUEST => 'GET', | ||||||||||||||||||
| ]); | ||||||||||||||||||
|
|
||||||||||||||||||
| $response = curl_exec($ch); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Handle cURL error | ||||||||||||||||||
| if ($response === false) { | ||||||||||||||||||
| die('cURL error: ' . curl_error($ch)); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Split the headers and body from the response | ||||||||||||||||||
| $headerEndPos = strpos($response, "\\r\\n\\r\\n"); | ||||||||||||||||||
| if ($headerEndPos === false) { | ||||||||||||||||||
| die('Invalid response format.'); | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+121
to
+132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Information disclosure & UX: Two issues on the proxy path:
🛡️ Suggested fix- $response = curl_exec($ch);
-
- // Handle cURL error
- if ($response === false) {
- die('cURL error: ' . curl_error($ch));
- }
-
- // Split the headers and body from the response
- $headerEndPos = strpos($response, "\\r\\n\\r\\n");
- if ($headerEndPos === false) {
- die('Invalid response format.');
- }
+ $response = curl_exec($ch);
+ if ($response === false) {
+ error_log('Subscription proxy cURL error: ' . curl_error($ch));
+ http_response_code(502);
+ curl_close($ch);
+ die('Upstream subscription service unavailable.');
+ }
+ $headerEndPos = strpos($response, "\\r\\n\\r\\n");
+ if ($headerEndPos === false) {
+ http_response_code(502);
+ curl_close($ch);
+ die('Upstream returned an invalid response.');
+ }🤖 Prompt for AI Agents |
||||||||||||||||||
|
|
||||||||||||||||||
| $headerText = substr($response, 0, $headerEndPos); | ||||||||||||||||||
| $responseBody = substr($response, $headerEndPos + 4); | ||||||||||||||||||
|
|
||||||||||||||||||
| // Forward the necessary headers from the cURL response | ||||||||||||||||||
| $isValidHeader = false; | ||||||||||||||||||
| foreach (explode("\\r\\n", $headerText) as $i => $line) { | ||||||||||||||||||
| if ($i === 0) continue; | ||||||||||||||||||
| if (strpos($line, ": ") !== false) { | ||||||||||||||||||
| list($key, $value) = explode(": ", $line, 2); | ||||||||||||||||||
| if (in_array(strtolower($key), ['content-disposition', 'content-type', 'subscription-userinfo', 'profile-update-interval'])) { | ||||||||||||||||||
| header("$key: $value"); | ||||||||||||||||||
| $isValidHeader = true; | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| if (!$isValidHeader && !$isHtmlRequest) { | ||||||||||||||||||
| die("Error! No valid headers found."); | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| // Output the response body | ||||||||||||||||||
| echo $responseBody; | ||||||||||||||||||
| curl_close($ch); | ||||||||||||||||||
|
|
||||||||||||||||||
| ?> | ||||||||||||||||||
| `; | ||||||||||||||||||
|
|
||||||||||||||||||
| // Write the index.php file | ||||||||||||||||||
| fs.writeFileSync(phpFile, phpTemplate, "utf-8"); | ||||||||||||||||||
| console.log("Generated index.php successfully"); | ||||||||||||||||||
| } catch (error) { | ||||||||||||||||||
| console.error("Error generating index.php:", error); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Silent failure when the Jinja2 marker is missing.
If
index.htmldoesn't contain the<!-- Jinja2 embedded initial data ... </script>block (e.g., template renamed, Vite output changes, build runs without Jinja preprocessing),htmlWithoutJinjaequals the original HTML, the placeholder never appears, andstr_replaceat runtime is a no-op — the page silently ships withoutwindow.__INITIAL_DATA__. Detect the no-match case and fail the build (or warn loudly) so this regression is caught at CI time.🛡️ Suggested guard
📝 Committable suggestion
🤖 Prompt for AI Agents