fix: update service api call
This commit is contained in:
@ -15,49 +15,62 @@ class LoginController
|
|||||||
*/
|
*/
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -70,7 +83,7 @@ class LoginController
|
|||||||
|
|
||||||
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';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -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.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
11
index.php
11
index.php
@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user