Refactor code

This commit is contained in:
2025-07-09 08:30:46 +02:00
parent 311cb4e4fa
commit 4aa3a766c6
8 changed files with 193 additions and 104 deletions

6
backend/app/.env Normal file
View File

@ -0,0 +1,6 @@
MAIN_DOMAIN=lxdapp.local
MAIN_COOKIE_DOMAIN=.lxdapp.local
LXD_API_URL=https://localhost:8443
LXD_CLIENT_CERT=/etc/ssl/lxdapp/client.crt
LXD_CLIENT_KEY=/etc/ssl/lxdapp/client.key
LXD_IMAGE_FINGERPRINT=2edfd84b1396

View File

@ -35,9 +35,6 @@ backend/
└── README.md └── README.md
---
--- ---
## ⚙️ Requirements ## ⚙️ Requirements
@ -46,9 +43,10 @@ backend/
- LXD installed and configured - LXD installed and configured
- PHP-FPM + NGINX - PHP-FPM + NGINX
- Composer - Composer
- `guzzlehttp/guzzle`
- `slim/slim` - `slim/slim`
- `slim/psr7` - `slim/psr7`
- `guzzlehttp/guzzle`
- `vlucas/phpdotenv`
--- ---
@ -57,41 +55,41 @@ backend/
1. **put the folder in /var/www/html** 1. **put the folder in /var/www/html**
2. **Install dependencies** 2. **Install dependencies**
```bash
composer install composer install
3. **Ensure PHP and NGINX are configured** 3. **Ensure PHP and NGINX are configured**
NGINX config example: NGINX config example:
```bash
server {
listen 80;
server_name *.lxdapp.local;
server { root /var/www/html/lxd-app/backend/public;
listen 80; index index.php;
server_name *.lxdapp.local;
root /var/www/html/lxd-app/backend/public; location / {
index index.php; try_files $uri /index.php?$query_string;
}
location / { location ~ \.php$ {
try_files $uri /index.php?$query_string; fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 300;
}
} }
location ~ \.php$ {
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_read_timeout 300;
}
}
4. **Map domain in /etc/hosts** 4. **Map domain in /etc/hosts**
```bash
127.0.0.1 customer1.lxdapp.local 127.0.0.1 mitul.lxdapp.local
5. **Make sure LXD is working** 5. **Make sure LXD is working**
```bash
lxc list lxc list
6. **🔐 CAPTCHA Protection** 6. **🔐 CAPTCHA Protection**
@ -106,7 +104,7 @@ server {
POST /api/v1/proxy POST /api/v1/proxy
**Request Body:** **Request Body:**
```bash
{ {
"source": "login", "source": "login",
"panswer": "abc123" "panswer": "abc123"
@ -114,19 +112,16 @@ server {
**Headers:** **Headers:**
Origin: http://customer1.lxdapp.local Origin: http://mitul.lxdapp.local
**Response:** **Response:**
```bash
{ {
"status": "success", "status": "success",
"ip": "10.210.189.24" "ip": "10.210.189.24"
} }
**🧠 Notes** **🧠 Notes**
Container names are auto-generated using the subdomain prefix. Container names are auto-generated using the subdomain prefix.
@ -144,6 +139,7 @@ server {
public/last-access-logs/container-*.txt public/last-access-logs/container-*.txt
NGINX error logs (for debugging): NGINX error logs (for debugging):
```bash
tail -f /var/log/nginx/error.log tail -f /var/log/nginx/error.log

View File

@ -4,7 +4,8 @@
"slim/psr7": "^1.7", "slim/psr7": "^1.7",
"php-di/php-di": "^7.0", "php-di/php-di": "^7.0",
"guzzlehttp/guzzle": "^7.9", "guzzlehttp/guzzle": "^7.9",
"nyholm/psr7": "^1.8" "nyholm/psr7": "^1.8",
"vlucas/phpdotenv": "^5.6"
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {

View File

@ -1,3 +1,2 @@
{ {
"customer1.lxdapp.local": "container-customer1"
} }

View File

@ -1,7 +1,18 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
$domain = '.lxdapp.local'; // Leading dot is important for subdomain support
use DI\ContainerBuilder;
use Slim\Factory\AppFactory;
use Dotenv\Dotenv;
require __DIR__ . '/../vendor/autoload.php';
// 🔹 Load .env config
$dotenv = Dotenv::createImmutable(__DIR__ . '/../');
$dotenv->load();
$domain = $_ENV['MAIN_COOKIE_DOMAIN'] ?? '.lxdapp.local';
session_set_cookie_params([ session_set_cookie_params([
'lifetime' => 0, 'lifetime' => 0,
'path' => '/', 'path' => '/',
@ -14,13 +25,6 @@ session_set_cookie_params([
if (session_status() === PHP_SESSION_NONE) { if (session_status() === PHP_SESSION_NONE) {
session_start(); session_start();
} }
use DI\ContainerBuilder;
use Slim\Factory\AppFactory;
require __DIR__ . '/../vendor/autoload.php';
error_reporting(E_ALL);
ini_set('display_errors', '1');
// Build Container using PHP-DI // Build Container using PHP-DI
$containerBuilder = new ContainerBuilder(); $containerBuilder = new ContainerBuilder();
@ -44,11 +48,11 @@ AppFactory::setContainer($container);
// Create App // Create App
$app = AppFactory::create(); $app = AppFactory::create();
// 🔹 CORS middleware
$app->add(function ($request, $handler) { $app->add(function ($request, $handler) {
$response = $handler->handle($request); $response = $handler->handle($request);
// $origin = $request->getHeaderLine('Origin') ?: '*';
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*'; $origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
return $response return $response
->withHeader('Access-Control-Allow-Origin', $origin) ->withHeader('Access-Control-Allow-Origin', $origin)
->withHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization') ->withHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization')
@ -56,7 +60,6 @@ $app->add(function ($request, $handler) {
->withHeader('Access-Control-Allow-Credentials', 'true'); ->withHeader('Access-Control-Allow-Credentials', 'true');
}); });
// Register middleware // Register middleware
(require __DIR__ . '/../src/Bootstrap/middleware.php')($app); (require __DIR__ . '/../src/Bootstrap/middleware.php')($app);

View File

@ -7,3 +7,4 @@
2025-07-08 15:12:49 : http://10.110.90.95:80 2025-07-08 15:12:49 : http://10.110.90.95:80
2025-07-08 15:23:59 : http://10.110.90.147:80 2025-07-08 15:23:59 : http://10.110.90.147:80
2025-07-08 15:24:26 : http://10.110.90.147:80 2025-07-08 15:24:26 : http://10.110.90.147:80
2025-07-09 06:15:42 : http://10.110.90.248:80

View File

@ -12,12 +12,15 @@ use App\lib\PCaptcha;
class ProxyController class ProxyController
{ {
/**
* Handles forwarding requests to the appropriate container.
*/
public function forward(Request $request, Response $response): Response public function forward(Request $request, Response $response): Response
{ {
$mainDomain = 'lxdapp.local'; $mainDomain = $_ENV['MAIN_DOMAIN'] ?? 'lxdapp.local';
$origin = $request->getHeaderLine('Origin'); $origin = $request->getHeaderLine('Origin');
$domain = parse_url($origin, PHP_URL_HOST); // e.g. customer1.lxdapp.local $domain = parse_url($origin, PHP_URL_HOST); // e.g. mitul.lxdapp.local
$params = (array)$request->getParsedBody(); $params = (array)$request->getParsedBody();
$configPath = __DIR__ . '/../../config.json'; $configPath = __DIR__ . '/../../config.json';
@ -95,6 +98,9 @@ class ProxyController
return $this->proxyToContainer($request, $response, $ip, $name); return $this->proxyToContainer($request, $response, $ip, $name);
} }
/**
* Maps a domain to a container name.
*/
private function mapDomainToContainer(string $domain): ?string private function mapDomainToContainer(string $domain): ?string
{ {
$configPath = __DIR__ . '/../../config.json'; $configPath = __DIR__ . '/../../config.json';
@ -108,7 +114,9 @@ class ProxyController
return $config[$domain] ?? null; return $config[$domain] ?? null;
} }
/**
* 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);
@ -121,6 +129,9 @@ class ProxyController
->withStatus($status); ->withStatus($status);
} }
/**
* Generates a sanitized container name based on a subdomain.
*/
public function generateContainerName(string $subdomain): string public function generateContainerName(string $subdomain): string
{ {
// Convert to lowercase, remove unsafe characters // Convert to lowercase, remove unsafe characters
@ -130,6 +141,9 @@ class ProxyController
return "container-{$sanitized}"; return "container-{$sanitized}";
} }
/**
* Proxies the request to the container.
*/
private function proxyToContainer(Request $request, Response $response, string $ip, string $name): Response private function proxyToContainer(Request $request, Response $response, string $ip, string $name): Response
{ {
$target = $ip ? "http://$ip:80" : "http://127.0.0.1:3000"; $target = $ip ? "http://$ip:80" : "http://127.0.0.1:3000";
@ -153,6 +167,9 @@ class ProxyController
} }
/**
* Logs the last access to a container.
*/
protected function writeLastAccessLog(string $name, string $uri): void { protected function writeLastAccessLog(string $name, string $uri): void {
// Dynamically resolve the log directory relative to the current file // Dynamically resolve the log directory relative to the current file
$logDir = realpath(__DIR__ . '/../../public/last-access-logs'); $logDir = realpath(__DIR__ . '/../../public/last-access-logs');

View File

@ -6,29 +6,40 @@ use Exception;
class LxdService class LxdService
{ {
private string $baseUrl; private string $baseUrl;
private string $socketPath; private string $imageFingerprint;
public function __construct() { public function __construct() {
// $this->baseUrl = "http://unix.socket"; $this->baseUrl = $_ENV['LXD_API_URL'] ?? 'https://localhost:8443';
$this->baseUrl = "https://localhost:8443"; $this->imageFingerprint = $_ENV['LXD_IMAGE_FINGERPRINT'] ?? '2edfd84b1396';
$this->socketPath = "/var/snap/lxd/common/lxd/unix.socket";
} }
/**
* Sends an HTTP request to the LXD API.
*
* @param string $method HTTP method (GET, POST, PUT, etc.)
* @param string $endpoint API endpoint
* @param array $body Request body (optional)
* @return array Response from the API
* @throws Exception
*/
private function request(string $method, string $endpoint, array $body = []): array { 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");
}
$ch = curl_init("{$this->baseUrl}{$endpoint}"); $ch = curl_init("{$this->baseUrl}{$endpoint}");
// Paths to your client cert and key
$clientCert = '/etc/ssl/lxdapp/client.crt';
$clientKey = '/etc/ssl/lxdapp/client.key';
//curl_setopt($ch, CURLOPT_UNIX_SOCKET_PATH, $this->socketPath); // Paths to client certificate and key for TLS authentication
$clientCert = $_ENV['LXD_CLIENT_CERT'] ?? '/etc/ssl/lxdapp/client.crt';
$clientKey = $_ENV['LXD_CLIENT_KEY'] ?? '/etc/ssl/lxdapp/client.key';
// Specify your client certificate and key for TLS authentication
curl_setopt($ch, CURLOPT_SSLCERT, $clientCert); curl_setopt($ch, CURLOPT_SSLCERT, $clientCert);
curl_setopt($ch, CURLOPT_SSLKEY, $clientKey); curl_setopt($ch, CURLOPT_SSLKEY, $clientKey);
// For testing only: disable peer verification (use carefully in production!)
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// curl_setopt($ch, CURLOPT_URL, "{$this->baseUrl}{$endpoint}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
@ -59,15 +70,12 @@ class LxdService
return $json; return $json;
} }
public function getStatus(string $name): ?array /**
{ * Retrieves a container.
try { *
return $this->request('GET', "/1.0/instances/$name/state"); * @param string $name Container name
} catch (\Throwable $e) { * @return array|null Container or null if an error occurs
return null; */
}
}
public function getContainer(string $name) { public function getContainer(string $name) {
try { try {
return $this->request('GET', "/1.0/instances/$name"); return $this->request('GET', "/1.0/instances/$name");
@ -76,6 +84,12 @@ class LxdService
} }
} }
/**
* Retrieves the status of a container.
*
* @param string $name Container name
* @return array|null Container status or null if an error occurs
*/
public function getContainerState(string $name) { public function getContainerState(string $name) {
try { try {
return $this->request('GET', "/1.0/instances/$name/state"); return $this->request('GET', "/1.0/instances/$name/state");
@ -84,10 +98,23 @@ class LxdService
} }
} }
/**
* Checks if a container exists.
*
* @param string $name Container name
* @return bool True if the container exists, false otherwise
*/
public function containerExists(string $name): bool { public function containerExists(string $name): bool {
return $this->getContainerState($name) !== null; return $this->getContainerState($name) !== null;
} }
/**
* Starts a container.
*
* @param string $name Container name
* @return array Response from the API
* @throws Exception
*/
public function startContainer(string $name): array { public function startContainer(string $name): array {
$startResponse = $this->request('PUT', "/1.0/instances/$name/state", [ $startResponse = $this->request('PUT', "/1.0/instances/$name/state", [
"action" => "start", "action" => "start",
@ -117,6 +144,12 @@ class LxdService
return $containerResponse['metadata'] ?? []; return $containerResponse['metadata'] ?? [];
} }
/**
* Stops a container.
*
* @param string $name Container name
* @return array Response from the API
*/
public function stopContainer(string $name): array { public function stopContainer(string $name): array {
$response = $this->request('PUT', "/1.0/instances/$name/state", [ $response = $this->request('PUT', "/1.0/instances/$name/state", [
"action" => "stop", "action" => "stop",
@ -127,18 +160,31 @@ class LxdService
return $response; return $response;
} }
public function createContainer(string $name, string $fingerprint = "2edfd84b1396"): array { /**
* Creates a new container.
*
* @param string $name Container name
* @param string $fingerprint Image fingerprint
* @return array Response from the API
*/
public function createContainer(string $name): array {
$response = $this->request('POST', "/1.0/instances", [ $response = $this->request('POST', "/1.0/instances", [
"name" => $name, "name" => $name,
"source" => [ "source" => [
"type" => "image", "type" => "image",
"fingerprint" => $fingerprint "fingerprint" => $this->imageFingerprint
] ]
]); ]);
sleep(5); sleep(5);
return $response; return $response;
} }
/**
* Installs packages inside a container.
*
* @param string $name Container name
* @throws Exception
*/
public function installPackages(string $name) { public function installPackages(string $name) {
$log = ''; $log = '';
@ -152,49 +198,44 @@ class LxdService
file_put_contents('/tmp/lxd_install.log', $log); file_put_contents('/tmp/lxd_install.log', $log);
// Wait for nginx service to be active // Wait for services to start
$nginxActive = false; $this->waitForService($name, 'nginx');
for ($i = 0; $i < 10; $i++) { $this->waitForService($name, 'mysql');
$status = shell_exec("/snap/bin/lxc exec $name -- systemctl is-active nginx"); }
if (trim($status) === 'active') {
$nginxActive = true;
break;
}
sleep(2); // wait 2 seconds before retry
}
// Wait for mysql service to be active /**
$mysqlActive = false; * Waits for a service to become active inside a container.
*
* @param string $name Container name
* @param string $service Service name
* @throws Exception
*/
private function waitForService(string $name, string $service)
{
for ($i = 0; $i < 10; $i++) { for ($i = 0; $i < 10; $i++) {
$status = shell_exec("/snap/bin/lxc exec $name -- systemctl is-active mysql"); $status = shell_exec("/snap/bin/lxc exec $name -- systemctl is-active $service");
if (trim($status) === 'active') { if (trim($status) === 'active') {
$mysqlActive = true; return;
break;
} }
sleep(2); sleep(2);
} }
if (!$nginxActive || !$mysqlActive) { throw new Exception("Failed to start $service inside container $name");
file_put_contents('/tmp/lxd_install.log', "Service(s) failed to start: nginx active=$nginxActive, mysql active=$mysqlActive\n", FILE_APPEND);
throw new \Exception("Failed to start nginx or mysql inside container $name");
}
}
public function getContainerIP($name)
{
$container = $this->getStatus($name);
return $this->getIPv4FromMetadata($container['metadata']);
} }
/**
* Creates a new container and wait for start.
*
* @param string $name Container name
* @param string $fingerprint Image fingerprint
* @return array Response from the API
*/
public function createContainerAndWait(string $name, array $config = []): array { public function createContainerAndWait(string $name, array $config = []): array {
// 1. Prepare the creation request body
$body = [ $body = [
"name" => $name, "name" => $name,
"source" => array_merge([ "source" => array_merge([
"type" => "image", "type" => "image",
"fingerprint" => "2edfd84b1396" "fingerprint" => $this->imageFingerprint
]), ]),
]; ];
@ -225,7 +266,27 @@ class LxdService
} }
public function waitForPort(string $ip, int $port = 80, int $timeout = 30): bool /**
* Retrieves the IPv4 address of a container.
*
* @param string $name Container name
* @return string|null IPv4 address or null if not found
*/
public function getContainerIP($name)
{
$container = $this->getContainerState($name);
return $this->getIPv4FromMetadata($container['metadata']);
}
/**
* 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
{ {
$startTime = time(); $startTime = time();
@ -244,7 +305,12 @@ class LxdService
} }
// Function to get IPv4 address from metadata /**
* Extracts the IPv4 address from container metadata.
*
* @param array $metadata Container metadata
* @return string|null IPv4 address or null if not found
*/
public function getIPv4FromMetadata(array $metadata): ?string public function getIPv4FromMetadata(array $metadata): ?string
{ {
if ( if (