Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
167 changes: 167 additions & 0 deletions generate-php.js
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 -->",
);
Comment on lines +13 to +16
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Silent failure when the Jinja2 marker is missing.

If index.html doesn't contain the <!-- Jinja2 embedded initial data ... </script> block (e.g., template renamed, Vite output changes, build runs without Jinja preprocessing), htmlWithoutJinja equals the original HTML, the placeholder never appears, and str_replace at runtime is a no-op — the page silently ships without window.__INITIAL_DATA__. Detect the no-match case and fail the build (or warn loudly) so this regression is caught at CI time.

🛡️ Suggested guard
-    const htmlWithoutJinja = htmlContent.replace(
-      /<!-- Jinja2 embedded initial data[\s\S]*?<\/script>/,
-      "<!-- PHP_INITIAL_DATA_PLACEHOLDER -->",
-    );
+    const jinjaPattern = /<!-- Jinja2 embedded initial data[\s\S]*?<\/script>/;
+    if (!jinjaPattern.test(htmlContent)) {
+      throw new Error(
+        "generateIndexPHP: Jinja2 initial-data marker not found in index.html; cannot inject PHP placeholder.",
+      );
+    }
+    const htmlWithoutJinja = htmlContent.replace(
+      jinjaPattern,
+      "<!-- PHP_INITIAL_DATA_PLACEHOLDER -->",
+    );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const htmlWithoutJinja = htmlContent.replace(
/<!-- Jinja2 embedded initial data[\s\S]*?<\/script>/,
"<!-- PHP_INITIAL_DATA_PLACEHOLDER -->",
);
const jinjaPattern = /<!-- Jinja2 embedded initial data[\s\S]*?<\/script>/;
if (!jinjaPattern.test(htmlContent)) {
throw new Error(
"generateIndexPHP: Jinja2 initial-data marker not found in index.html; cannot inject PHP placeholder.",
);
}
const htmlWithoutJinja = htmlContent.replace(
jinjaPattern,
"<!-- PHP_INITIAL_DATA_PLACEHOLDER -->",
);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@generate-php.js` around lines 13 - 16, The replacement using
htmlContent.replace(...) can silently do nothing when the Jinja2 marker is
missing; update the code that produces htmlWithoutJinja to detect when the regex
didn’t match (compare htmlWithoutJinja === htmlContent or use RegExp.test on
htmlContent with /<!-- Jinja2 embedded initial data[\s\S]*?<\/script>/) and if
there is no match fail fast by logging a clear error and exiting the build
(e.g., processLogger.error or console.error plus process.exit(1)) so CI catches
the missing marker; ensure references to htmlContent, htmlWithoutJinja and the
regex are used so you change the right spot.


// 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

BASE_URL ships as a placeholder — generated index.php is broken by default.

'https://yourdomain.com:443' will be written verbatim into every build artifact. Anyone deploying without manually patching dist/index.php will hit a non-existent host. Make this configurable, e.g., read from an env var at build time:

♻️ 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 time

If keeping the placeholder is intentional for downstream patching, document this clearly in the README and emit a build-time warning.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@generate-php.js` at line 33, The generated constant BASE_URL in
generate-php.js currently writes the hardcoded string
'https://yourdomain.com:443' into dist/index.php; change generate-php.js to
source the value from a build-time environment variable (e.g.,
process.env.BASE_URL) with a sensible fallback (or empty string) and write that
value into the define('BASE_URL', ...) output so builds are not broken by
default; if you intentionally want a placeholder instead, add a clear README
note and emit a build-time console warning from the same generate-php.js path
when the env var is missing.


// 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

REQUEST_URI includes the query string — concatenation produces malformed URLs.

