diff --git a/.DS_Store b/.DS_Store index 96bccd0..5027262 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/backend/app/.env b/api/.env similarity index 100% rename from backend/app/.env rename to api/.env diff --git a/backend/app/README.md b/api/README.md similarity index 88% rename from backend/app/README.md rename to api/README.md index ea4d376..4e15f52 100644 --- a/backend/app/README.md +++ b/api/README.md @@ -6,7 +6,6 @@ This is a PHP-based API using the **Slim 4 Framework** that dynamically manages ## ๐Ÿงฉ Features -- Automatically creates and starts LXD containers per subdomain - CAPTCHA validation for provisioning - Proxies requests to containerized environments - Persists domain-to-container mapping @@ -17,8 +16,7 @@ This is a PHP-based API using the **Slim 4 Framework** that dynamically manages ## ๐Ÿ“ Project Structure -backend/ -โ”œโ”€โ”€ app/ +api/ โ”‚ โ”œโ”€โ”€ src/ โ”‚ โ”‚ โ”œโ”€โ”€ Controllers/ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€ ProxyController.php @@ -39,7 +37,7 @@ backend/ ## โš™๏ธ Requirements -- PHP 8.1+ +- PHP 8.4+ - LXD installed and configured - PHP-FPM + NGINX - Composer @@ -61,13 +59,14 @@ backend/ 3. **Ensure PHP and NGINX are configured** - Check in backend/nginx.conf - +NGINX config example: + ```bash + Please check nginx.conf file in root 4. **Map domain in /etc/hosts** ```bash - 127.0.0.1 test.lxdapp.local + 127.0.0.1 testone.lxdapp.local 5. **Make sure LXD is working** ```bash @@ -75,14 +74,14 @@ backend/ 6. **๐Ÿ” CAPTCHA Protection** - The /api/v1/proxy POST endpoint expects a field panswer with the correct CAPTCHA. + The /api/ POST endpoint expects a field panswer with the correct CAPTCHA. You can configure PCaptcha class for custom logic. 7. **๐Ÿงช API Usage** - POST /api/v1/proxy + POST /api **Request Body:** ```bash @@ -93,7 +92,7 @@ backend/ **Headers:** - Origin: http://customer.lxdapp.local + Origin: http://testone.lxdapp.local **Response:** ```bash diff --git a/backend/app/composer.json b/api/composer.json similarity index 100% rename from backend/app/composer.json rename to api/composer.json diff --git a/api/config.json b/api/config.json new file mode 100644 index 0000000..82f42f1 --- /dev/null +++ b/api/config.json @@ -0,0 +1,3 @@ +{ + "lxdapp.local": "testone" +} \ No newline at end of file diff --git a/backend/app/nginx.conf b/api/nginx.conf similarity index 71% rename from backend/app/nginx.conf rename to api/nginx.conf index 9552d7a..b957e29 100644 --- a/backend/app/nginx.conf +++ b/api/nginx.conf @@ -2,7 +2,7 @@ server { listen 80; server_name *.lxdapp.local lxdapp.local; - root /var/www/html/lxd-app/backend/app/public; # Adjust this to your Slim project's public folder + root /var/www/html/lxd-app; # Adjust this to your Slim project's public folder index index.php index.html index.htm; # Reverse proxy to Vue.js for /app/ route @@ -17,20 +17,18 @@ server { # rewrite ^/app/(/.*)$ $1 break; } - # (Optional: Serve static assets directly if needed) - # location /app/_nuxt/ { - # proxy_pass http://127.0.0.1:3000; - # } - # Handle PHP Slim location / { - try_files $uri $uri/ /index.php?$query_string; + # try_files $uri $uri/ /index.php?$query_string; + try_files $uri /index.php?$query_string; } + # Pass PHP scripts to PHP-FPM - location ~ \.php$ { + #location ~ \.php$ { + location = /index.php { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.4-fpm.sock; - fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_param SCRIPT_FILENAME $document_root/index.php; include fastcgi_params; } diff --git a/backend/app/public/.htaccess b/api/public/.htaccess similarity index 100% rename from backend/app/public/.htaccess rename to api/public/.htaccess diff --git a/api/public/index.php b/api/public/index.php new file mode 100644 index 0000000..1a7b1ae --- /dev/null +++ b/api/public/index.php @@ -0,0 +1,91 @@ +load(); + +$domain = $_ENV['MAIN_COOKIE_DOMAIN'] ?? '.lxdapp.local'; +session_set_cookie_params([ + 'lifetime' => 0, + 'path' => '/', + 'domain' => $domain, + 'secure' => false, // set true if using HTTPS + 'httponly' => true, + 'samesite' => 'Lax', +]); + +if (session_status() === PHP_SESSION_NONE) { + session_start(); +} + +// Build Container using PHP-DI +$containerBuilder = new ContainerBuilder(); +$containerBuilder->useAutowiring(true); // Enable autowiring globally + + +// Add settings +$settings = require __DIR__ . '/../src/Settings/Settings.php'; +$settings($containerBuilder); + +// Add dependencies +$dependencies = require __DIR__ . '/../src/Dependencies/Dependencies.php'; +$dependencies($containerBuilder); + +// Build the container +$container = $containerBuilder->build(); + +// Set container to AppFactory +AppFactory::setContainer($container); + +// Create App +$app = AppFactory::create(); + +// ๐Ÿ”น CORS middleware +$app->add(CorsMiddleware::class); + + +// Register middleware +(require __DIR__ . '/../src/Bootstrap/Middleware.php')($app); + + +// Register routes + +// API contianer proxy route +$app->group('/api', function ($group) { + $group->get('/captcha', [CaptchaController::class, 'get']); + $group->post('/login', [LoginController::class, 'index']); + $group->get('/status', [LoginController::class, 'status']); + +}); + +/** + * JSON response helper + */ +function jsonResponse(Response $response, array $data, int $status = 200): Response { + $payload = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); + $response->getBody()->write($payload); + + return $response + ->withHeader('Content-Type', 'application/json') + ->withHeader('Access-Control-Allow-Origin', '*') + ->withStatus($status); +} + +// Run app +$app->run(); diff --git a/backend/app/src/Bootstrap/middleware.php b/api/src/Bootstrap/Middleware.php similarity index 100% rename from backend/app/src/Bootstrap/middleware.php rename to api/src/Bootstrap/Middleware.php diff --git a/backend/app/src/Controllers/CaptchaController.php b/api/src/Controllers/CaptchaController.php similarity index 100% rename from backend/app/src/Controllers/CaptchaController.php rename to api/src/Controllers/CaptchaController.php diff --git a/backend/app/src/Controllers/LoginController.php b/api/src/Controllers/LoginController.php similarity index 51% rename from backend/app/src/Controllers/LoginController.php rename to api/src/Controllers/LoginController.php index b07683a..e7ca393 100644 --- a/backend/app/src/Controllers/LoginController.php +++ b/api/src/Controllers/LoginController.php @@ -48,8 +48,9 @@ class LoginController // Write log LogWriterHelper::write($name, $request->getUri()); + $redirect = '/waiting?name='.$name .'&redirect='.urlencode($params['redirect']); // Login success - return $this->json($response, ['status' => 'success', 'message' => 'Container started!']); + return $this->json($response, ['status' => 'success', 'message' => 'Container started!', 'redirect' => $redirect]); } catch (\Throwable $e) { return $this->json($response, [ 'status' => 'error', @@ -58,6 +59,68 @@ class LoginController } } + public function status(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface + { + $queryParams = $request->getQueryParams(); + $name = $queryParams['name'] ?? ''; + + if (empty($name)) { + return $this->json($response, [ + 'status' => 'error', + 'message' => 'Missing container name.', + ], 400); + } + + $lxd = new LxdService(); + + $state = $lxd->getContainerState($name); + if (!$state) { + return $this->json($response, [ + 'status' => 'not_found', + 'message' => 'Container not found', + ], 404); + } + + $status = $state['metadata']['status'] ?? 'Stopped'; + if ($status !== 'Running') { + return $this->json($response, [ + 'status' => 'starting', + 'message' => 'Container is not yet running', + ]); + } + + $ip = $lxd->getContainerIP($name); + $nginx = $lxd->isServiceRunning($name, 'nginx'); + $mysql = $lxd->isServiceRunning($name, 'mysql'); + if ($ip && $nginx === 'active' && $mysql === 'active') { + return $this->json($response, [ + 'status' => 'ready', + 'ip' => $ip, + 'message' => 'Container is ready', + ]); + } + + if($nginx === 'failed'){ + return $this->json($response, [ + 'status' => 'failed', + 'message' => 'Failed to start web server service in container', + ], 400); + } + + if($mysql === 'failed'){ + return $this->json($response, [ + 'status' => 'failed', + 'message' => 'Failed to start mysql service in container', + ], 400); + } + + return $this->json($response, [ + 'status' => 'running', + 'message' => 'Container is running, waiting for services...', + ]); + } + + /** * Sends a JSON response. */ diff --git a/backend/app/src/Dependencies/dependencies.php b/api/src/Dependencies/Dependencies.php similarity index 100% rename from backend/app/src/Dependencies/dependencies.php rename to api/src/Dependencies/Dependencies.php diff --git a/backend/app/src/lib/PCaptcha.php b/api/src/Lib/PCaptcha.php similarity index 100% rename from backend/app/src/lib/PCaptcha.php rename to api/src/Lib/PCaptcha.php diff --git a/backend/app/src/lib/arial.ttf b/api/src/Lib/arial.ttf similarity index 100% rename from backend/app/src/lib/arial.ttf rename to api/src/Lib/arial.ttf diff --git a/backend/app/src/Middleware/CorsMiddleware.php b/api/src/Middleware/CorsMiddleware.php similarity index 100% rename from backend/app/src/Middleware/CorsMiddleware.php rename to api/src/Middleware/CorsMiddleware.php diff --git a/backend/app/src/scripts/auto-stop-containers.php b/api/src/Scripts/auto-stop-containers.php similarity index 100% rename from backend/app/src/scripts/auto-stop-containers.php rename to api/src/Scripts/auto-stop-containers.php diff --git a/backend/app/src/Services/LxdService.php b/api/src/Services/LxdService.php similarity index 78% rename from backend/app/src/Services/LxdService.php rename to api/src/Services/LxdService.php index 69135df..54699ac 100644 --- a/backend/app/src/Services/LxdService.php +++ b/api/src/Services/LxdService.php @@ -22,10 +22,9 @@ class LxdService * @throws Exception */ private function request(string $method, string $endpoint, array $body = []): array { - - if (!isset($_ENV['LXD_CLIENT_CERT'], $_ENV['LXD_CLIENT_KEY'])) { - throw new \Exception("LXD_CLIENT_CERT and LXD_CLIENT_KEY must be set in .env"); - } + // if (!isset($_ENV['LXD_CLIENT_CERT'], $_ENV['LXD_CLIENT_KEY'])) { + // throw new \Exception("LXD_CLIENT_CERT and LXD_CLIENT_KEY must be set in .env"); + // } $ch = curl_init("{$this->baseUrl}{$endpoint}"); @@ -78,6 +77,7 @@ class LxdService return $this->request('GET', "/1.0/instances/$name/state"); } catch (\Throwable $e) { return null; + // echo 'Error: ' . $e->getMessage(); } } @@ -99,7 +99,6 @@ class LxdService * @throws Exception */ public function startContainer(string $name): array { - $startResponse = $this->request('PUT', "/1.0/instances/$name/state", [ "action" => "start", "timeout" => 30, @@ -107,41 +106,10 @@ class LxdService ]); if (!isset($startResponse['operation'])) { - throw new \Exception("No operation returned from start request"); + throw new \Exception("Failed to start container, please try again."); } - $startOpUrl = $startResponse['operation']; - - // 5. Wait for start operation to complete - do { - sleep(1); - $startOpResp = $this->request('GET', $startOpUrl); - $startStatus = $startOpResp['metadata']['status_code'] ?? 0; - } while ($startStatus < 200); - - if ($startStatus >= 400) { - throw new \Exception("Failed to start container, Please try again."); - } - - sleep(5); - - // Poll services (Nginx and MySQL) for up to 30 seconds - $maxRetries = 30; - $retry = 0; - - while ($retry < $maxRetries) { - $nginxReady = $this->isServiceRunning($name, 'nginx'); - $mysqlReady = $this->isServiceRunning($name, 'mysql'); - - if ($nginxReady && $mysqlReady) { - return $startResponse; // โœ… All good - } - - $retry++; - sleep(1); - } - - throw new \Exception("Container started but services not ready."); + return $startResponse; } /** @@ -194,11 +162,11 @@ class LxdService return null; } - public function isServiceRunning(string $container, string $service): bool + public function isServiceRunning(string $container, string $service): string { $lxcPath = $_ENV['LXC_PATH'] ?: 'lxc'; $cmd = "$lxcPath exec {$container} -- systemctl is-active {$service} 2>&1"; $output = shell_exec($cmd); - return trim($output) === 'active'; + return trim($output); } } \ No newline at end of file diff --git a/backend/app/src/Settings/settings.php b/api/src/Settings/Settings.php similarity index 100% rename from backend/app/src/Settings/settings.php rename to api/src/Settings/Settings.php diff --git a/backend/app/src/Utils/LogWriterHelper.php b/api/src/Utils/LogWriterHelper.php similarity index 100% rename from backend/app/src/Utils/LogWriterHelper.php rename to api/src/Utils/LogWriterHelper.php diff --git a/frontend/.gitignore b/app/.gitignore similarity index 100% rename from frontend/.gitignore rename to app/.gitignore diff --git a/frontend/README.md b/app/README.md similarity index 56% rename from frontend/README.md rename to app/README.md index 535a6ec..5bbd13b 100644 --- a/frontend/README.md +++ b/app/README.md @@ -6,7 +6,7 @@ This is the **frontend Nuxt.js** application that works with the [LXD Proxy API] ## ๐ŸŒ Overview -- Each user has a subdomain (e.g., `mitul.lxdapp.local`) +- Each user has a subdomain (e.g., `testone.lxdapp.local`) - On visiting the subdomain, a login page prompts for CAPTCHA - Upon successful CAPTCHA, a request is made to the backend to: - Create or start an LXD container @@ -20,7 +20,7 @@ This is the **frontend Nuxt.js** application that works with the [LXD Proxy API] - Subdomain-aware dynamic environment provisioning - CAPTCHA login screen for triggering container creation - API integration with backend (Slim PHP) service -- Axios-based POST to `/api/v1/proxy` +- Axios-based POST to `/api/` --- @@ -34,11 +34,16 @@ This is the **frontend Nuxt.js** application that works with the [LXD Proxy API] ```bash npm run dev +3. **Visit the subdomain** + + http://testone.lxdapp.local:3000 + + โš ๏ธ Make sure this domain is mapped in /etc/hosts and that the backend is running on the correct origin. **๐Ÿ—‚ Project Structure** -frontend/ +app/ โ”œโ”€โ”€ pages/ โ”‚ โ””โ”€โ”€ index.vue # Login screen โ”œโ”€โ”€ plugins/ @@ -49,3 +54,39 @@ frontend/ โ”œโ”€โ”€ package.json โ””โ”€โ”€ README.md + +**๐Ÿ” CAPTCHA Flow** + +User opens subdomain: testone.lxdapp.local + +Enters the CAPTCHA value + +Form submits: + +POST /api/login with JSON payload: + + { + "source": "login", + "panswer": "abc123" + } + +Adds header: + + Origin: http://testone.lxdapp.local + +**Backend:** + + Validates CAPTCHA + + Creates or starts container + + Responds with container IP proxy + + +**๐Ÿงช Development Notes** + + config.json in backend maps subdomain to LXD container + + This frontend app assumes the backend is on the same subdomain + + If needed, configure proxy in nuxt.config.js or use relative API paths \ No newline at end of file diff --git a/frontend/app.vue b/app/app.vue similarity index 100% rename from frontend/app.vue rename to app/app.vue diff --git a/app/assets/css/common.css b/app/assets/css/common.css new file mode 100644 index 0000000..80ee3eb --- /dev/null +++ b/app/assets/css/common.css @@ -0,0 +1,33 @@ +*{ + box-sizing: border-box; +} +body{ + position: relative; + padding: 0px; + margin: 0px; + overflow-x: hidden; +} +.page-wrapper { + width: 100%; + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 0; + margin: 0; + text-align: center; +} +.title { + text-align: center; + margin-bottom: 1.5rem; + font-weight: 700; + font-size: 1.8rem; + color: #222; +} +.error-text { + color: red; + font-size: 0.95rem; + margin-bottom: 1rem; + text-align: center; +} \ No newline at end of file diff --git a/frontend/components/Captcha.vue b/app/components/Captcha.vue similarity index 100% rename from frontend/components/Captcha.vue rename to app/components/Captcha.vue diff --git a/frontend/components/Spinner.vue b/app/components/Spinner.vue similarity index 100% rename from frontend/components/Spinner.vue rename to app/components/Spinner.vue diff --git a/frontend/nuxt.config.ts b/app/nuxt.config.ts similarity index 72% rename from frontend/nuxt.config.ts rename to app/nuxt.config.ts index d736bce..87f5cf8 100644 --- a/frontend/nuxt.config.ts +++ b/app/nuxt.config.ts @@ -13,13 +13,6 @@ export default defineNuxtConfig({ apiUrl: `${process.env.BACKEND_URL}/api` } }, - nitro: { - prerender: { - crawlLinks: true, - routes: ['/', '/login'], // Add known routes - ignore: ['/*.php'], // Or leave empty to catch with [...slug].vue - } - }, css: [ '~/assets/css/common.css' // Path to your global CSS file ], diff --git a/frontend/package.json b/app/package.json similarity index 100% rename from frontend/package.json rename to app/package.json diff --git a/frontend/pages/index.vue b/app/pages/index.vue similarity index 85% rename from frontend/pages/index.vue rename to app/pages/index.vue index 2e133f9..9ad7fd2 100644 --- a/frontend/pages/index.vue +++ b/app/pages/index.vue @@ -78,7 +78,9 @@ onMounted(() => { const submitForm = async () => { loading.value = true; output.value = ''; - const config = useRuntimeConfig(); + const encodedRedirect = new URL(redirectTo.value, window.location.origin); + encodedRedirect.searchParams.set('username', username.value); + encodedRedirect.searchParams.set('password', password.value); try { const res = await $fetch(`${window.location.origin}/api/login`, { @@ -91,17 +93,16 @@ const submitForm = async () => { password: password.value, source: 'login', panswer: captchaValue.value, - redirect: redirectTo.value, + redirect: encodedRedirect.toString() }, credentials: 'include', throwHttpErrors: false, // important: do NOT throw on 401/4xx }); captchaError.value = ''; - const encodedRedirect = new URL(redirectTo.value, window.location.origin); - encodedRedirect.searchParams.set('username', username.value); - encodedRedirect.searchParams.set('password', password.value); - window.location.href = encodedRedirect.toString(); + + router.push(res.redirect); + // window.location.href = encodedRedirect.toString(); } catch (error) { if (error.statusCode === 400) { captchaError.value = error?.data?.message || 'Invalid CAPTCHA'; @@ -121,17 +122,6 @@ const submitForm = async () => { + + diff --git a/frontend/plugins/fontawesome.client.ts b/app/plugins/fontawesome.client.ts similarity index 100% rename from frontend/plugins/fontawesome.client.ts rename to app/plugins/fontawesome.client.ts diff --git a/frontend/public/favicon.ico b/app/public/favicon.ico similarity index 100% rename from frontend/public/favicon.ico rename to app/public/favicon.ico diff --git a/frontend/public/robots.txt b/app/public/robots.txt similarity index 100% rename from frontend/public/robots.txt rename to app/public/robots.txt diff --git a/frontend/server/tsconfig.json b/app/server/tsconfig.json similarity index 100% rename from frontend/server/tsconfig.json rename to app/server/tsconfig.json diff --git a/frontend/tsconfig.json b/app/tsconfig.json similarity index 100% rename from frontend/tsconfig.json rename to app/tsconfig.json diff --git a/backend/app/config.json b/backend/app/config.json deleted file mode 100644 index 623df91..0000000 --- a/backend/app/config.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "testone.lxdapp.local": "testone" -} \ No newline at end of file diff --git a/backend/app/public/index.php b/backend/app/public/index.php deleted file mode 100644 index 70a3654..0000000 --- a/backend/app/public/index.php +++ /dev/null @@ -1,166 +0,0 @@ -load(); - -$domain = $_ENV['MAIN_COOKIE_DOMAIN'] ?? '.lxdapp.local'; -session_set_cookie_params([ - 'lifetime' => 0, - 'path' => '/', - 'domain' => $domain, - 'secure' => false, // set true if using HTTPS - 'httponly' => true, - 'samesite' => 'Lax', -]); - -if (session_status() === PHP_SESSION_NONE) { - session_start(); -} - -// Build Container using PHP-DI -$containerBuilder = new ContainerBuilder(); -$containerBuilder->useAutowiring(true); // Enable autowiring globally - - -// Add settings -$settings = require __DIR__ . '/../src/Settings/Settings.php'; -$settings($containerBuilder); - -// Add dependencies -$dependencies = require __DIR__ . '/../src/Dependencies/Dependencies.php'; -$dependencies($containerBuilder); - -// Build the container -$container = $containerBuilder->build(); - -// Set container to AppFactory -AppFactory::setContainer($container); - -// Create App -$app = AppFactory::create(); - -// ๐Ÿ”น CORS middleware -$app->add(CorsMiddleware::class); - - -// Register middleware -(require __DIR__ . '/../src/Bootstrap/Middleware.php')($app); - - -// Register routes - -// API contianer proxy route -$app->group('/api', function ($group) { - $group->get('/captcha', [CaptchaController::class, 'get']); - $group->post('/login', [LoginController::class, 'index']); - -}); - -$app->any('/{routes:.*}', function (Request $request, Response $response, array $args) { - try { - $mainDomain = $_ENV['MAIN_DOMAIN'] ?? 'lxdapp.local'; - - $origin = $request->getHeaderLine('Origin'); - if (!empty($origin)) { - $domain = parse_url($origin, PHP_URL_HOST); - } else { - $domain = $request->getHeaderLine('Host'); - } - - $configPath = __DIR__ . '/../config.json'; - $config = file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : []; - $name = $config[$domain] ?? null; - - $lxd = new LxdService(); - - if (!$name) { - return jsonResponse($response, ['status' => 'error', 'message' => 'Container does not exist.'], 404); - } - - if (!$lxd->containerExists($name)) { - return jsonResponse($response, ['status' => 'error', 'message' => 'Container does not exist.'], 404); - } - - $containerInfo = $lxd->getContainerState($name); - $status = $containerInfo['metadata']['status'] ?? 'Stopped'; - - if ($status !== 'Running') { - $redirectUrl = (string) $request->getUri(); - return $response - ->withHeader('Location', 'app/?auth=ok&redirect=' . urlencode($redirectUrl)) - ->withStatus(302); - } - - $ip = $lxd->getContainerIP($name); - if (!$ip) { - return jsonResponse($response, ['status' => 'error', 'message' => 'Could not fetch container IP'], 500); - } - - // BEGIN Proxy logic - $baseUrl = "http://$ip/"; - $uri = $request->getUri(); - $path = $uri->getPath(); - - $prefix = '/api/'; - if (strpos($path, $prefix) === 0) { - $path = substr($path, strlen($prefix)); - if ($path === '') { - $path = '/'; - } - } - - $query = $uri->getQuery(); - $targetUrl = $baseUrl . $path . ($query ? '?' . $query : ''); - - Proxy::$AUTH_KEY = $_ENV['AUTH_KEY'] ?? 'YOUR_DEFAULT_AUTH_KEY'; - Proxy::$ENABLE_AUTH = true; - Proxy::$HEADER_HTTP_PROXY_AUTH = 'HTTP_PROXY_AUTH'; - - $_SERVER['HTTP_PROXY_AUTH'] = Proxy::$AUTH_KEY; - $_SERVER['HTTP_PROXY_TARGET_URL'] = $targetUrl; - - $responseCode = Proxy::run(); - - LogWriterHelper::write($name, $targetUrl); - - return $response; - } catch (\Throwable $e) { - return jsonResponse($response, [ - 'status' => 'error', - 'message' => 'Internal Server Error: ' . $e->getMessage() - ], 500); - } -}); - -/** - * JSON response helper - */ -function jsonResponse(Response $response, array $data, int $status = 200): Response { - $payload = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); - $response->getBody()->write($payload); - - return $response - ->withHeader('Content-Type', 'application/json') - ->withHeader('Access-Control-Allow-Origin', '*') - ->withStatus($status); -} - -// Run app -$app->run(); diff --git a/frontend/assets/css/common.css b/frontend/assets/css/common.css deleted file mode 100644 index 17d56ce..0000000 --- a/frontend/assets/css/common.css +++ /dev/null @@ -1,9 +0,0 @@ -*{ - box-sizing: border-box; -} -body{ - position: relative; - padding: 0px; - margin: 0px; - overflow-x: hidden; -} \ No newline at end of file diff --git a/index.php b/index.php new file mode 100644 index 0000000..f3b96e2 --- /dev/null +++ b/index.php @@ -0,0 +1,92 @@ +containerExists($container)) { + redirect($redirectBase); +} + +// === Check container status === +$state = $lxd->getContainerState($container)['metadata']['status'] ?? 'Stopped'; + +if ($state !== 'Running') { + redirect($redirectBase); +} + +// === Get container IP === +$ip = $lxd->getContainerIP($container); +if (!$ip) { + redirect($waitingPage); +} + +// === Proxy to container === +proxy($container, "http://{$ip}{$requestUri}"); +exit; + + +// === Functions === + +function redirect(string $to): void { + header("Location: $to", true, 302); + exit; +} + +function proxy(string $name, string $targetUrl): void { + Proxy::$AUTH_KEY = $_ENV['AUTH_KEY'] ?? 'YOUR_DEFAULT_AUTH_KEY'; + Proxy::$ENABLE_AUTH = true; + Proxy::$HEADER_HTTP_PROXY_AUTH = 'HTTP_PROXY_AUTH'; + + $_SERVER['HTTP_PROXY_AUTH'] = Proxy::$AUTH_KEY; + $_SERVER['HTTP_PROXY_TARGET_URL'] = $targetUrl; + + $responseCode = Proxy::run(); + writeLog($name, $targetUrl); +} + +function getFullUrl(): string { + $protocol = (!empty($_SERVER['HTTPS']) || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://"; + $host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME']; + return $protocol . $host . $_SERVER['REQUEST_URI']; +} + +function parseUrl(string $url = ''): string { + $scheme = $_SERVER['REQUEST_SCHEME'] ?? 'http'; + return "$scheme://$url"; +} + +function writeLog(string $name, string $uri): void { + $logDir = __DIR__ . '/api/public/last-access-logs'; + + if (!is_dir($logDir)) { + mkdir($logDir, 0777, true); + } + + $logLine = date("Y-m-d H:i:s") . " : " . $uri . "\n"; + file_put_contents("$logDir/$name.txt", $logLine, FILE_APPEND); +}