forked from urvishpatelce/lxd-app
feat: change structure
This commit is contained in:
9
api/.env
Normal file
9
api/.env
Normal file
@ -0,0 +1,9 @@
|
||||
MAIN_DOMAIN=lxdapp.local
|
||||
MAIN_COOKIE_DOMAIN=.lxdapp.local
|
||||
LXC_PATH=/snap/bin/lxc
|
||||
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
|
||||
AUTH_KEY=R9kX2HFA7ZjLdVYm8TsQWpCeNuB1v0GrS6MI4axf
|
||||
|
||||
132
api/README.md
Normal file
132
api/README.md
Normal file
@ -0,0 +1,132 @@
|
||||
# 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
|
||||
|
||||
- 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
|
||||
|
||||
api/
|
||||
│ ├── 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.4+
|
||||
- LXD installed and configured
|
||||
- PHP-FPM + NGINX
|
||||
- Composer
|
||||
- `slim/slim`
|
||||
- `slim/psr7`
|
||||
- `guzzlehttp/guzzle`
|
||||
- `vlucas/phpdotenv`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Setup Instructions
|
||||
|
||||
1. **put the folder in /var/www/html**
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
composer install
|
||||
|
||||
|
||||
3. **Ensure PHP and NGINX are configured**
|
||||
|
||||
NGINX config example:
|
||||
```bash
|
||||
Please check nginx.conf file in root
|
||||
|
||||
|
||||
4. **Map domain in /etc/hosts**
|
||||
```bash
|
||||
127.0.0.1 testone.lxdapp.local
|
||||
|
||||
5. **Make sure LXD is working**
|
||||
```bash
|
||||
lxc list
|
||||
|
||||
6. **🔐 CAPTCHA Protection**
|
||||
|
||||
The /api/ POST endpoint expects a field panswer with the correct CAPTCHA.
|
||||
|
||||
You can configure PCaptcha class for custom logic.
|
||||
|
||||
|
||||
7. **🧪 API Usage**
|
||||
|
||||
POST /api
|
||||
|
||||
**Request Body:**
|
||||
```bash
|
||||
{
|
||||
"source": "login",
|
||||
"panswer": "abc123"
|
||||
}
|
||||
|
||||
**Headers:**
|
||||
|
||||
Origin: http://testone.lxdapp.local
|
||||
|
||||
**Response:**
|
||||
```bash
|
||||
{
|
||||
"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):
|
||||
```bash
|
||||
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
api/composer.json
Normal file
14
api/composer.json
Normal file
@ -0,0 +1,14 @@
|
||||
{
|
||||
"require": {
|
||||
"slim/slim": "4.*",
|
||||
"slim/psr7": "^1.7",
|
||||
"php-di/php-di": "^7.0",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"zounar/php-proxy": "^1.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"App\\": "src/"
|
||||
}
|
||||
}
|
||||
}
|
||||
3
api/config.json
Normal file
3
api/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"lxdapp.local": "testone"
|
||||
}
|
||||
43
api/nginx.conf
Normal file
43
api/nginx.conf
Normal file
@ -0,0 +1,43 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name *.lxdapp.local lxdapp.local;
|
||||
|
||||
root /var/www/html/lxd-app; # Adjust this to your Slim project's public folder
|
||||
index index.php index.html index.htm;
|
||||
|
||||
# Reverse proxy to Vue.js for /app/ route
|
||||
location ^~ /app/ {
|
||||
proxy_pass http://127.0.0.1:3000/app/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
|
||||
# rewrite ^/app/(/.*)$ $1 break;
|
||||
}
|
||||
|
||||
# Handle PHP Slim
|
||||
location / {
|
||||
# try_files $uri $uri/ /index.php?$query_string;
|
||||
try_files $uri /index.php?$query_string;
|
||||
}
|
||||
|
||||
# Pass PHP scripts to PHP-FPM
|
||||
#location ~ \.php$ {
|
||||
location = /index.php {
|
||||
include snippets/fastcgi-php.conf;
|
||||
fastcgi_pass unix:/run/php/php8.4-fpm.sock;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
# Static file optimization (optional)
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|otf)$ {
|
||||
expires 30d;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
error_log /var/log/nginx/tutorial_error.log;
|
||||
access_log /var/log/nginx/tutorial_access.log;
|
||||
}
|
||||
3
api/public/.htaccess
Normal file
3
api/public/.htaccess
Normal file
@ -0,0 +1,3 @@
|
||||
RewriteEngine On
|
||||
RewriteCond %{REQUEST_FILENAME} !-f
|
||||
RewriteRule ^ index.php [QSA,L]
|
||||
91
api/public/index.php
Normal file
91
api/public/index.php
Normal file
@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use DI\ContainerBuilder;
|
||||
use Slim\Factory\AppFactory;
|
||||
use Dotenv\Dotenv;
|
||||
use App\Middleware\CorsMiddleware;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Message\ResponseInterface as Response;
|
||||
use App\Controllers\CaptchaController;
|
||||
use App\Controllers\LoginController;
|
||||
use App\Services\LxdService;
|
||||
use Zounar\PHPProxy\Proxy;
|
||||
use App\Utils\LogWriterHelper;
|
||||
|
||||
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([
|
||||
'lifetime' => 0,
|
||||
'path' => '/',
|
||||
'domain' => $domain,
|
||||
'secure' => false, // set true if using HTTPS
|
||||
'httponly' => true,
|
||||
'samesite' => 'Lax',
|
||||
]);
|
||||
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
// 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();
|
||||
|
||||
// 🔹 CORS middleware
|
||||
$app->add(CorsMiddleware::class);
|
||||
|
||||
|
||||
// Register middleware
|
||||
(require __DIR__ . '/../src/Bootstrap/Middleware.php')($app);
|
||||
|
||||
|
||||
// Register routes
|
||||
|
||||
// API contianer proxy route
|
||||
$app->group('/api', function ($group) {
|
||||
$group->get('/captcha', [CaptchaController::class, 'get']);
|
||||
$group->post('/login', [LoginController::class, 'index']);
|
||||
$group->get('/status', [LoginController::class, 'status']);
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* JSON response helper
|
||||
*/
|
||||
function jsonResponse(Response $response, array $data, int $status = 200): Response {
|
||||
$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);
|
||||
}
|
||||
|
||||
// Run app
|
||||
$app->run();
|
||||
40
api/src/Bootstrap/Middleware.php
Normal file
40
api/src/Bootstrap/Middleware.php
Normal 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);
|
||||
};
|
||||
34
api/src/Controllers/CaptchaController.php
Normal file
34
api/src/Controllers/CaptchaController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
139
api/src/Controllers/LoginController.php
Normal file
139
api/src/Controllers/LoginController.php
Normal file
@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Lib\PCaptcha;
|
||||
use App\Services\LxdService;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use App\Utils\LogWriterHelper;
|
||||
|
||||
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');
|
||||
}
|
||||
|
||||
$configPath = __DIR__ . '/../../config.json';
|
||||
$config = file_exists($configPath) ? json_decode(file_get_contents($configPath), true) : [];
|
||||
$name = $config[$domain] ?? null;
|
||||
|
||||
$params = (array)$request->getParsedBody();
|
||||
|
||||
$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());
|
||||
|
||||
$redirect = '/waiting?name='.$name .'&redirect='.urlencode($params['redirect']);
|
||||
// Login success
|
||||
return $this->json($response, ['status' => 'success', 'message' => 'Container started!', 'redirect' => $redirect]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->json($response, [
|
||||
'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',
|
||||
'message' => 'Missing container name.',
|
||||
], 400);
|
||||
}
|
||||
|
||||
$lxd = new LxdService();
|
||||
|
||||
$state = $lxd->getContainerState($name);
|
||||
if (!$state) {
|
||||
return $this->json($response, [
|
||||
'status' => 'not_found',
|
||||
'message' => 'Container not found',
|
||||
], 404);
|
||||
}
|
||||
|
||||
$status = $state['metadata']['status'] ?? 'Stopped';
|
||||
if ($status !== 'Running') {
|
||||
return $this->json($response, [
|
||||
'status' => 'starting',
|
||||
'message' => 'Container is not yet running',
|
||||
]);
|
||||
}
|
||||
|
||||
$ip = $lxd->getContainerIP($name);
|
||||
$nginx = $lxd->isServiceRunning($name, 'nginx');
|
||||
$mysql = $lxd->isServiceRunning($name, 'mysql');
|
||||
if ($ip && $nginx === 'active' && $mysql === 'active') {
|
||||
return $this->json($response, [
|
||||
'status' => 'ready',
|
||||
'ip' => $ip,
|
||||
'message' => 'Container is ready',
|
||||
]);
|
||||
}
|
||||
|
||||
if($nginx === 'failed'){
|
||||
return $this->json($response, [
|
||||
'status' => 'failed',
|
||||
'message' => 'Failed to start web server service in container',
|
||||
], 400);
|
||||
}
|
||||
|
||||
if($mysql === 'failed'){
|
||||
return $this->json($response, [
|
||||
'status' => 'failed',
|
||||
'message' => 'Failed to start mysql service in container',
|
||||
], 400);
|
||||
}
|
||||
|
||||
return $this->json($response, [
|
||||
'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);
|
||||
|
||||
$response->getBody()->write($payload);
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withHeader('Access-Control-Allow-Origin', '*')
|
||||
->withStatus($status);
|
||||
}
|
||||
|
||||
}
|
||||
12
api/src/Dependencies/Dependencies.php
Normal file
12
api/src/Dependencies/Dependencies.php
Normal 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()
|
||||
]);
|
||||
};
|
||||
239
api/src/Lib/PCaptcha.php
Normal file
239
api/src/Lib/PCaptcha.php
Normal 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 );}
|
||||
}
|
||||
}
|
||||
BIN
api/src/Lib/arial.ttf
Normal file
BIN
api/src/Lib/arial.ttf
Normal file
Binary file not shown.
28
api/src/Middleware/CorsMiddleware.php
Normal file
28
api/src/Middleware/CorsMiddleware.php
Normal file
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
namespace App\Middleware;
|
||||
|
||||
use Psr\Http\Server\MiddlewareInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface as Request;
|
||||
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Slim\Psr7\Response;
|
||||
|
||||
class CorsMiddleware implements MiddlewareInterface
|
||||
{
|
||||
public function process(Request $request, RequestHandler $handler): ResponseInterface
|
||||
{
|
||||
$origin = $_SERVER['HTTP_ORIGIN'] ?? '*';
|
||||
|
||||
if ($request->getMethod() === 'OPTIONS') {
|
||||
$response = new Response(204);
|
||||
} else {
|
||||
$response = $handler->handle($request);
|
||||
}
|
||||
|
||||
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');
|
||||
}
|
||||
}
|
||||
68
api/src/Scripts/auto-stop-containers.php
Executable file
68
api/src/Scripts/auto-stop-containers.php
Executable 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;
|
||||
}
|
||||
172
api/src/Services/LxdService.php
Normal file
172
api/src/Services/LxdService.php
Normal file
@ -0,0 +1,172 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
use Exception;
|
||||
class LxdService
|
||||
{
|
||||
private string $baseUrl;
|
||||
|
||||
public function __construct() {
|
||||
$this->baseUrl = $_ENV['LXD_API_URL'] ?? 'https://localhost:8443';
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
// 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}");
|
||||
|
||||
// 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';
|
||||
|
||||
|
||||
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, $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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
try {
|
||||
return $this->request('GET', "/1.0/instances/$name/state");
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
// echo 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
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 {
|
||||
$startResponse = $this->request('PUT', "/1.0/instances/$name/state", [
|
||||
"action" => "start",
|
||||
"timeout" => 30,
|
||||
"force" => true
|
||||
]);
|
||||
|
||||
if (!isset($startResponse['operation'])) {
|
||||
throw new \Exception("Failed to start container, please try again.");
|
||||
}
|
||||
|
||||
return $startResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops a container.
|
||||
*
|
||||
* @param string $name Container name
|
||||
* @return array Response from the API
|
||||
*/
|
||||
public function stopContainer(string $name): array {
|
||||
$response = $this->request('PUT', "/1.0/instances/$name/state", [
|
||||
"action" => "stop",
|
||||
"timeout" => 30,
|
||||
"force" => true
|
||||
]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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']);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
11
api/src/Settings/Settings.php
Normal file
11
api/src/Settings/Settings.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
use DI\ContainerBuilder;
|
||||
|
||||
return function (ContainerBuilder $containerBuilder) {
|
||||
$containerBuilder->addDefinitions([
|
||||
'settings' => [
|
||||
'displayErrorDetails' => true, // turn off in production
|
||||
],
|
||||
]);
|
||||
};
|
||||
21
api/src/Utils/LogWriterHelper.php
Normal file
21
api/src/Utils/LogWriterHelper.php
Normal file
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace App\Utils;
|
||||
|
||||
class LogWriterHelper {
|
||||
public static function write(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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user