$_SERVER['REQUEST_URI'] is the raw path plus query (e.g., /sub/abc?token=xyz). The current code yields:

  • $infoUrl = https://.../sub/abc?token=xyz/info/info ends up inside the query
  • $subUrl = https://.../sub/abc?token=xyz

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$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;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@generate-php.js` around lines 36 - 37, The current construction of $infoUrl
and $subUrl uses $_SERVER['REQUEST_URI'] which contains the query string and
results in malformed URLs; replace direct use of $_SERVER['REQUEST_URI'] with
the path-only component using parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH)
(falling back to ''), capture the query via $_SERVER['QUERY_STRING'] if needed,
and then build $infoUrl and $subUrl by concatenating BASE_URL with the cleaned
path (use rtrim on the path to avoid double slashes) and re-attaching the query
with '?' only when $_SERVER['QUERY_STRING'] is non-empty so that $infoUrl ends
with '/info' after the path and query parameters remain valid.


// 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Unsafe escaping of HTML into a PHP single-quoted string — backslashes will corrupt output.

htmlWithoutJinja.replace(/'/g, "\\'") only escapes '. PHP single-quoted strings also interpret \\ (literal backslash) and \', so any backslash present in the bundled HTML/JS (e.g., regex literals like /\d/, Windows paths in source maps if ever inlined, escape sequences in inline JSON) will produce a malformed PHP string and likely a parse error in index.php. Backslashes must be escaped first.

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: htmlWithoutJinja.replace(/\\/g, "\\\\").replace(/'/g, "\\'"). Either way, also assert that the chosen NOWDOC delimiter / unique sentinel doesn't appear in htmlWithoutJinja before generating.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@generate-php.js` around lines 98 - 105, The current replacement uses
htmlWithoutJinja.replace(/'/g, "\\'") which fails to escape backslashes and can
corrupt the PHP single-quoted string; update the logic in generate-php.js where
$html is built (the call that replaces '<!-- PHP_INITIAL_DATA_PLACEHOLDER -->'
and uses htmlWithoutJinja) to either (A) switch to embedding the HTML via a PHP
NOWDOC with a guaranteed-unique delimiter (verify the delimiter does not appear
in htmlWithoutJinja before writing) or (B) if keeping the single-quoted string,
first escape backslashes then single quotes (escape "\\" before "'") so
backslashes are doubled before quote-escaping, and ensure the final $html and
echoed output are correct for index.php consumption.


// 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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Information disclosure & UX: die(curl_error(...)) leaks internals; HTML and non-HTML paths handle errors inconsistently.

Two issues on the proxy path:

  1. die('cURL error: ' . curl_error($ch)) echoes raw cURL error strings (potentially including resolved IPs, internal hostnames, TLS chain details) directly to the client. Log server-side instead and return a generic message with a non-200 status.
  2. The HTML branch silently degrades when upstream curl_exec fails (renders with null/empty data), but the non-HTML branch hard-dies. Subscription clients will get a 200 with the error string in the body, which most clients will then parse as the subscription content.
🛡️ 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
Verify each finding against the current code and only fix it if needed.

In `@generate-php.js` around lines 121 - 132, The current proxy path calls
curl_exec($ch) and uses die('cURL error: ' . curl_error($ch)) which leaks
internal details and inconsistently handles HTML vs non-HTML flows; change this
so that after curl_exec($ch) you detect errors by checking $response === false,
then log the detailed curl_error($ch) server-side (use your existing logger) and
return a generic client-facing error with a non-200 HTTP status (e.g., 502)
instead of die(); also update the HTML branch that currently silently renders
null so it too returns an appropriate status and safe fallback content (or empty
body) while avoiding printing raw curl_error output; ensure you reference the
same variables ($response, $ch, $headerEndPos) and keep header/body splitting
behavior intact when $response is valid.


$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);
}
}
12 changes: 11 additions & 1 deletion vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,23 @@ import tailwindcss from "@tailwindcss/vite"
import react from "@vitejs/plugin-react"
import { defineConfig } from "vite"
import { viteSingleFile } from "vite-plugin-singlefile";
// @ts-expect-error generate-php is a JS utility without type declarations.
import { generateIndexPHP } from "./generate-php"

// https://vite.dev/config/
export default defineConfig({
plugins: [
react(),
tailwindcss(),
viteSingleFile()
viteSingleFile(),
{
name: "generate-index-php",
apply: "build",
closeBundle() {
const buildPath = "dist"
generateIndexPHP(buildPath)
},
},
],
resolve: {
alias: {
Expand Down