fix: update service api call

This commit is contained in:
2025-09-01 15:06:16 +02:00
parent e96bcb7a94
commit 7db9ce4d17
3 changed files with 151 additions and 56 deletions

View File

@ -14,63 +14,76 @@ class LoginController
* Login with captcha * Login with captcha
*/ */
public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface public function index(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{ {
try { try {
$mainDomain = $_ENV['MAIN_DOMAIN'] ?? 'lxdapp.local';
$origin = $request->getHeaderLine('Origin'); $origin = $request->getHeaderLine('Origin');
if (!empty($origin)) { $domain = !empty($origin) ? parse_url($origin, PHP_URL_HOST) : $request->getHeaderLine('Host');
$domain = parse_url($origin, PHP_URL_HOST);
} else {
$domain = $request->getHeaderLine('Host');
}
$configPath = __DIR__ . '/../../config.json'; $configPath = __DIR__ . '/../../config.json';
$config = file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : []; $config = file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : [];
$name = $config[$domain] ?? null; $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); return $this->json($response, ['status' => 'error', 'message' => 'Invalid CAPTCHA'], 400);
} }
$lxd = new LxdService(); $lxd = new LxdService();
$status = $lxd->getContainerState($name)['metadata']['status'] ?? 'Stopped'; $status = $lxd->getContainerState($name)['metadata']['status'] ?? 'Stopped';
if ($status !== 'Running') { if ($status !== 'Running') {
$lxd->startContainer($name); $lxd->startContainer($name);
} }
// Write log // Log path only (avoid leaking query in logs)
LogWriterHelper::write($name, $request->getUri()); $uri = $request->getUri();
LogWriterHelper::write($name, $uri->getPath());
$redirect = '/waiting?name='.$name .'&redirect='.urlencode($params['redirect']); // ---- NEW: create one-time handoff and store creds server-side ----
// Login success $handoffId = bin2hex(random_bytes(16));
return $this->json($response, ['status' => 'success', 'message' => 'Container started!', 'redirect' => $redirect]); $_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) { } catch (\Throwable $e) {
return $this->json($response, [ return $this->json($response, [
'status' => 'error', 'status' => 'error',
'message' => $e->getMessage(), 'message' => $e->getMessage(),
], 500); ], 500);
} }
} }
public function status(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface public function status(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{ {
$queryParams = $request->getQueryParams(); $queryParams = $request->getQueryParams();
$name = $queryParams['name'] ?? ''; $name = $queryParams['name'] ?? '';
if (empty($name)) { if (empty($name)) {
return $this->json($response, [ return $this->json($response, [
'status' => 'error', 'status' => 'error',
'message' => 'Missing container name.', 'message' => 'Missing container name.',
], 400); ], 400);
} }
@ -80,7 +93,7 @@ class LoginController
$state = $lxd->getContainerState($name); $state = $lxd->getContainerState($name);
if (!$state) { if (!$state) {
return $this->json($response, [ return $this->json($response, [
'status' => 'not_found', 'status' => 'not_found',
'message' => 'Container not found', 'message' => 'Container not found',
], 404); ], 404);
} }
@ -88,46 +101,44 @@ class LoginController
$status = $state['metadata']['status'] ?? 'Stopped'; $status = $state['metadata']['status'] ?? 'Stopped';
if ($status !== 'Running') { if ($status !== 'Running') {
return $this->json($response, [ return $this->json($response, [
'status' => 'starting', 'status' => 'starting',
'message' => 'Container is not yet running', 'message' => 'Container is not yet running',
]); ]);
} }
$ip = $lxd->getContainerIP($name); $ip = $lxd->getContainerIP($name);
$nginx = $lxd->isServiceRunning($name, 'nginx'); $nginx = $lxd->getContainerServiceStatus($name, 'nginx');
$mysql = $lxd->isServiceRunning($name, 'mysql'); $mysql = $lxd->getContainerServiceStatus($name, 'mysql');
if ($ip && $nginx === 'active' && $mysql === 'active') { if ($ip && $nginx === 'active' && $mysql === 'active') {
// ---- CHANGED: do NOT return fields/creds here ----
return $this->json($response, [ return $this->json($response, [
'status' => 'ready', 'status' => 'ready',
'ip' => $ip, 'ip' => $ip,
'message' => 'Container is ready', 'message' => 'Container is ready',
]); ]);
} }
if($nginx === 'failed'){ if ($nginx === 'failed') {
return $this->json($response, [ return $this->json($response, [
'status' => 'failed', 'status' => 'failed',
'message' => 'Failed to start web server service in container', 'message' => 'Failed to start web server service in container',
], 400); ], 400);
} }
if($mysql === 'failed'){ if ($mysql === 'failed') {
return $this->json($response, [ return $this->json($response, [
'status' => 'failed', 'status' => 'failed',
'message' => 'Failed to start mysql service in container', 'message' => 'Failed to start mysql service in container',
], 400); ], 400);
} }
return $this->json($response, [ return $this->json($response, [
'status' => 'running', 'status' => 'running',
'message' => 'Container is running, waiting for services...', 'message' => 'Container is running, waiting for services...',
]); ]);
} }
/**
* Sends a JSON response.
*/
protected function json($response, array $data, int $status = 200) protected function json($response, array $data, int $status = 200)
{ {
$payload = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); $payload = json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
@ -140,4 +151,20 @@ class LoginController
->withStatus($status); ->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';
}
} }

View File

@ -59,6 +59,7 @@ class LxdService
} }
$json = json_decode($response, true); $json = json_decode($response, true);
if ($httpCode >= 400) { if ($httpCode >= 400) {
throw new Exception("LXD API Error: " . ($json['error'] ?? 'Unknown')); throw new Exception("LXD API Error: " . ($json['error'] ?? 'Unknown'));
} }
@ -66,6 +67,43 @@ class LxdService
return $json; 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. * Retrieves the status of a container.
* *
@ -162,11 +200,48 @@ class LxdService
return null; 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'; // Step 1: Prepare exec command
$cmd = "$lxcPath exec {$container} -- systemctl is-active {$service} 2>&1"; $execPayload = [
$output = shell_exec($cmd); "command" => ["systemctl", "is-active", $service],
return trim($output); "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.
} }
} }

View File

@ -41,17 +41,10 @@ if ($state !== 'Running') {
redirect($redirectBase); redirect($redirectBase);
} }
// === Check container status ===
$state = $lxd->getContainerState($container)['metadata']['status'] ?? 'Stopped';
if ($state !== 'Running') {
redirect($redirectBase);
}
// === Get container IP === // === Get container IP ===
$ip = $lxd->getContainerIP($container); $ip = $lxd->getContainerIP($container);
$nginx = $lxd->isServiceRunning($container, 'nginx'); $nginx = $lxd->getContainerServiceStatus($container, 'nginx');
$mysql = $lxd->isServiceRunning($container, 'mysql'); $mysql = $lxd->getContainerServiceStatus($container, 'mysql');
if (!$ip || $nginx !== 'active' || $mysql !== 'active') { if (!$ip || $nginx !== 'active' || $mysql !== 'active') {
redirect($waitingPage); redirect($waitingPage);