initialize Project

This commit is contained in:
2025-07-08 20:21:50 +02:00
commit 311cb4e4fa
40 changed files with 1944 additions and 0 deletions

BIN
backend/.DS_Store vendored Normal file

Binary file not shown.

156
backend/app/README.md Normal file
View File

@ -0,0 +1,156 @@
# LXD Proxy API (Slim Framework)
This is a PHP-based API using the **Slim 4 Framework** that dynamically manages and proxies traffic to LXD containers based on subdomain mappings.
---
## 🧩 Features
- Automatically creates and starts LXD containers per subdomain
- CAPTCHA validation for provisioning
- Proxies requests to containerized environments
- Persists domain-to-container mapping
- Waits for container service to be ready before forwarding
- Logs last access per container
---
## 📁 Project Structure
backend/
├── app/
│ ├── src/
│ │ ├── Controllers/
│ │ │ └── ProxyController.php
│ │ ├── Services/
│ │ │ └── LxdService.php
│ │ └── Utils/
│ │ └── SubdomainHelper.php
│ ├── vendor/ # Composer dependencies
│ ├── public/
│ │ └── last-access-logs/
│ ├── config.json # Domain-to-container mappings
│ └── index.php # Slim entry point
├── composer.json
└── README.md
---
---
## ⚙️ Requirements
- PHP 8.1+
- LXD installed and configured
- PHP-FPM + NGINX
- Composer
- `guzzlehttp/guzzle`
- `slim/slim`
- `slim/psr7`
---
## 🚀 Setup Instructions
1. **put the folder in /var/www/html**
2. **Install dependencies**
composer install
3. **Ensure PHP and NGINX are configured**
NGINX config example:
server {
listen 80;
server_name *.lxdapp.local;
root /var/www/html/lxd-app/backend/public;
index index.php;
location / {
try_files $uri /index.php?$query_string;
}
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**
127.0.0.1 customer1.lxdapp.local
5. **Make sure LXD is working**
lxc list
6. **🔐 CAPTCHA Protection**
The /api/v1/proxy POST endpoint expects a field panswer with the correct CAPTCHA.
You can configure PCaptcha class for custom logic.
7. **🧪 API Usage**
POST /api/v1/proxy
**Request Body:**
{
"source": "login",
"panswer": "abc123"
}
**Headers:**
Origin: http://customer1.lxdapp.local
**Response:**
{
"status": "success",
"ip": "10.210.189.24"
}
**🧠 Notes**
Container names are auto-generated using the subdomain prefix.
All containers are mapped in config.json.
If a container does not yet exist, it's created, started, and software is installed.
The system waits for the container to expose port 80 before proxying.
**🪵 Logs**
Last access logs for containers are saved in:
public/last-access-logs/container-*.txt
NGINX error logs (for debugging):
tail -f /var/log/nginx/error.log
**👨‍💻 Development**
Codebase follows PSR-4 autoloading (App\ namespace).
To add new functionality, work within app/src/Controllers or Services.

14
backend/app/composer.json Normal file
View File

@ -0,0 +1,14 @@
{
"require": {
"slim/slim": "4.*",
"slim/psr7": "^1.7",
"php-di/php-di": "^7.0",
"guzzlehttp/guzzle": "^7.9",
"nyholm/psr7": "^1.8"
},
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
}

3
backend/app/config.json Normal file
View File

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

View File

@ -0,0 +1,3 @@
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^ index.php [QSA,L]

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
$domain = '.lxdapp.local'; // Leading dot is important for subdomain support
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'domain' => $domain,
'secure' => false, // set true if using HTTPS
'httponly' => true,
'samesite' => 'Lax',
]);
if (session_status() === PHP_SESSION_NONE) {
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
$containerBuilder = new ContainerBuilder();
$containerBuilder->useAutowiring(true); // Enable autowiring globally
// Add settings
$settings = require __DIR__ . '/../src/Settings/settings.php';
$settings($containerBuilder);
// Add dependencies
$dependencies = require __DIR__ . '/../src/Dependencies/dependencies.php';
$dependencies($containerBuilder);
// Build the container
$container = $containerBuilder->build();
// Set container to AppFactory
AppFactory::setContainer($container);
// Create App
$app = AppFactory::create();
$app->add(function ($request, $handler) {
$response = $handler->handle($request);
// $origin = $request->getHeaderLine('Origin') ?: '*';
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
return $response
->withHeader('Access-Control-Allow-Origin', $origin)
->withHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS')
->withHeader('Access-Control-Allow-Credentials', 'true');
});
// Register middleware
(require __DIR__ . '/../src/Bootstrap/middleware.php')($app);
// Register routes
(require __DIR__ . '/../src/Bootstrap/routes.php')($app);
// Run app
$app->run();

View File

@ -0,0 +1,9 @@
2025-07-08 13:07:05 : http://10.110.90.30:80
2025-07-08 13:29:10 : http://10.110.90.30:80
2025-07-08 14:43:45 : http://10.110.90.144:80
2025-07-08 14:50:28 : http://10.110.90.89:80
2025-07-08 14:53:41 : http://10.110.90.89:80
2025-07-08 15:07:07 : 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:24:26 : http://10.110.90.147:80

View File

@ -0,0 +1,40 @@
<?php
use Slim\App;
use Slim\Middleware\ErrorMiddleware;
use Slim\Middleware\BodyParsingMiddleware;
return function (App $app) {
// Add body parsing middleware (for JSON, form, etc.)
$app->addBodyParsingMiddleware();
// Add error middleware
$container = $app->getContainer();
$settings = $container->get('settings');
$errorMiddleware = new ErrorMiddleware(
$app->getCallableResolver(),
$app->getResponseFactory(),
$settings['displayErrorDetails'],
true,
true
);
// Optional custom 404 handler
$errorMiddleware->setErrorHandler(
Slim\Exception\HttpNotFoundException::class,
function (
Psr\Http\Message\ServerRequestInterface $request,
Throwable $exception,
bool $displayErrorDetails,
bool $logErrors,
bool $logErrorDetails
) use ($app) {
$response = $app->getResponseFactory()->createResponse();
$response->getBody()->write('404 Not Found — Custom Page');
return $response->withStatus(404);
}
);
$app->add($errorMiddleware);
};

View File

@ -0,0 +1,11 @@
<?php
use Slim\App;
return function (App $app) {
// Web routes
(require __DIR__ . '/../Routes/web.php')($app);
// API routes
(require __DIR__ . '/../Routes/v1/proxy.php')($app);
};

View File

@ -0,0 +1,34 @@
<?php
namespace App\Controllers;
use App\lib\PCaptcha;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
class CaptchaController
{
/**
* Generates and outputs a captcha image.
*/
public function get(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
{
if (session_status() === PHP_SESSION_NONE) {
session_start();
}
$captcha = new PCaptcha();
$captcha->width = 200;
$captcha->hight = 50;
$captcha->leng = 6;
try {
$imageData = $captcha->get_captcha();
} catch (\Throwable $e) {
$response->getBody()->write("Captcha error: " . $e->getMessage());
return $response->withStatus(500)->withHeader('Content-Type', 'text/plain');
}
$response->getBody()->write($imageData);
return $response->withHeader('Content-Type', 'image/png');
}
}

View File

@ -0,0 +1,173 @@
<?php
namespace App\Controllers;
use App\Managers\LXDProxyManager;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use GuzzleHttp\Client;
use App\Utils\SubdomainHelper;
use App\Services\LxdService;
use App\lib\PCaptcha;
class ProxyController
{
public function forward(Request $request, Response $response): Response
{
$mainDomain = 'lxdapp.local';
$origin = $request->getHeaderLine('Origin');
$domain = parse_url($origin, PHP_URL_HOST); // e.g. customer1.lxdapp.local
$params = (array)$request->getParsedBody();
$configPath = __DIR__ . '/../../config.json';
$config = file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : [];
$name = $config[$domain] ?? null;
$lxd = new LxdService();
// print_r($params);
// CASE 1: From Login Page Create if not mapped
if (isset($params['source']) && $params['source'] === 'login') {
$captcha = new PCaptcha();
if (!$captcha->validate_captcha($params['panswer'])) {
return $this->json($response, ['status' => 'error', 'error' => 'invalid_captcha']);
}
if (!$name) {
$subdomain = SubdomainHelper::getSubdomain($domain, $mainDomain);
$name = $this->generateContainerName($subdomain); // or just use subdomain
$config[$domain] = $name;
file_put_contents($configPath, json_encode($config, JSON_PRETTY_PRINT));
}
if (!$lxd->containerExists($name)) {
$lxd->createContainerAndWait($name);
sleep(5);
//$lxd->startContainer($name);
$lxd->installPackages($name);
sleep(5);
}
$ip = $lxd->getContainerIP($name);
if (!$ip) {
return $this->json($response, [
'status' => 'error',
'message' => "Failed to get container IP for '$name'"
], 500);
}
// if (!$lxd->waitForPort($ip, 80, 30)) {
// return $this->json($response, ['status' => 'error', 'message' => 'Container not ready'], 500);
// }
return $this->json($response, ['status' => 'success', 'ip' => $ip]);
}
// CASE 2: Not from login and no mapping
if (!$name) {
return $this->json($response, ['status' => 'not-found']);
}
// CASE 3: Check if container exists in LXD
$containerInfo = $lxd->getContainerState($name);
if (!$containerInfo) {
return $this->json($response, ['status' => 'not-found']);
}
if ($containerInfo['metadata']['status'] !== 'Running') {
$lxd->startContainer($name);
sleep(5);
}
$ip = $lxd->getContainerIP($name);
// if (!$ip) {
// $ip = $lxd->getContainerIP($name);
// }
if (!$lxd->waitForPort($ip, 80, 30)) {
return $this->json($response, ['status' => 'error', 'message' => 'Service not available'], 500);
}
return $this->proxyToContainer($request, $response, $ip, $name);
}
private function mapDomainToContainer(string $domain): ?string
{
$configPath = __DIR__ . '/../../config.json';
if (!file_exists($configPath)) {
return null;
}
$config = json_decode(file_get_contents($configPath), true);
return $config[$domain] ?? null;
}
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);
}
public function generateContainerName(string $subdomain): string
{
// Convert to lowercase, remove unsafe characters
$sanitized = preg_replace('/[^a-z0-9\-]/', '-', strtolower($subdomain));
// Optionally, ensure it's prefixed/suffixed for uniqueness
return "container-{$sanitized}";
}
private function proxyToContainer(Request $request, Response $response, string $ip, string $name): Response
{
$target = $ip ? "http://$ip:80" : "http://127.0.0.1:3000";
$client = new Client([
'http_errors' => false,
'timeout' => 5,
]);
// Just make a GET request to the base URL
$forwarded = $client->request('GET', $target);
$this->writeLastAccessLog($name, $target);
// Return the body and status
$response->getBody()->write((string) $forwarded->getBody());
return $response
->withHeader('Content-Type', 'text/html') // or json if API
->withHeader('Access-Control-Allow-Origin', '*')
->withStatus($forwarded->getStatusCode());
}
protected function writeLastAccessLog(string $name, string $uri): void {
// Dynamically resolve the log directory relative to the current file
$logDir = realpath(__DIR__ . '/../../public/last-access-logs');
// If the resolved path doesn't exist (e.g., public dir was missing), create it
if (!$logDir) {
$logDir = __DIR__ . '/../../public/last-access-logs';
if (!file_exists($logDir)) {
mkdir($logDir, 0777, true);
}
}
$logLine = date("Y-m-d H:i:s") . " : " . $uri . "\n";
file_put_contents($logDir . '/' . $name . '.txt', $logLine, FILE_APPEND);
}
}

