From 7db9ce4d179e579b6e3050a1fefe2ba93ecc04c8 Mon Sep 17 00:00:00 2001 From: Urvish Patel Date: Mon, 1 Sep 2025 15:06:16 +0200 Subject: [PATCH] fix: update service api call --- api/src/Controllers/LoginController.php | 111 +++++++++++++++--------- api/src/Services/LxdService.php | 85 ++++++++++++++++-- index.php | 11 +-- 3 files changed, 151 insertions(+), 56 deletions(-) diff --git a/api/src/Controllers/LoginController.php b/api/src/Controllers/LoginController.php index ab84061..075b82c 100644 --- a/api/src/Controllers/LoginController.php +++ b/api/src/Controllers/LoginController.php @@ -14,63 +14,76 @@ class LoginController * Login with captcha */ public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface - { - + { 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'); - } + $domain = !empty($origin) ? parse_url($origin, PHP_URL_HOST) : $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(); - if(!$name){ - return $this->json($response, [ - 'status' => 'not_found', - 'message' => 'Container not found', - ], 404); - } - $captcha = new PCaptcha(); - if (!$captcha->validate_captcha($params['panswer'])) { + $params = (array)$request->getParsedBody(); + + if (!$name) { + return $this->json($response, [ + 'status' => 'not_found', + 'message' => 'Container not found', + ], 404); + } + + $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()); + // Log path only (avoid leaking query in logs) + $uri = $request->getUri(); + LogWriterHelper::write($name, $uri->getPath()); - $redirect = '/waiting?name='.$name .'&redirect='.urlencode($params['redirect']); - // Login success - return $this->json($response, ['status' => 'success', 'message' => 'Container started!', 'redirect' => $redirect]); + // ---- NEW: create one-time handoff and store creds server-side ---- + $handoffId = bin2hex(random_bytes(16)); + $_SESSION["handoff:$name:$handoffId"] = [ + 'username' => (string)($params['username'] ?? ''), + 'password' => (string)($params['password'] ?? ''), + 'created_at' => time(), + ]; + + // sanitize the container path (not a full URL!) + $path = $this->sanitizeRedirectPath($params['redirect'] ?? '/login'); + + // Client goes to waiting page; when ready it will be sent to the bridge + $redirect = '/waiting?name=' . rawurlencode($name) + . '&handoff=' . rawurlencode($handoffId) + . '&path=' . rawurlencode($path); + + return $this->json($response, [ + 'status' => 'success', + 'message' => 'Container started!', + 'redirect' => $redirect + ]); } catch (\Throwable $e) { return $this->json($response, [ - 'status' => 'error', + 'status' => 'error', 'message' => $e->getMessage(), ], 500); } } public function status(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface - { + { $queryParams = $request->getQueryParams(); $name = $queryParams['name'] ?? ''; if (empty($name)) { return $this->json($response, [ - 'status' => 'error', + 'status' => 'error', 'message' => 'Missing container name.', ], 400); } @@ -80,7 +93,7 @@ class LoginController $state = $lxd->getContainerState($name); if (!$state) { return $this->json($response, [ - 'status' => 'not_found', + 'status' => 'not_found', 'message' => 'Container not found', ], 404); } @@ -88,46 +101,44 @@ class LoginController $status = $state['metadata']['status'] ?? 'Stopped'; if ($status !== 'Running') { return $this->json($response, [ - 'status' => 'starting', + 'status' => 'starting', 'message' => 'Container is not yet running', ]); } $ip = $lxd->getContainerIP($name); - $nginx = $lxd->isServiceRunning($name, 'nginx'); - $mysql = $lxd->isServiceRunning($name, 'mysql'); + $nginx = $lxd->getContainerServiceStatus($name, 'nginx'); + $mysql = $lxd->getContainerServiceStatus($name, 'mysql'); + if ($ip && $nginx === 'active' && $mysql === 'active') { + // ---- CHANGED: do NOT return fields/creds here ---- return $this->json($response, [ - 'status' => 'ready', - 'ip' => $ip, + 'status' => 'ready', + 'ip' => $ip, 'message' => 'Container is ready', ]); } - if($nginx === 'failed'){ + if ($nginx === 'failed') { return $this->json($response, [ - 'status' => 'failed', + 'status' => 'failed', 'message' => 'Failed to start web server service in container', ], 400); } - if($mysql === 'failed'){ + if ($mysql === 'failed') { return $this->json($response, [ - 'status' => 'failed', + 'status' => 'failed', 'message' => 'Failed to start mysql service in container', ], 400); } return $this->json($response, [ - 'status' => 'running', + 'status' => 'running', 'message' => 'Container is running, waiting for services...', ]); } - - /** - * Sends a JSON response. - */ protected function json($response, array $data, int $status = 200) { $payload = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); @@ -140,4 +151,20 @@ class LoginController ->withStatus($status); } + /** + * Allow only known relative paths (never full URLs) + */ + private function sanitizeRedirectPath(string $raw): string + { + // deny absolute URLs + if (preg_match('#^https?://#i', $raw)) return '/login'; + if (!str_starts_with($raw, '/')) return '/login'; + + $path = parse_url($raw, PHP_URL_PATH) ?? '/login'; + + // allowlist of container paths you expect + $allow = ['/', '/login', '/signin']; + return in_array($path, $allow, true) ? $path : '/login'; + } } + \ No newline at end of file diff --git a/api/src/Services/LxdService.php b/api/src/Services/LxdService.php index 54699ac..6fdef64 100644 --- a/api/src/Services/LxdService.php +++ b/api/src/Services/LxdService.php @@ -59,6 +59,7 @@ class LxdService } $json = json_decode($response, true); + if ($httpCode >= 400) { throw new Exception("LXD API Error: " . ($json['error'] ?? 'Unknown')); } @@ -66,6 +67,43 @@ class LxdService return $json; } + /** + * Sends a GET request and returns raw (non-JSON) response. + * + * @param string $endpoint API endpoint + * @return string Raw response body + * @throws Exception + */ + private function requestRaw(string $endpoint): string { + $url = "{$this->baseUrl}{$endpoint}"; + $ch = curl_init($url); + + $clientCert = $_ENV['LXD_CLIENT_CERT'] ?? '/etc/ssl/lxdapp/client.crt'; + $clientKey = $_ENV['LXD_CLIENT_KEY'] ?? '/etc/ssl/lxdapp/client.key'; + + curl_setopt($ch, CURLOPT_SSLCERT, $clientCert); + curl_setopt($ch, CURLOPT_SSLKEY, $clientKey); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'GET'); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($response === false || $response === null) { + throw new \Exception("Raw LXD API call failed: $curlError"); + } + + if ($httpCode >= 400) { + throw new \Exception("LXD raw API returned HTTP $httpCode"); + } + + return $response; + } + + /** * Retrieves the status of a container. * @@ -162,11 +200,48 @@ class LxdService return null; } - public function isServiceRunning(string $container, string $service): string + // 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); + // } + + public function getContainerServiceStatus(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); + // Step 1: Prepare exec command + $execPayload = [ + "command" => ["systemctl", "is-active", $service], + "wait-for-websocket" => false, + "record-output" => true, + "interactive" => false + ]; + + // Step 2: Start exec operation + $execResponse = $this->request('POST', "/1.0/instances/{$container}/exec", $execPayload); + $operationUrl = $execResponse['operation'] ?? null; + + if (!$operationUrl) { + throw new \Exception("Failed to create exec operation"); + } + + // Step 3: Wait for operation to complete + $waitResponse = $this->request('GET', "{$operationUrl}/wait?timeout=10"); + $metadata = $waitResponse['metadata']['metadata'] ?? []; + + $outputPaths = $metadata['output'] ?? []; + $stdoutPath = $outputPaths['1'] ?? null; + + if (!$stdoutPath) { + throw new \Exception("No stdout path returned by LXD"); + } + + // Step 4: Fetch raw stdout output + $rawOutput = $this->requestRaw($stdoutPath); + + return trim($rawOutput); // Expected: 'active', 'inactive', etc. } + + } \ No newline at end of file diff --git a/index.php b/index.php index 07c21b3..47733c3 100644 --- a/index.php +++ b/index.php @@ -41,17 +41,10 @@ if ($state !== 'Running') { redirect($redirectBase); } -// === Check container status === -$state = $lxd->getContainerState($container)['metadata']['status'] ?? 'Stopped'; - -if ($state !== 'Running') { - redirect($redirectBase); -} - // === Get container IP === $ip = $lxd->getContainerIP($container); -$nginx = $lxd->isServiceRunning($container, 'nginx'); -$mysql = $lxd->isServiceRunning($container, 'mysql'); +$nginx = $lxd->getContainerServiceStatus($container, 'nginx'); +$mysql = $lxd->getContainerServiceStatus($container, 'mysql'); if (!$ip || $nginx !== 'active' || $mysql !== 'active') { redirect($waitingPage);