diff --git a/.DS_Store b/.DS_Store index dd8bcdf..96bccd0 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/backend/app/.env b/backend/app/.env index 0609530..9e7c528 100644 --- a/backend/app/.env +++ b/backend/app/.env @@ -1,5 +1,6 @@ MAIN_DOMAIN=lxdapp.local MAIN_COOKIE_DOMAIN=.lxdapp.local +LXC_PATH=/snap/bin/lxc LXD_API_URL=https://localhost:8443 LXD_CLIENT_CERT=/etc/ssl/lxdapp/client.crt LXD_CLIENT_KEY=/etc/ssl/lxdapp/client.key diff --git a/backend/app/public/index.php b/backend/app/public/index.php index b66272f..70a3654 100644 --- a/backend/app/public/index.php +++ b/backend/app/public/index.php @@ -6,11 +6,13 @@ use DI\ContainerBuilder; use Slim\Factory\AppFactory; use Dotenv\Dotenv; use App\Middleware\CorsMiddleware; -use Psr\Http\Message\ServerRequestInterface; -use Psr\Http\Message\ResponseInterface; +use Psr\Http\Message\ServerRequestInterface as Request; +use Psr\Http\Message\ResponseInterface as Response; use App\Controllers\CaptchaController; -use App\Controllers\ProxyController; use App\Controllers\LoginController; +use App\Services\LxdService; +use Zounar\PHPProxy\Proxy; +use App\Utils\LogWriterHelper; require __DIR__ . '/../vendor/autoload.php'; @@ -71,7 +73,94 @@ $app->group('/api', function ($group) { }); -$app->any('/{routes:.*}', [ProxyController::class, 'forward']); +$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/backend/app/public/last-access-logs/testone.txt b/backend/app/public/last-access-logs/testone.txt deleted file mode 100644 index 0c27550..0000000 --- a/backend/app/public/last-access-logs/testone.txt +++ /dev/null @@ -1,27 +0,0 @@ -2025-07-15 18:59:58 : http://lxdapp.local/api/login -2025-07-15 18:59:59 : http://10.110.90.24//demo -2025-07-15 19:00:26 : http://10.110.90.24// -2025-07-15 19:01:46 : http://lxdapp.local/api/login -2025-07-15 19:01:47 : http://10.110.90.24// -2025-07-15 19:02:21 : http://10.110.90.24// -2025-07-15 19:04:09 : http://lxdapp.local/api/login -2025-07-15 19:04:10 : http://10.110.90.24// -2025-07-15 19:11:18 : http://lxdapp.local/api/login -2025-07-15 19:11:18 : http://10.110.90.24// -2025-07-15 19:12:44 : http://10.110.90.24//sdfsd -2025-07-15 19:13:12 : http://10.110.90.24// -2025-07-15 19:17:17 : http://10.110.90.24// -2025-07-15 19:25:26 : http://testone.lxdapp.local/api/login -2025-07-15 19:25:28 : http://10.110.90.24// -2025-07-16 09:15:04 : http://10.110.90.24// -2025-07-16 09:21:18 : http://10.110.90.24// -2025-07-16 09:22:08 : http://testone.lxdapp.local/api/login -2025-07-16 09:22:09 : http://10.110.90.24// -2025-07-16 09:30:13 : http://10.110.90.24// -2025-07-16 09:32:41 : http://testone.lxdapp.local/api/login -2025-07-16 09:32:42 : http://10.110.90.24// -2025-07-16 09:33:50 : http://testone.lxdapp.local/api/login -2025-07-16 09:33:50 : http://10.110.90.24// -2025-07-16 09:33:58 : http://10.110.90.24// -2025-07-16 09:45:10 : http://10.110.90.24// -2025-07-16 09:45:12 : http://10.110.90.24// diff --git a/backend/app/src/Controllers/LoginController.php b/backend/app/src/Controllers/LoginController.php index 50fe2b6..b07683a 100644 --- a/backend/app/src/Controllers/LoginController.php +++ b/backend/app/src/Controllers/LoginController.php @@ -15,41 +15,47 @@ class LoginController */ public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface { - $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; - $params = (array)$request->getParsedBody(); + try { + $mainDomain = $_ENV['MAIN_DOMAIN'] ?? 'lxdapp.local'; - $captcha = new PCaptcha(); + $origin = $request->getHeaderLine('Origin'); + if (!empty($origin)) { + $domain = parse_url($origin, PHP_URL_HOST); + } else { + $domain = $request->getHeaderLine('Host'); + } - if (!$captcha->validate_captcha($params['panswer'])) { - return $this->json($response, ['status' => 'error', 'message' => 'Invalid CAPTCHA'], 200); - } - - $lxd = new LxdService(); - - $status = $lxd->getContainerState($name)['metadata']['status'] ?? 'Stopped'; - if ($status !== 'Running') { - $lxd->startContainer($name); - sleep(10); + $configPath = __DIR__ . '/../../config.json'; + $config = file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : []; + $name = $config[$domain] ?? null; + $params = (array)$request->getParsedBody(); + + $captcha = new PCaptcha(); + + if (!$captcha->validate_captcha($params['panswer'])) { + return $this->json($response, ['status' => 'error', 'message' => 'Invalid CAPTCHA'], 400); + } + + $lxd = new LxdService(); + + $status = $lxd->getContainerState($name)['metadata']['status'] ?? 'Stopped'; + if ($status !== 'Running') { + $lxd->startContainer($name); + } + + // Write log + LogWriterHelper::write($name, $request->getUri()); + + // Login success + return $this->json($response, ['status' => 'success', 'message' => 'Container started!']); + } catch (\Throwable $e) { + return $this->json($response, [ + 'status' => 'error', + 'message' => $e->getMessage(), + ], 500); } - - // Write log - LogWriterHelper::write($name, $request->getUri()); - - // Login success - return $this->json($response, ['status' => 'success', 'message' => 'Container started!']); } /** diff --git a/backend/app/src/Controllers/ProxyController.php b/backend/app/src/Controllers/ProxyController.php deleted file mode 100644 index f94153a..0000000 --- a/backend/app/src/Controllers/ProxyController.php +++ /dev/null @@ -1,130 +0,0 @@ -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(); - - // STEP 1: If container mapping not found - if (!$name) { - return $this->json($response, ['status' => 'error', 'message' => 'Container does not exist.'], 404); - } - - // STEP 2: Check if container exists in LXD - if (!$lxd->containerExists($name)) { - return $this->json($response, ['status' => 'error', 'message' => 'Container does not exist.'], 404); - } - - // STEP 4: Check container status - $containerInfo = $lxd->getContainerState($name); - $status = $containerInfo['metadata']['status'] ?? 'Stopped'; - - if ($status !== 'Running') { - // Container not running → redirect to login page - $scheme = $request->getUri()->getScheme(); // "http" or "https" - $redirectUrl = $request->getUri(); - - return $response - ->withHeader('Location', 'app/?auth=ok&redirect=' . urlencode($redirectUrl)) - ->withStatus(302); - } - - // STEP 5: Container is running → proxy request - $ip = $lxd->getContainerIP($name); - if (!$ip) { - return $this->json($response, ['status' => 'error', 'message' => 'Could not fetch container IP'], 500); - } - - return $this->proxyToContainer($request, $response, $ip, $name); - } catch (\Throwable $e) { - // Global fallback for any exception - return $this->json($response, [ - 'status' => 'error', - 'message' => 'Internal Server Error: ' . $e->getMessage() - ], 500); - } - } - - - /** - * Sends a JSON response. - */ - protected function json($response, array $data, int $status = 200) - { - $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); - } - - /** - * Proxies the request to the container. - */ - private function proxyToContainer(Request $request, Response $response, string $ip, string $name): Response - { - - $baseUrl = $ip ? "http://$ip/" : "http://127.0.0.1:3000"; - - $uri = $request->getUri(); - $path = $uri->getPath(); - - // Remove /api/v1 prefix from path - $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'] ?? 'Bj5pnZEX6DkcG6Nz6AjDUT1bvcGRVhRaXDuKDX9CjsEs2'; - Proxy::$ENABLE_AUTH = true; // Enable auth - Proxy::$HEADER_HTTP_PROXY_AUTH = 'HTTP_PROXY_AUTH'; // Ensure it matches the key - - $_SERVER['HTTP_PROXY_AUTH'] = $_ENV['AUTH_KEY'] ?? 'Bj5pnZEX6DkcG6Nz6AjDUT1bvcGRVhRaXDuKDX9CjsEs2'; - $_SERVER['HTTP_PROXY_TARGET_URL'] = $targetUrl; - // Do your custom logic before running proxy - $responseCode = Proxy::run(); - - // Write log - LogWriterHelper::write($name, $targetUrl); - - return $response; - } - -} \ No newline at end of file diff --git a/backend/app/src/Services/LxdService.php b/backend/app/src/Services/LxdService.php index cfa1e5b..69135df 100644 --- a/backend/app/src/Services/LxdService.php +++ b/backend/app/src/Services/LxdService.php @@ -2,7 +2,6 @@ namespace App\Services; use Exception; - class LxdService { private string $baseUrl; @@ -100,6 +99,7 @@ class LxdService * @throws Exception */ public function startContainer(string $name): array { + $startResponse = $this->request('PUT', "/1.0/instances/$name/state", [ "action" => "start", "timeout" => 30, @@ -120,10 +120,28 @@ class LxdService } while ($startStatus < 200); if ($startStatus >= 400) { - throw new \Exception("Failed to start container: " . json_encode($startOpResp)); + throw new \Exception("Failed to start container, Please try again."); } - return $startResponse; + 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."); } /** @@ -176,29 +194,11 @@ class LxdService return null; } - /** - * Waits for a specific port to become available. - * - * @param string $ip IP address - * @param int $port Port number - * @param int $timeout Timeout in seconds - * @return bool True if the port is available, false otherwise - */ - public function waitForPort(string $ip, int $port = 80, int $timeout = 10): bool + public function isServiceRunning(string $container, string $service): bool { - $startTime = time(); - - while ((time() - $startTime) < $timeout) { - $connection = @fsockopen($ip, $port, $errno, $errstr, 2); - - if ($connection) { - fclose($connection); - return true; // Port is open - } - - sleep(1); // Wait 1 second before retrying - } - - return false; // Timed out + $lxcPath = $_ENV['LXC_PATH'] ?: 'lxc'; + $cmd = "$lxcPath exec {$container} -- systemctl is-active {$service} 2>&1"; + $output = shell_exec($cmd); + return trim($output) === 'active'; } } \ No newline at end of file diff --git a/frontend/components/Captcha.vue b/frontend/components/Captcha.vue index 8e38a6b..975a65c 100644 --- a/frontend/components/Captcha.vue +++ b/frontend/components/Captcha.vue @@ -48,6 +48,10 @@ const emit = defineEmits(['update:captcha']); watch(captchaInput, (newVal) => { emit('update:captcha', newVal) }); + +defineExpose({ + refreshCaptcha +});