View File

@ -0,0 +1,12 @@
<?php
use DI\ContainerBuilder;
use function DI\autowire;
use App\Services\LxdService;
return function (ContainerBuilder $containerBuilder) {
// Example service binding
$containerBuilder->addDefinitions([
\App\Services\LxdService::class => autowire()
]);
};

View File

@ -0,0 +1,143 @@
<?php
namespace App\Managers;
use App\Services\LxdService;
class LXDProxyManager
{
protected LxdService $lxdService;
protected string $mapFile = '/etc/nginx/lxd_map.conf';
public function __construct(LxdService $lxdService)
{
$this->lxdService = $lxdService;
}
/**
* Handle request to container: return IP or null if fallback needed.
*/
public function handleRequest(string $name, $domain)
{
try {
$container = $this->lxdService->containerExists($name);
if (!$container) {
$this->lxdService->createContainer($name);
sleep(10);
$this->lxdService->startContainer($name);
$maxRetries = 20;
$delay = 2;
for ($i = 0; $i < $maxRetries; $i++) {
$status = $this->lxdService->getStatus($name);
if ($status['metadata']['status'] === 'Running') {
break;
}
sleep($delay);
}
$this->lxdService->installPackages($name);
sleep(5);
$ipv4 = $this->lxdService->getContainerIP($name);
$maxRetries = 20;
$waitSeconds = 3;
$ready = false;
for ($i = 0; $i < $maxRetries; $i++) {
// Try to connect to port 80 on container IP (you can use fsockopen, curl, or similar)
$connection = @fsockopen($ipv4, 80, $errno, $errstr, 2);
if ($connection) {
fclose($connection);
$ready = true;
break;
}
sleep($waitSeconds);
}
$hostConfigPath = __DIR__ . '/../../config.json';
if (file_exists($hostConfigPath)) {
$hostConfigData = json_decode(file_get_contents($hostConfigPath), true);
} else {
// If the file doesn't exist, initialize an empty array
$hostConfigData = [];
}
// Step 3: Append the new container entry to the JSON data
$hostConfigData[$domain] = $name;
// Step 4: Write the updated data back to the host-config.json file
file_put_contents($hostConfigPath, json_encode($hostConfigData, JSON_PRETTY_PRINT));
@mkdir('/var/www/last-access', 0777, true);
@touch("/var/www/last-access/$name.txt");
return [
'status' => 'success',
'ip' => $ipv4,
];
}
if ($container['metadata']['status'] === 'Running') {
$ipv4 = $this->getIPv4FromMetadata($container['metadata']);
return [
'status' => 'success',
'ip' => $ipv4,
];
}
// Start the container and wait for IP
$this->lxdService->startContainer($name);
$maxRetries = 30;
$delay = 2;
$ipv4 = null;
for ($i = 0; $i < $maxRetries; $i++) {
$status = $this->lxdService->getStatus($name);
$ipv4 = $this->getIPv4FromMetadata($status['metadata']);
if (!empty($ipv4)) {
break;
}
sleep($delay);
}
if ($ipv4) {
return [
'status' => 'success',
'ip' => $ipv4,
];
}
return [
'status' => 'failed',
'message' => 'Container did not acquire an IP in time.',
];
} catch (\Exception $e) {
return [
'status' => 'failed',
'error' => $e->getMessage(),
];
}
}
public function getIPv4FromMetadata(array $metadata): ?string
{
if (
isset($metadata['network']['eth0']['addresses']) &&
is_array($metadata['network']['eth0']['addresses'])
) {
foreach ($metadata['network']['eth0']['addresses'] as $addr) {
if (isset($addr['family']) && $addr['family'] === 'inet' && isset($addr['address'])) {
return $addr['address'];
}
}
}
return null;
}
}

