forked from urvishpatelce/lxd-app
feat: change structure
This commit is contained in:
@ -6,7 +6,6 @@ This is a PHP-based API using the **Slim 4 Framework** that dynamically manages
|
||||
|
||||
## 🧩 Features
|
||||
|
||||
- Automatically creates and starts LXD containers per subdomain
|
||||
- CAPTCHA validation for provisioning
|
||||
- Proxies requests to containerized environments
|
||||
- Persists domain-to-container mapping
|
||||
@ -17,8 +16,7 @@ This is a PHP-based API using the **Slim 4 Framework** that dynamically manages
|
||||
|
||||
## 📁 Project Structure
|
||||
|
||||
backend/
|
||||
├── app/
|
||||
api/
|
||||
│ ├── src/
|
||||
│ │ ├── Controllers/
|
||||
│ │ │ └── ProxyController.php
|
||||
@ -39,7 +37,7 @@ backend/
|
||||
|
||||
## ⚙️ Requirements
|
||||
|
||||
- PHP 8.1+
|
||||
- PHP 8.4+
|
||||
- LXD installed and configured
|
||||
- PHP-FPM + NGINX
|
||||
- Composer
|
||||
@ -61,13 +59,14 @@ backend/
|
||||
|
||||
3. **Ensure PHP and NGINX are configured**
|
||||
|
||||
Check in backend/nginx.conf
|
||||
|
||||
NGINX config example:
|
||||
```bash
|
||||
Please check nginx.conf file in root
|
||||
|
||||
|
||||
4. **Map domain in /etc/hosts**
|
||||
```bash
|
||||
127.0.0.1 test.lxdapp.local
|
||||
127.0.0.1 testone.lxdapp.local
|
||||
|
||||
5. **Make sure LXD is working**
|
||||
```bash
|
||||
@ -75,14 +74,14 @@ backend/
|
||||
|
||||
6. **🔐 CAPTCHA Protection**
|
||||
|
||||
The /api/v1/proxy POST endpoint expects a field panswer with the correct CAPTCHA.
|
||||
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/v1/proxy
|
||||
POST /api
|
||||
|
||||
**Request Body:**
|
||||
```bash
|
||||
@ -93,7 +92,7 @@ backend/
|
||||
|
||||
**Headers:**
|
||||
|
||||
Origin: http://customer.lxdapp.local
|
||||
Origin: http://testone.lxdapp.local
|
||||
|
||||
**Response:**
|
||||
```bash
|
||||
3
api/config.json
Normal file
3
api/config.json
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"lxdapp.local": "testone"
|
||||
}
|
||||
@ -2,7 +2,7 @@ server {
|
||||
listen 80;
|
||||
server_name *.lxdapp.local lxdapp.local;
|
||||
|
||||
root /var/www/html/lxd-app/backend/app/public; # Adjust this to your Slim project's public folder
|
||||
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
|
||||
@ -17,20 +17,18 @@ server {
|
||||
# rewrite ^/app/(/.*)$ $1 break;
|
||||
}
|
||||
|
||||
# (Optional: Serve static assets directly if needed)
|
||||
# location /app/_nuxt/ {
|
||||
# proxy_pass http://127.0.0.1:3000;
|
||||
# }
|
||||
|
||||
# Handle PHP Slim
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
# try_files $uri $uri/ /index.php?$query_string;
|
||||
try_files $uri /index.php?$query_string;
|
||||
}
|
||||
|
||||
# Pass PHP scripts to PHP-FPM
|
||||
location ~ \.php$ {
|
||||
#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$fastcgi_script_name;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root/index.php;
|
||||
include fastcgi_params;
|
||||
}
|
||||
|
||||
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();
|
||||
@ -48,8 +48,9 @@ class LoginController
|
||||
// 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!']);
|
||||
return $this->json($response, ['status' => 'success', 'message' => 'Container started!', 'redirect' => $redirect]);
|
||||
} catch (\Throwable $e) {
|
||||
return $this->json($response, [
|
||||
'status' => 'error',
|
||||
@ -58,6 +59,68 @@ class LoginController
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
*/
|
||||
@ -22,10 +22,9 @@ class LxdService
|
||||
* @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");
|
||||
}
|
||||
// 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}");
|
||||
|
||||
@ -78,6 +77,7 @@ class LxdService
|
||||
return $this->request('GET', "/1.0/instances/$name/state");
|
||||
} catch (\Throwable $e) {
|
||||
return null;
|
||||
// echo 'Error: ' . $e->getMessage();
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,7 +99,6 @@ class LxdService
|
||||
* @throws Exception
|
||||
*/
|
||||
public function startContainer(string $name): array {
|
||||
|
||||
$startResponse = $this->request('PUT', "/1.0/instances/$name/state", [
|
||||
"action" => "start",
|
||||
"timeout" => 30,
|
||||
@ -107,41 +106,10 @@ class LxdService
|
||||
]);
|
||||
|
||||
if (!isset($startResponse['operation'])) {
|
||||
throw new \Exception("No operation returned from start request");
|
||||
throw new \Exception("Failed to start container, please try again.");
|
||||
}
|
||||
|
||||
$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, Please try again.");
|
||||
}
|
||||
|
||||
sleep(5);
|
||||
|
||||
// Poll services (Nginx and MySQL) for up to 30 seconds
|
||||
$maxRetries = 30;
|
||||
$retry = 0;
|
||||
|
||||
while ($retry < $maxRetries) {
|
||||
$nginxReady = $this->isServiceRunning($name, 'nginx');
|
||||
$mysqlReady = $this->isServiceRunning($name, 'mysql');
|
||||
|
||||
if ($nginxReady && $mysqlReady) {
|
||||
return $startResponse; // ✅ All good
|
||||
}
|
||||
|
||||
$retry++;
|
||||
sleep(1);
|
||||
}
|
||||
|
||||
throw new \Exception("Container started but services not ready.");
|
||||
return $startResponse;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -194,11 +162,11 @@ class LxdService
|
||||
return null;
|
||||
}
|
||||
|
||||
public function isServiceRunning(string $container, string $service): bool
|
||||
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) === 'active';
|
||||
return trim($output);
|
||||
}
|
||||
}
|
||||
0
frontend/.gitignore → app/.gitignore
vendored
0
frontend/.gitignore → app/.gitignore
vendored
@ -6,7 +6,7 @@ This is the **frontend Nuxt.js** application that works with the [LXD Proxy API]
|
||||
|
||||
## 🌐 Overview
|
||||
|
||||
- Each user has a subdomain (e.g., `mitul.lxdapp.local`)
|
||||
- Each user has a subdomain (e.g., `testone.lxdapp.local`)
|
||||
- On visiting the subdomain, a login page prompts for CAPTCHA
|
||||
- Upon successful CAPTCHA, a request is made to the backend to:
|
||||
- Create or start an LXD container
|
||||
@ -20,7 +20,7 @@ This is the **frontend Nuxt.js** application that works with the [LXD Proxy API]
|
||||
- Subdomain-aware dynamic environment provisioning
|
||||
- CAPTCHA login screen for triggering container creation
|
||||
- API integration with backend (Slim PHP) service
|
||||
- Axios-based POST to `/api/v1/proxy`
|
||||
- Axios-based POST to `/api/`
|
||||
|
||||
---
|
||||
|
||||
@ -34,11 +34,16 @@ This is the **frontend Nuxt.js** application that works with the [LXD Proxy API]
|
||||
```bash
|
||||
npm run dev
|
||||
|
||||
3. **Visit the subdomain**
|
||||
|
||||
http://testone.lxdapp.local:3000
|
||||
|
||||
⚠️ Make sure this domain is mapped in /etc/hosts and that the backend is running on the correct origin.
|
||||
|
||||
|
||||
**🗂 Project Structure**
|
||||
|
||||
frontend/
|
||||
app/
|
||||
├── pages/
|
||||
│ └── index.vue # Login screen
|
||||
├── plugins/
|
||||
@ -49,3 +54,39 @@ frontend/
|
||||
├── package.json
|
||||
└── README.md
|
||||
|
||||
|
||||
**🔐 CAPTCHA Flow**
|
||||
|
||||
User opens subdomain: testone.lxdapp.local
|
||||
|
||||
Enters the CAPTCHA value
|
||||
|
||||
Form submits:
|
||||
|
||||
POST /api/login with JSON payload:
|
||||
|
||||
{
|
||||
"source": "login",
|
||||
"panswer": "abc123"
|
||||
}
|
||||
|
||||
Adds header:
|
||||
|
||||
Origin: http://testone.lxdapp.local
|
||||
|
||||
**Backend:**
|
||||
|
||||
Validates CAPTCHA
|
||||
|
||||
Creates or starts container
|
||||
|
||||
Responds with container IP proxy
|
||||
|
||||
|
||||
**🧪 Development Notes**
|
||||
|
||||
config.json in backend maps subdomain to LXD container
|
||||
|
||||
This frontend app assumes the backend is on the same subdomain
|
||||
|
||||
If needed, configure proxy in nuxt.config.js or use relative API paths
|
||||
33
app/assets/css/common.css
Normal file
33
app/assets/css/common.css
Normal file
@ -0,0 +1,33 @@
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body{
|
||||
position: relative;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.page-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.title {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.8rem;
|
||||
color: #222;
|
||||
}
|
||||
.error-text {
|
||||
color: red;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
@ -13,13 +13,6 @@ export default defineNuxtConfig({
|
||||
apiUrl: `${process.env.BACKEND_URL}/api`
|
||||
}
|
||||
},
|
||||
nitro: {
|
||||
prerender: {
|
||||
crawlLinks: true,
|
||||
routes: ['/', '/login'], // Add known routes
|
||||
ignore: ['/*.php'], // Or leave empty to catch with [...slug].vue
|
||||
}
|
||||
},
|
||||
css: [
|
||||
'~/assets/css/common.css' // Path to your global CSS file
|
||||
],
|
||||
@ -78,7 +78,9 @@ onMounted(() => {
|
||||
const submitForm = async () => {
|
||||
loading.value = true;
|
||||
output.value = '';
|
||||
const config = useRuntimeConfig();
|
||||
const encodedRedirect = new URL(redirectTo.value, window.location.origin);
|
||||
encodedRedirect.searchParams.set('username', username.value);
|
||||
encodedRedirect.searchParams.set('password', password.value);
|
||||
|
||||
try {
|
||||
const res = await $fetch(`${window.location.origin}/api/login`, {
|
||||
@ -91,17 +93,16 @@ const submitForm = async () => {
|
||||
password: password.value,
|
||||
source: 'login',
|
||||
panswer: captchaValue.value,
|
||||
redirect: redirectTo.value,
|
||||
redirect: encodedRedirect.toString()
|
||||
},
|
||||
credentials: 'include',
|
||||
throwHttpErrors: false, // important: do NOT throw on 401/4xx
|
||||
});
|
||||
|
||||
captchaError.value = '';
|
||||
const encodedRedirect = new URL(redirectTo.value, window.location.origin);
|
||||
encodedRedirect.searchParams.set('username', username.value);
|
||||
encodedRedirect.searchParams.set('password', password.value);
|
||||
window.location.href = encodedRedirect.toString();
|
||||
|
||||
router.push(res.redirect);
|
||||
// window.location.href = encodedRedirect.toString();
|
||||
} catch (error) {
|
||||
if (error.statusCode === 400) {
|
||||
captchaError.value = error?.data?.message || 'Invalid CAPTCHA';
|
||||
@ -121,17 +122,6 @@ const submitForm = async () => {
|
||||
|
||||
|
||||
<style scoped>
|
||||
.page-wrapper {
|
||||
width: 100%;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
text-align: center;
|
||||
}
|
||||
.login-container {
|
||||
max-width: 100%;
|
||||
width: 400px;
|
||||
@ -141,14 +131,6 @@ const submitForm = async () => {
|
||||
background: #f9fafb;
|
||||
box-shadow: 0 4px 10px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
.title {
|
||||
text-align: center;
|
||||
margin-bottom: 1.5rem;
|
||||
font-weight: 700;
|
||||
font-size: 1.8rem;
|
||||
color: #222;
|
||||
}
|
||||
.input-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -175,12 +157,6 @@ const submitForm = async () => {
|
||||
background-color: #93c5fd; /* blue-300 */
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.error-text {
|
||||
color: red;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
.input-group input {
|
||||
padding: 0.8rem;
|
||||
border: 1px solid #ccc;
|
||||
74
app/pages/waiting.vue
Normal file
74
app/pages/waiting.vue
Normal file
@ -0,0 +1,74 @@
|
||||
<template>
|
||||
<div class="page-wrapper">
|
||||
<div class="waiting-container">
|
||||
<h2>Container Status: {{ status }}</h2>
|
||||
<p v-if="status === 'ready'">
|
||||
Your container is ready! <a :href="`http://${ip}`" target="_blank">Open it</a>
|
||||
</p>
|
||||
<p v-else-if="status === 'running'">Waiting for services to be ready...</p>
|
||||
<p v-else-if="status === 'starting'">Starting container...</p>
|
||||
<p v-else-if="status === 'error'" class="error-text">{{ errorMessage }}</p>
|
||||
</div>
|
||||
<Spinner :loading="loading" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const name = route.query.name;
|
||||
const redirect = route.query.redirect;
|
||||
const loading = ref(true);
|
||||
const status = ref('starting');
|
||||
const ip = ref(null);
|
||||
const interval = ref(null);
|
||||
const errorMessage = ref(null);
|
||||
|
||||
const checkStatus = async () => {
|
||||
try {
|
||||
const res = await $fetch(`${window.location.origin}/api/status?name=${name}`);
|
||||
status.value = res.status;
|
||||
|
||||
if (res.status === 'ready') {
|
||||
ip.value = res.ip;
|
||||
clearInterval(interval.value);
|
||||
loading.value = false;
|
||||
// Redirect or show login link
|
||||
window.location.href= redirect ?? `http://${res.ip}`;
|
||||
}
|
||||
} catch (error) {
|
||||
loading.value = false;
|
||||
status.value = 'error';
|
||||
errorMessage.value = error?.data?.message || 'Internal server error!';
|
||||
clearInterval(interval.value);
|
||||
setTimeout( () => {
|
||||
router.replace(`/?auth=ok&redirect=${redirect}`);
|
||||
}, 3000);
|
||||
|
||||
}
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
checkStatus();
|
||||
interval.value = setInterval(checkStatus, 3000); // Poll every 3s
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
clearInterval(interval.value);
|
||||
});
|
||||
</script>
|
||||
<style scoped>
|
||||
.waiting-container {
|
||||
max-width: 100%;
|
||||
width: 400px;
|
||||
margin: 5rem auto;
|
||||
padding: 2rem;
|
||||
border-radius: 8px;
|
||||
background: #f9fafb;
|
||||
box-shadow: 0 4px 10px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 4.2 KiB |
@ -1,3 +0,0 @@
|
||||
{
|
||||
"testone.lxdapp.local": "testone"
|
||||
}
|
||||
@ -1,166 +0,0 @@
|
||||
<?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']);
|
||||
|
||||
});
|
||||
|
||||
$app->any('/{routes:.*}', function (Request $request, Response $response, array $args) {
|
||||
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;
|
||||
|
||||
$lxd = new LxdService();
|
||||
|
||||
if (!$name) {
|
||||
return jsonResponse($response, ['status' => 'error', 'message' => 'Container does not exist.'], 404);
|
||||
}
|
||||
|
||||
if (!$lxd->containerExists($name)) {
|
||||
return jsonResponse($response, ['status' => 'error', 'message' => 'Container does not exist.'], 404);
|
||||
}
|
||||
|
||||
$containerInfo = $lxd->getContainerState($name);
|
||||
$status = $containerInfo['metadata']['status'] ?? 'Stopped';
|
||||
|
||||
if ($status !== 'Running') {
|
||||
$redirectUrl = (string) $request->getUri();
|
||||
return $response
|
||||
->withHeader('Location', 'app/?auth=ok&redirect=' . urlencode($redirectUrl))
|
||||
->withStatus(302);
|
||||
}
|
||||
|
||||
$ip = $lxd->getContainerIP($name);
|
||||
if (!$ip) {
|
||||
return jsonResponse($response, ['status' => 'error', 'message' => 'Could not fetch container IP'], 500);
|
||||
}
|
||||
|
||||
// BEGIN Proxy logic
|
||||
$baseUrl = "http://$ip/";
|
||||
$uri = $request->getUri();
|
||||
$path = $uri->getPath();
|
||||
|
||||
$prefix = '/api/';
|
||||
if (strpos($path, $prefix) === 0) {
|
||||
$path = substr($path, strlen($prefix));
|
||||
if ($path === '') {
|
||||
$path = '/';
|
||||
}
|
||||
}
|
||||
|
||||
$query = $uri->getQuery();
|
||||
$targetUrl = $baseUrl . $path . ($query ? '?' . $query : '');
|
||||
|
||||
Proxy::$AUTH_KEY = $_ENV['AUTH_KEY'] ?? 'YOUR_DEFAULT_AUTH_KEY';
|
||||
Proxy::$ENABLE_AUTH = true;
|
||||
Proxy::$HEADER_HTTP_PROXY_AUTH = 'HTTP_PROXY_AUTH';
|
||||
|
||||
$_SERVER['HTTP_PROXY_AUTH'] = Proxy::$AUTH_KEY;
|
||||
$_SERVER['HTTP_PROXY_TARGET_URL'] = $targetUrl;
|
||||
|
||||
$responseCode = Proxy::run();
|
||||
|
||||
LogWriterHelper::write($name, $targetUrl);
|
||||
|
||||
return $response;
|
||||
} catch (\Throwable $e) {
|
||||
return jsonResponse($response, [
|
||||
'status' => 'error',
|
||||
'message' => 'Internal Server Error: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* 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();
|
||||
@ -1,9 +0,0 @@
|
||||
*{
|
||||
box-sizing: border-box;
|
||||
}
|
||||
body{
|
||||
position: relative;
|
||||
padding: 0px;
|
||||
margin: 0px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
92
index.php
Normal file
92
index.php
Normal file
@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
// === Handle API requests ===
|
||||
$requestUri = $_SERVER['REQUEST_URI'];
|
||||
if (str_starts_with($requestUri, '/api/')) {
|
||||
require __DIR__ . '/api/public/index.php';
|
||||
exit;
|
||||
}
|
||||
|
||||
// === Setup ===
|
||||
require __DIR__ . "/api/vendor/autoload.php";
|
||||
require __DIR__ . "/api/src/Services/LxdService.php";
|
||||
|
||||
use Zounar\PHPProxy\Proxy;
|
||||
use App\Services\LxdService;
|
||||
|
||||
|
||||
$host = $_SERVER['HTTP_HOST'] ?? '';
|
||||
$method = $_SERVER['REQUEST_METHOD'];
|
||||
|
||||
// Load container mapping from domain config
|
||||
$config = json_decode(file_get_contents(__DIR__ . '/api/config.json'), true);
|
||||
$container = $config[$host] ?? null;
|
||||
$lxd = new LxdService();
|
||||
|
||||
// === Helper URLs ===
|
||||
$redirectBase = parseUrl("$host/app?auth=ok&redirect=" . urlencode(getFullUrl()));
|
||||
$waitingPage = parseUrl("$host/app/waiting?name=$container&redirect=" . urlencode($requestUri));
|
||||
|
||||
// === If container is missing or invalid ===
|
||||
if (!$container || !$lxd->containerExists($container)) {
|
||||
redirect($redirectBase);
|
||||
}
|
||||
|
||||
// === Check container status ===
|
||||
$state = $lxd->getContainerState($container)['metadata']['status'] ?? 'Stopped';
|
||||
|
||||
if ($state !== 'Running') {
|
||||
redirect($redirectBase);
|
||||
}
|
||||
|
||||
// === Get container IP ===
|
||||
$ip = $lxd->getContainerIP($container);
|
||||
if (!$ip) {
|
||||
redirect($waitingPage);
|
||||
}
|
||||
|
||||
// === Proxy to container ===
|
||||
proxy($container, "http://{$ip}{$requestUri}");
|
||||
exit;
|
||||
|
||||
|
||||
// === Functions ===
|
||||
|
||||
function redirect(string $to): void {
|
||||
header("Location: $to", true, 302);
|
||||
exit;
|
||||
}
|
||||
|
||||
function proxy(string $name, string $targetUrl): void {
|
||||
Proxy::$AUTH_KEY = $_ENV['AUTH_KEY'] ?? 'YOUR_DEFAULT_AUTH_KEY';
|
||||
Proxy::$ENABLE_AUTH = true;
|
||||
Proxy::$HEADER_HTTP_PROXY_AUTH = 'HTTP_PROXY_AUTH';
|
||||
|
||||
$_SERVER['HTTP_PROXY_AUTH'] = Proxy::$AUTH_KEY;
|
||||
$_SERVER['HTTP_PROXY_TARGET_URL'] = $targetUrl;
|
||||
|
||||
$responseCode = Proxy::run();
|
||||
writeLog($name, $targetUrl);
|
||||
}
|
||||
|
||||
function getFullUrl(): string {
|
||||
$protocol = (!empty($_SERVER['HTTPS']) || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
|
||||
$host = $_SERVER['HTTP_HOST'] ?? $_SERVER['SERVER_NAME'];
|
||||
return $protocol . $host . $_SERVER['REQUEST_URI'];
|
||||
}
|
||||
|
||||
function parseUrl(string $url = ''): string {
|
||||
$scheme = $_SERVER['REQUEST_SCHEME'] ?? 'http';
|
||||
return "$scheme://$url";
|
||||
}
|
||||
|
||||
function writeLog(string $name, string $uri): void {
|
||||
$logDir = __DIR__ . '/api/public/last-access-logs';
|
||||
|
||||
if (!is_dir($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