View File

@ -0,0 +1,11 @@
<?php
use Slim\App;
use App\Controllers\ProxyController;
return function (App $app) {
// Proxy route
$app->group('/api/v1', function ($group) {
$group->any('/proxy', [ProxyController::class, 'forward']);
});
};

View File

@ -0,0 +1,19 @@
<?php
use Slim\App;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Message\ResponseInterface;
use App\Controllers\CaptchaController;
return function (App $app) {
// Home route
$app->get('/', function (ServerRequestInterface $request, ResponseInterface $response) {
$response->getBody()->write('Welcome to the LXD App!');
return $response;
});
// Captcha route
$app->get('/api/captcha', [CaptchaController::class, 'get']);
};

View File

@ -0,0 +1,262 @@
<?php
namespace App\Services;
use Exception;
class LxdService
{
private string $baseUrl;
private string $socketPath;
public function __construct() {
// $this->baseUrl = "http://unix.socket";
$this->baseUrl = "https://localhost:8443";
$this->socketPath = "/var/snap/lxd/common/lxd/unix.socket";
}
private function request(string $method, string $endpoint, array $body = []): array {
$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);
// Specify your client certificate and key for TLS authentication
curl_setopt($ch, CURLOPT_SSLCERT, $clientCert);
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_URL, "{$this->baseUrl}{$endpoint}");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if (!empty($body)) {
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']);
}
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
// Check if curl_exec failed
if (curl_errno($ch)) {
throw new Exception("Curl error: " . curl_error($ch));
}
curl_close($ch);
if (!$response) {
throw new Exception("LXD API error or not reachable.");
}
$json = json_decode($response, true);
if ($httpCode >= 400) {
throw new Exception("LXD API Error: " . ($json['error'] ?? 'Unknown'));
}
return $json;
}
public function getStatus(string $name): ?array
{
try {
return $this->request('GET', "/1.0/instances/$name/state");
} catch (\Throwable $e) {
return null;
}
}
public function getContainer(string $name) {
try {
return $this->request('GET', "/1.0/instances/$name");
} catch (\Throwable $e) {
return null;
}
}
public function getContainerState(string $name) {
try {
return $this->request('GET', "/1.0/instances/$name/state");
} catch (\Throwable $e) {
return null;
}
}
public function containerExists(string $name): bool {
return $this->getContainerState($name) !== null;
}
public function startContainer(string $name): array {
$startResponse = $this->request('PUT', "/1.0/instances/$name/state", [
"action" => "start",
"timeout" => 30,
"force" => true
]);
if (!isset($startResponse['operation'])) {
throw new \Exception("No operation returned from start request");
}
$startOpUrl = $startResponse['operation'];
// 5. Wait for start operation to complete
do {
sleep(1);
$startOpResp = $this->request('GET', $startOpUrl);
$startStatus = $startOpResp['metadata']['status_code'] ?? 0;
} while ($startStatus < 200);
if ($startStatus >= 400) {
throw new \Exception("Failed to start container: " . json_encode($startOpResp));
}
// 6. Return final container info
$containerResponse = $this->getContainer($name);
return $containerResponse['metadata'] ?? [];
}
public function stopContainer(string $name): array {
$response = $this->request('PUT', "/1.0/instances/$name/state", [
"action" => "stop",
"timeout" => 30,
"force" => true
]);
return $response;
}
public function createContainer(string $name, string $fingerprint = "2edfd84b1396"): array {
$response = $this->request('POST', "/1.0/instances", [
"name" => $name,
"source" => [
"type" => "image",
"fingerprint" => $fingerprint
]
]);
sleep(5);
return $response;
}
public function installPackages(string $name) {
$log = '';
$log .= "=== apt update ===\n";
$out1 = shell_exec("/snap/bin/lxc exec $name -- bash -c 'apt update 2>&1'");
$log .= $out1 . "\n\n";
$log .= "=== apt install ===\n";
$out2 = shell_exec("/snap/bin/lxc exec $name -- bash -c 'apt install -y nginx mysql-server 2>&1'");
$log .= $out2 . "\n\n";
file_put_contents('/tmp/lxd_install.log', $log);
// Wait for nginx service to be active
$nginxActive = false;
for ($i = 0; $i < 10; $i++) {
$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;
for ($i = 0; $i < 10; $i++) {
$status = shell_exec("/snap/bin/lxc exec $name -- systemctl is-active mysql");
if (trim($status) === 'active') {
$mysqlActive = true;
break;
}
sleep(2);
}
if (!$nginxActive || !$mysqlActive) {
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']);
}
public function createContainerAndWait(string $name, array $config = []): array {
// 1. Prepare the creation request body
$body = [
"name" => $name,
"source" => array_merge([
"type" => "image",
"fingerprint" => "2edfd84b1396"
]),
];
$body = array_merge($body, $config);
// 2. Send creation request
$response = $this->request('POST', '/1.0/instances', $body);
if (!isset($response['operation'])) {
throw new \Exception("No operation returned from create request");
}
$operationUrl = $response['operation'];
// 3. Wait for container creation to finish
do {
sleep(1);
$opResponse = $this->request('GET', $operationUrl);
$statusCode = $opResponse['metadata']['status_code'] ?? 0;
} while ($statusCode < 200);
if ($statusCode >= 400) {
throw new \Exception("Container creation failed: " . json_encode($opResponse));
}
// 4. Start the container
return $this->startContainer($name);
}
public function waitForPort(string $ip, int $port = 80, int $timeout = 30): 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
}
// Function to get IPv4 address from metadata
public function getIPv4FromMetadata(array $metadata): ?string
{
if (
isset($metadata['network']['eth0']['addresses']) &&
is_array($metadata['network']['eth0']['addresses'])
) {
foreach ($metadata['network']['eth0']['addresses'] as $addr) {
if (isset($addr['family']) && $addr['family'] === 'inet' && isset($addr['address'])) {
return $addr['address'];
}
}
}
return null;
}
}

View File

@ -0,0 +1,11 @@
<?php
use DI\ContainerBuilder;
return function (ContainerBuilder $containerBuilder) {
$containerBuilder->addDefinitions([
'settings' => [
'displayErrorDetails' => true, // turn off in production
],
]);
};

View File

@ -0,0 +1,14 @@
<?php
namespace App\Utils;
class SubdomainHelper {
public static function getSubdomain(string $host, string $mainDomain): ?string {
if (!str_ends_with($host, $mainDomain)) {
return null;
}
$subPart = substr($host, 0, -strlen($mainDomain) - 1);
$parts = explode('.', $subPart);
return $parts[0] ?? null;
}
}

View File

@ -0,0 +1,239 @@
<?php
namespace App\lib;
/**
* PCaptcha :
* A simple/lightweight class provides you with the necessary tools to generate a friendly/secure captcha and validate it
*
* @author Bader Almutairi (Phpfalcon)
* @link https://github.com/phpfalcon/pcaptcha/
* @license http://opensource.org/licenses/MIT MIT License
*/
class PCaptcha
{
//options
var $width = 150;
var $hight = 45;
var $size = 0; //this will be generated automaticlly - you can set it manually
var $scale = 0; //this will be generated automaticlly - you can set it manually
var $leng = 5;
var $noise_level = 0;
var $pixlized = 2;
var $angle = 20;
var $grid = true;
var $font;
var $drawcross = false;
var $drawcircles = true;
var $background = array(0, 0, 0); //rand if empty
var $text = array(255, 255, 255); //white
var $colortwo = array(204, 204, 204); //grey
var $postfield = 'panswer';
/**
* called outside to genearate captcha
*
* @return void
*/
function get_captcha()
{
if (@extension_loaded('gd') && function_exists('gd_info')) {
try {
$this->gen_cpatcha_image();
} catch (Throwable $e) {
http_response_code(500);
header("Content-Type: text/plain");
echo "Captcha generation failed: " . $e->getMessage();
}
} else {
http_response_code(500);
header("Content-Type: text/plain");
echo "GD library not found";
}
exit();
}
/**
* Validate captcha data in post fields
*
* @return bool
*/
function validate_captcha($answer)
{
// $answer = trim((string) ($_POST[$this->postfield] ?? ''));
if((!empty($_SESSION['p_code']) && $answer != '') && ($_SESSION['p_code'] == $answer))
{
unset($_SESSION['p_code']);
return true;
}
return false;
}
/**
* Generate a CAPTCHA
*
* @return void
*/
function gen_cpatcha_image()
{
if(!isset($this->font))
{
$this->font = dirname(__FILE__) . DIRECTORY_SEPARATOR . 'arial.ttf';
}
if($this->background[0] == 0)
{
$this->background[0] = mt_rand(70, 200);
$this->background[1] = 0;
$this->background[2] = mt_rand(70, 200);
}
//generate a secure random int by using openssl
$hash = bin2hex(openssl_random_pseudo_bytes(16));
$num = hexdec( substr(sha1($hash) , 0,15) );
$security_code = substr(intval($num) ,0, $this->leng); //trim it down to $leng
//store the security code
$_SESSION["p_code"] = $security_code;
//Set the image width and height
$width = $this->width;
$height = $this->hight;
$sc = 0;
//gen scale
if($this->scale <= 0)
{
$sc = $height / $width;
$sc = ($sc < 1) ? 2 : $sc + 2;
}
else
{
$sc = $this->scale;
}
//gen size
$size = ($this->size <= 0) ? round($height / $sc) : $this->size;
//Create the image resource
$image = ImageCreate($width, $height);
//grab our paint tools , colors, white, black and gray
$white = ImageColorAllocate($image, $this->text[0], $this->text[1], $this->text[2]);
$black = ImageColorAllocate($image, $this->background[0], $this->background[1], $this->background[2]);
$grey = ImageColorAllocate($image, $this->colortwo[0], $this->colortwo[1], $this->colortwo[2]);
//fill the world in black
ImageFill($image, 0, 0, $black);
$text_count = strlen($security_code);
$text = str_split($security_code);
$step = round($width/3) - $sc * 15;
$y = $size + mt_rand(0, round($height/3) - $sc) ;
foreach($text as $txt)
{
$box = imagettfbbox( $size, 0, $this->font, $txt );
$x = abs($box[2]-$box[0]);
imagettftext($image, $size, mt_rand(-$this->angle, 0), $step, $y, $white, $this->font, $txt );
$step += ($x+5);
}
if($this->pixlized > 1)
{
imagefilter($image, IMG_FILTER_PIXELATE, $this->pixlized, true);
}
if($this->grid)
{
$transp = $black;
$this->imagegrid($image, $width, $height, 10, $transp);
}
if($this->drawcircles)
{
$transp2 = ImageColorAllocate($image, $this->background[0] + 30, $this->background[1], $this->background[2] + 30);
$this->imagecircles($image, $width, $height, 10, $transp2);
}
if ($this->noise_level > 0)
{
$noise_level = $this->noise_level;
for ($i = 0; $i < $noise_level; ++$i)
{
$x = mt_rand(2, $width);
$y = mt_rand(2, $height);
$size = 2;
imagefilledarc($image, $x, $y, $size, $size, 0, 360, $white, IMG_ARC_PIE);
}
}
//drow borders
ImageRectangle($image,0,0,$width-1,$height-1,$grey);
if($this->drawcross)
{
imageline($image, 0, $height/2, $width, $height/2, $grey);
imageline($image, $width/2, 0, $width/2, $height, $grey);
}
//header file
header("Content-Type: image/png");
//Output the newly created image in jpeg format
ImagePng($image);
//Free up resources
ImageDestroy($image);
}
/**
* Draw circles
* @param image $image given image
* @param width $w image's width
* @param height $w image's height
* @param size $w circles size
* @param color $color
* @return void
*/
function imagecircles($image, $w, $h, $s, $color)
{
$x = $w/2;
$y = $h/2;
for($iw=1; $iw<$w; $iw++)
{
imagearc($image, $x, $y, $iw*$s, $iw*$s, 0, 360, $color);
}
}
/**
* Draws a nice looking grid
* @param image $image given image
* @param width $w image's width
* @param height $w image's height
* @param size $w size
* @param color $color
* @return void
*/
function imagegrid($image, $w, $h, $s, $color)
{
for($iw=1; $iw<$w/$s; $iw++){imageline($image, $iw*$s, 0, $iw*$s, $w, $color );}
for($ih=1; $ih<$h/$s; $ih++){imageline($image, 0, $ih*$s, $w, $ih*$s, $color );}
}
}

Binary file not shown.

View File

@ -0,0 +1,68 @@
<?php
require __DIR__ . '/../../vendor/autoload.php'; // Adjust path as needed
use App\Services\LxdService;
// Initialize LXD service
$lxdService = new LxdService();
// Define the directory containing access logs
$logDir = realpath(__DIR__ . '/../../public/last-access-logs'); // Adjust if you're in /app/src/Cron or similar
// Define the idle threshold in minutes
$thresholdMinutes = 30;
// Iterate over all log files in the specified directory
foreach (glob($logDir . '/*.txt') as $filePath) {
// Extract the container name from the file name
$containerName = basename($filePath, '.txt');
// Get the last line from the log file
$lastLine = getLastLine($filePath);
if (!$lastLine) {
echo "No access logs found for $containerName.\n";
continue;
}
// Parse the timestamp from the last log entry
$parts = explode(' : ', $lastLine);
if (!isset($parts[0])) continue;
$lastAccess = DateTime::createFromFormat('Y-m-d H:i:s', trim($parts[0]));
if (!$lastAccess) continue;
// Calculate the idle time in seconds
$now = new DateTime();
$interval = $now->getTimestamp() - $lastAccess->getTimestamp();
// Check if the container has been idle for longer than the threshold
if ($interval > $thresholdMinutes * 60) {
echo "$containerName has been idle for over $thresholdMinutes minutes. Stopping...\n";
try {
// Check if the container exists and stop it if it does
if ($lxdService->containerExists($containerName)) {
$lxdService->stopContainer($containerName);
echo "Stopped container: $containerName\n";
} else {
echo "Container $containerName does not exist.\n";
}
} catch (Throwable $e) {
// Handle any errors that occur while stopping the container
echo "Error stopping $containerName: " . $e->getMessage() . "\n";
}
}
}
/**
* Get the last non-empty line from a file.
*
* @param string $filePath Path to the file.
* @return string|null The last line, or null if the file is empty.
*/
function getLastLine(string $filePath): ?string
{
$lines = file($filePath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
return $lines ? end($lines) : null;
}