Merge feature/ssh-auth-draft into master — release v2.0.0
This commit is contained in:
+32
@@ -32,6 +32,21 @@ test_qr.svg
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Local tooling / scratch
|
||||
.trae/
|
||||
scripts/_cycle_out/
|
||||
|
||||
# Local exported client configs (do not commit)
|
||||
oleg*.conf
|
||||
adminnew*.conf
|
||||
________*.conf
|
||||
*fixed*.conf
|
||||
test_client_simple.conf
|
||||
|
||||
# Local QR exports / screenshots
|
||||
*_QR.png
|
||||
photo_*.jpeg
|
||||
|
||||
# Build artifacts
|
||||
dist/
|
||||
build/
|
||||
@@ -42,3 +57,20 @@ backup/
|
||||
backups/
|
||||
TEST_RESULTS.md
|
||||
LDAP_FEATURE.md
|
||||
|
||||
# Documentation and Tests
|
||||
tests/
|
||||
docs/
|
||||
amnezia-web-panel.code-workspace
|
||||
restore_local.php
|
||||
test_protocols.php
|
||||
scripts/regen_qr.php
|
||||
scripts/test_xray_install.sh
|
||||
scripts/test_online.php
|
||||
API_AWG_DOCS.md
|
||||
log.txt
|
||||
scripts/bootstrap_awg_container.sh
|
||||
scripts/fix_server_visibility.sh
|
||||
scripts/remote_fix_client_create.sh
|
||||
scripts/retest_client_api.sh
|
||||
scripts/awg2_retest_final.sh
|
||||
|
||||
@@ -18,6 +18,34 @@ Response:
|
||||
}
|
||||
```
|
||||
|
||||
## Protocols
|
||||
|
||||
### List Active Protocols (for JWT API clients)
|
||||
```bash
|
||||
curl -X GET http://localhost:8082/api/protocols/active \
|
||||
-H "Authorization: Bearer $TOKEN"
|
||||
```
|
||||
|
||||
Example response:
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"protocols": [
|
||||
{"id": 11, "slug": "awg2", "name": "AmneziaWG 2.0"},
|
||||
{"id": 13, "slug": "aivpn", "name": "AIVPN"},
|
||||
{"id": 12, "slug": "mtproxy", "name": "MTProxy (Telegram)"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Install Protocol on Server
|
||||
```bash
|
||||
curl -X POST http://localhost:8082/api/servers/1/protocols/install \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"protocol_id":11}'
|
||||
```
|
||||
|
||||
## Clients
|
||||
|
||||
### Create Client with QR Code
|
||||
|
||||
@@ -14,6 +14,7 @@ RUN apt-get update && apt-get install -y \
|
||||
qrencode \
|
||||
cron \
|
||||
libldap2-dev \
|
||||
docker.io \
|
||||
&& docker-php-ext-configure ldap --with-libdir=lib/x86_64-linux-gnu/ \
|
||||
&& docker-php-ext-install pdo_mysql mbstring exif pcntl bcmath gd ldap \
|
||||
&& a2enmod rewrite \
|
||||
@@ -56,6 +57,14 @@ RUN chmod +x /var/www/html/bin/monitor_metrics.sh
|
||||
# Create startup script
|
||||
RUN echo '#!/bin/bash\n\
|
||||
service cron start\n\
|
||||
# Ensure www-data can talk to host docker socket if mounted\n\
|
||||
if [ -S /var/run/docker.sock ]; then\n\
|
||||
SOCK_GID=$(stat -c %g /var/run/docker.sock)\n\
|
||||
if ! getent group docker >/dev/null; then\n\
|
||||
groupadd -g "$SOCK_GID" docker || true\n\
|
||||
fi\n\
|
||||
usermod -aG docker www-data || true\n\
|
||||
fi\n\
|
||||
# Start metrics collector on container startup\n\
|
||||
/bin/bash /var/www/html/bin/monitor_metrics.sh\n\
|
||||
apache2-foreground' > /start.sh \
|
||||
|
||||
@@ -4,11 +4,15 @@ Web-based management panel for Amnezia AWG (WireGuard) VPN servers.
|
||||
|
||||
## Features
|
||||
|
||||
- VPN server deployment via SSH
|
||||
- VPN server deployment via SSH (Password or **SSH Key**)
|
||||
- **Import from existing VPN panels** (wg-easy, 3x-ui)
|
||||
- **Advanced Protocol Management** (WireGuard, AmneziaWG, OpenVPN, Shadowsocks, etc.)
|
||||
- **AI-powered Protocol Configuration** using OpenRouter (optional)
|
||||
- Client configuration management with **expiration dates**
|
||||
- **Traffic limits** for clients with automatic enforcement
|
||||
- **Server backup and restore** functionality
|
||||
- **Scenario Testing**: Define and test different VPN connection scenarios across protocols
|
||||
- **Advanced Log Management**: View, search, and manage system and container logs
|
||||
- Traffic statistics monitoring
|
||||
- QR code generation for mobile apps
|
||||
- Multi-language interface (English, Russian, Spanish, German, French, Chinese)
|
||||
@@ -16,6 +20,19 @@ Web-based management panel for Amnezia AWG (WireGuard) VPN servers.
|
||||
- User authentication and access control
|
||||
- **Automatic client expiration and traffic limit checks** via cron
|
||||
|
||||
## Available Protocols
|
||||
|
||||
- AmneziaWG Advanced (`amnezia-wg-advanced`)
|
||||
- AmneziaWG 2.0 (`awg2`)
|
||||
- WireGuard Standard (`wireguard-standard`)
|
||||
- OpenVPN (`openvpn`)
|
||||
- Shadowsocks (`shadowsocks`)
|
||||
- XRay VLESS (`xray-vless`)
|
||||
- MTProxy (Telegram) (`mtproxy`)
|
||||
- SMB Server (`smb`)
|
||||
- AIVPN (`aivpn`) - https://github.com/infosave2007/aivpn
|
||||
|
||||
|
||||
## Requirements
|
||||
|
||||
- Docker
|
||||
@@ -32,15 +49,50 @@ cp .env.example .env
|
||||
docker compose up -d
|
||||
docker compose exec web composer install
|
||||
|
||||
# Apply migrations (fresh install + updates)
|
||||
# 1) bootstrap base schema
|
||||
docker compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < migrations/001_init.sql
|
||||
|
||||
# 2) apply the rest (safe to run repeatedly)
|
||||
for f in migrations/*.sql; do
|
||||
[ "$(basename "$f")" = "001_init.sql" ] && continue
|
||||
docker compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < "$f" || true
|
||||
done
|
||||
|
||||
# Or for older Docker Compose V1
|
||||
docker-compose up -d
|
||||
docker-compose exec web composer install
|
||||
|
||||
docker-compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < migrations/001_init.sql
|
||||
|
||||
for f in migrations/*.sql; do
|
||||
[ "$(basename "$f")" = "001_init.sql" ] && continue
|
||||
docker-compose exec -T db mysql -u"$DB_USERNAME" -p"$DB_PASSWORD" "$DB_DATABASE" < "$f" || true
|
||||
done
|
||||
```
|
||||
|
||||
Access: http://localhost:8082
|
||||
|
||||
Default login: admin@amnez.ia / admin123
|
||||
|
||||
### Remote Server Prerequisite
|
||||
|
||||
For protocol deployment on a clean remote host, Docker Engine must be available on that host.
|
||||
If Docker is missing, install it first (Ubuntu example):
|
||||
|
||||
```bash
|
||||
apt-get update -y
|
||||
apt-get install -y ca-certificates curl gnupg lsb-release
|
||||
install -m 0755 -d /etc/apt/keyrings
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --batch --yes --dearmor -o /etc/apt/keyrings/docker.gpg
|
||||
chmod a+r /etc/apt/keyrings/docker.gpg
|
||||
. /etc/os-release
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu ${VERSION_CODENAME} stable" > /etc/apt/sources.list.d/docker.list
|
||||
apt-get update -y
|
||||
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
|
||||
systemctl enable --now docker
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Edit `.env`:
|
||||
@@ -50,7 +102,7 @@ DB_HOST=db
|
||||
DB_PORT=3306
|
||||
DB_DATABASE=amnezia_panel
|
||||
DB_USERNAME=amnezia
|
||||
DB_PASSWORD=amnezia123
|
||||
DB_PASSWORD=amnezia
|
||||
|
||||
ADMIN_EMAIL=admin@amnez.ia
|
||||
ADMIN_PASSWORD=admin123
|
||||
@@ -63,7 +115,10 @@ JWT_SECRET=your-secret-key-change-this
|
||||
### Add VPN Server
|
||||
|
||||
1. Servers → Add Server
|
||||
2. Enter: name, host IP, SSH port, username, password
|
||||
1. Servers → Add Server
|
||||
2. Enter: name, host IP, SSH port, username
|
||||
3. Choose authentication method: **Password** or **SSH Key**
|
||||
- For SSH Key: Paste your private key (PEM/OpenSSH format)
|
||||
3. **(Optional) Enable import from existing panel:**
|
||||
- Check "Import from existing panel"
|
||||
- Select panel type (wg-easy or 3x-ui)
|
||||
@@ -141,6 +196,30 @@ curl -X POST http://localhost:8082/api/servers/1/restore \
|
||||
-d '{"backup_id": 123}'
|
||||
```
|
||||
|
||||
### Protocol Management
|
||||
|
||||
Manage VPN protocols via **Settings → Protocols**:
|
||||
- Install/Uninstall protocols (WireGuard, AmneziaWG, OpenVPN, etc.)
|
||||
- Configure protocol settings (ports, transport, obfuscation)
|
||||
- **AI Assistant**: Use "Ask AI" to generate complex protocol configurations tailored to your needs (requires OpenRouter API key).
|
||||
|
||||
### Scenario Testing & Logs
|
||||
|
||||
**Scenario Testing**:
|
||||
- Create test scenarios to verify connectivity across different protocols and network conditions.
|
||||
- Run automated tests to ensure your VPN infrastructure is reliable.
|
||||
|
||||
**Log Management**:
|
||||
- Centralized view of all system, container, and application logs.
|
||||
- Search and filter capabilities to quickly diagnose issues.
|
||||
|
||||
### AI Assistant
|
||||
|
||||
Configure OpenRouter API key in **Settings** to enable:
|
||||
- Auto-translation of the interface
|
||||
- AI-assisted protocol configuration
|
||||
- Intelligent troubleshooting suggestions
|
||||
|
||||
### Automatic Monitoring and Metrics Collection
|
||||
|
||||
**Metrics collector runs automatically** on container startup and is monitored by cron every 3 minutes. If the process crashes, it will be automatically restarted.
|
||||
@@ -222,16 +301,25 @@ DELETE /api/servers/{id}/delete - Delete server by ID
|
||||
GET /api/servers/{id}/clients - List clients on server
|
||||
```
|
||||
|
||||
### Protocols
|
||||
```
|
||||
GET /api/protocols/active - List all available protocols (JWT-friendly, includes protocol IDs)
|
||||
GET /api/protocols - Protocol management endpoint (requires session admin auth, not JWT)
|
||||
GET /api/servers/{id}/protocols - List installed protocols on server
|
||||
POST /api/servers/{id}/protocols/install - Install protocol
|
||||
```
|
||||
|
||||
### Clients
|
||||
```
|
||||
GET /api/clients - List all clients
|
||||
GET /api/clients/{id}/details - Get client details with stats, config and QR code
|
||||
GET /api/clients/{id}/qr - Get client QR code
|
||||
POST /api/clients/create - Create new client (returns config and QR code)
|
||||
Parameters: server_id, name, expires_in_days (optional)
|
||||
Parameters: server_id, name, protocol_id (optional, default: installed), expires_in_days (optional)
|
||||
POST /api/clients/{id}/revoke - Revoke client access
|
||||
POST /api/clients/{id}/restore - Restore client access
|
||||
DELETE /api/clients/{id}/delete - Delete client by ID
|
||||
DELETE /api/clients/{id}/delete - Delete client by ID (removes from DB and server)
|
||||
POST /api/clients/{id}/set-expiration - Set client expiration date
|
||||
POST /api/clients/{id}/set-expiration - Set client expiration date
|
||||
Parameters: expires_at (Y-m-d H:i:s or null)
|
||||
POST /api/clients/{id}/extend - Extend client expiration
|
||||
@@ -284,6 +372,8 @@ inc/ - Core classes
|
||||
JWT.php - Token auth
|
||||
QrUtil.php - QR code generation
|
||||
PanelImporter.php - Import from wg-easy/3x-ui
|
||||
InstallProtocolManager.php - Protocol management core
|
||||
OpenRouterService.php - AI integration
|
||||
templates/ - Twig templates
|
||||
migrations/ - SQL migrations (executed in alphabetical order)
|
||||
```
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
require_once __DIR__ . '/../inc/Config.php';
|
||||
Config::load(__DIR__ . '/../.env');
|
||||
require_once __DIR__ . '/../inc/DB.php';
|
||||
require_once __DIR__ . '/../inc/VpnServer.php';
|
||||
require_once __DIR__ . '/../inc/VpnClient.php';
|
||||
@@ -55,6 +56,17 @@ while (true) {
|
||||
|
||||
$monitoring = new ServerMonitoring($server['id']);
|
||||
|
||||
// Enforce single IP per user for Xray servers
|
||||
$containerName = $server['container_name'] ?? '';
|
||||
if (strpos($containerName, 'xray') !== false) {
|
||||
$monitoring->enforceXraySingleIpPerUser();
|
||||
}
|
||||
|
||||
// Enforce single IP per peer for AWG servers
|
||||
if (strpos($containerName, 'awg') !== false || strpos($containerName, 'wireguard') !== false) {
|
||||
$monitoring->enforceAwgSingleIpPerPeer();
|
||||
}
|
||||
|
||||
// Collect server metrics
|
||||
$serverMetrics = $monitoring->collectMetrics();
|
||||
echo " Server: CPU={$serverMetrics['cpu_percent']}% RAM={$serverMetrics['ram_used_mb']}/{$serverMetrics['ram_total_mb']}MB ";
|
||||
|
||||
@@ -0,0 +1,378 @@
|
||||
<?php
|
||||
|
||||
class AIController
|
||||
{
|
||||
|
||||
private $openRouterService;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->openRouterService = new OpenRouterService();
|
||||
}
|
||||
|
||||
/**
|
||||
* AI Assistant endpoint for generating installation scripts
|
||||
*/
|
||||
public function assist(): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$input) {
|
||||
throw new Exception('Invalid JSON input');
|
||||
}
|
||||
|
||||
$prompt = trim($input['prompt'] ?? '');
|
||||
$model = trim($input['model'] ?? 'openai/gpt-3.5-turbo');
|
||||
$protocolType = trim($input['protocol_type'] ?? '');
|
||||
$protocolId = isset($input['protocol_id']) ? (int) $input['protocol_id'] : null;
|
||||
$target = trim($input['target'] ?? 'install'); // install, uninstall, template
|
||||
|
||||
if (empty($prompt)) {
|
||||
throw new Exception('Prompt is required');
|
||||
}
|
||||
|
||||
// Generate enhanced prompt based on protocol type and target
|
||||
$enhancedPrompt = $this->enhancePrompt($prompt, $protocolType, $target);
|
||||
|
||||
// Call OpenRouter API
|
||||
$response = $this->openRouterService->generateScript($enhancedPrompt, $model);
|
||||
|
||||
// Save AI generation to database
|
||||
$generationId = $this->saveAIGeneration([
|
||||
'protocol_id' => $protocolId,
|
||||
'model_used' => $model,
|
||||
'prompt' => "[$target] " . $prompt,
|
||||
'generated_script' => $response['script'] ?? '',
|
||||
'suggestions' => json_encode($response['suggestions'] ?? []),
|
||||
'ubuntu_compatible' => $response['ubuntu_compatible'] ?? null,
|
||||
'created_at' => date('Y-m-d H:i:s')
|
||||
]);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => [
|
||||
'script' => $response['script'] ?? '',
|
||||
'suggestions' => $response['suggestions'] ?? [],
|
||||
'ubuntu_compatible' => $response['ubuntu_compatible'] ?? false,
|
||||
'estimated_time' => $response['estimated_time'] ?? '5 minutes',
|
||||
'model_used' => $model,
|
||||
'generation_id' => $generationId,
|
||||
'target' => $target
|
||||
]
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in AIController::assist: " . $e->getMessage());
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available AI models
|
||||
*/
|
||||
public function getModels(): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$models = $this->openRouterService->getAvailableModels();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $models
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in AIController::getModels: " . $e->getMessage());
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function testModel(): void
|
||||
{
|
||||
requireAdmin();
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$input) {
|
||||
throw new Exception('Invalid JSON input');
|
||||
}
|
||||
|
||||
$model = trim($input['model'] ?? '');
|
||||
if ($model === '') {
|
||||
throw new Exception('Model id is required');
|
||||
}
|
||||
|
||||
$result = $this->openRouterService->testModelAvailability($model);
|
||||
|
||||
echo json_encode([
|
||||
'success' => $result['success'] ?? false,
|
||||
'message' => $result['message'] ?? null,
|
||||
'http_code' => $result['http_code'] ?? null
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AI generation history for a protocol
|
||||
*/
|
||||
public function getGenerationHistory(int $protocolId): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$generations = $this->getAIGenerationsByProtocol($protocolId);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $generations
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in AIController::getGenerationHistory: " . $e->getMessage());
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply AI-generated script to protocol
|
||||
*/
|
||||
public function applyGeneration(int $generationId): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$generation = $this->getAIGeneration($generationId);
|
||||
if (!$generation) {
|
||||
throw new Exception('AI generation not found');
|
||||
}
|
||||
|
||||
if (!$generation['protocol_id']) {
|
||||
throw new Exception('This generation is not associated with any protocol');
|
||||
}
|
||||
|
||||
// Determine target from prompt
|
||||
$target = 'install';
|
||||
if (preg_match('/^\[(uninstall|template)\]/', $generation['prompt'], $matches)) {
|
||||
$target = $matches[1];
|
||||
}
|
||||
|
||||
// Update protocol with generated script
|
||||
$pdo = DB::conn();
|
||||
|
||||
if ($target === 'uninstall') {
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE protocols
|
||||
SET uninstall_script = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([
|
||||
$generation['generated_script'],
|
||||
date('Y-m-d H:i:s'),
|
||||
$generation['protocol_id']
|
||||
]);
|
||||
} elseif ($target === 'template') {
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE protocols
|
||||
SET output_template = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([
|
||||
$generation['generated_script'],
|
||||
date('Y-m-d H:i:s'),
|
||||
$generation['protocol_id']
|
||||
]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare('
|
||||
UPDATE protocols
|
||||
SET install_script = ?, ubuntu_compatible = ?, updated_at = ?
|
||||
WHERE id = ?
|
||||
');
|
||||
$stmt->execute([
|
||||
$generation['generated_script'],
|
||||
$generation['ubuntu_compatible'],
|
||||
date('Y-m-d H:i:s'),
|
||||
$generation['protocol_id']
|
||||
]);
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'AI-generated script applied to protocol successfully'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in AIController::applyGeneration: " . $e->getMessage());
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preview AI-generated script with syntax highlighting
|
||||
*/
|
||||
public function previewGeneration(int $generationId): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$generation = $this->getAIGeneration($generationId);
|
||||
if (!$generation) {
|
||||
throw new Exception('AI generation not found');
|
||||
}
|
||||
|
||||
View::render('ai/preview_generation.twig', [
|
||||
'generation' => $generation,
|
||||
'script' => $generation['generated_script'],
|
||||
'suggestions' => json_decode($generation['suggestions'], true) ?? []
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$_SESSION['protocol_error'] = $e->getMessage();
|
||||
redirect('/settings/protocols');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enhance user prompt with context and requirements
|
||||
*/
|
||||
private function enhancePrompt(string $prompt, string $protocolType, string $target = 'install'): string
|
||||
{
|
||||
$context = "";
|
||||
|
||||
if ($target === 'uninstall') {
|
||||
$context = "Create a bash uninstallation script for a VPN protocol. ";
|
||||
} elseif ($target === 'template') {
|
||||
$context = "Create a WireGuard-compatible client config template. ";
|
||||
} else {
|
||||
$context = "Create a bash installation script for a VPN protocol. ";
|
||||
}
|
||||
|
||||
if ($protocolType) {
|
||||
$context .= "This is for a $protocolType protocol. ";
|
||||
}
|
||||
|
||||
$context .= "Requirements:\n";
|
||||
|
||||
if ($target === 'template') {
|
||||
$context .= "- Use Mustache-style placeholders like {{private_key}}, {{client_ip}}, {{server_host}}, {{server_port}}\n";
|
||||
$context .= "- The template should be valid configuration format (e.g. .conf for WireGuard)\n";
|
||||
} elseif ($target === 'uninstall') {
|
||||
$context .= "- The script should be compatible with Ubuntu 22.04 and 24.04\n";
|
||||
$context .= "- Remove all docker containers, images, and networks created by the installation\n";
|
||||
$context .= "- Remove configuration files and directories\n";
|
||||
$context .= "- Clean up firewall rules if necessary\n";
|
||||
} else {
|
||||
$context .= "- The script should be compatible with Ubuntu 22.04 and 24.04\n";
|
||||
$context .= "- Use Docker containers where possible for isolation\n";
|
||||
$context .= "- Generate necessary keys and certificates automatically\n";
|
||||
$context .= "- Configure appropriate firewall rules\n";
|
||||
$context .= "- Provide clear output about installation progress\n";
|
||||
$context .= "- Handle errors gracefully\n";
|
||||
$context .= "- Include configuration validation\n";
|
||||
}
|
||||
|
||||
$context .= "\nUser requirements: $prompt\n";
|
||||
$context .= "\nReturn the response in this JSON format:\n";
|
||||
$context .= "{\n";
|
||||
if ($target === 'template') {
|
||||
$context .= ' "script": "[Interface]\\nPrivateKey = {{private_key}}...",' . "\n";
|
||||
} else {
|
||||
$context .= ' "script": "#!/bin/bash\\n# Complete script",' . "\n";
|
||||
}
|
||||
$context .= ' "suggestions": ["suggestion 1", "suggestion 2"],' . "\n";
|
||||
$context .= ' "ubuntu_compatible": true,' . "\n";
|
||||
$context .= ' "estimated_time": "5 minutes"' . "\n";
|
||||
$context .= "}\n";
|
||||
|
||||
return $context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Database methods
|
||||
*/
|
||||
|
||||
private function saveAIGeneration(array $data): int
|
||||
{
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO ai_generations (protocol_id, model_used, prompt, generated_script, suggestions, ubuntu_compatible, created_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
');
|
||||
$stmt->execute([
|
||||
$data['protocol_id'],
|
||||
$data['model_used'],
|
||||
$data['prompt'],
|
||||
$data['generated_script'],
|
||||
$data['suggestions'],
|
||||
$data['ubuntu_compatible'],
|
||||
$data['created_at']
|
||||
]);
|
||||
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
private function getAIGeneration(int $id): ?array
|
||||
{
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT ag.*, p.name as protocol_name, p.slug as protocol_slug
|
||||
FROM ai_generations ag
|
||||
LEFT JOIN protocols p ON ag.protocol_id = p.id
|
||||
WHERE ag.id = ?
|
||||
');
|
||||
$stmt->execute([$id]);
|
||||
$generation = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $generation ?: null;
|
||||
}
|
||||
|
||||
private function getAIGenerationsByProtocol(int $protocolId): array
|
||||
{
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT ag.*, p.name as protocol_name
|
||||
FROM ai_generations ag
|
||||
LEFT JOIN protocols p ON ag.protocol_id = p.id
|
||||
WHERE ag.protocol_id = ?
|
||||
ORDER BY ag.created_at DESC
|
||||
LIMIT 50
|
||||
');
|
||||
$stmt->execute([$protocolId]);
|
||||
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* LogsController
|
||||
* Manages application logs viewing and management
|
||||
* Admin-only access to application log files
|
||||
*/
|
||||
class LogsController {
|
||||
|
||||
private const LOGS_DIR = __DIR__ . '/../logs';
|
||||
private const MAX_FILE_SIZE = 10485760; // 10 MB
|
||||
private const ALLOWED_EXTENSIONS = ['log', 'txt'];
|
||||
|
||||
/**
|
||||
* List and view application logs
|
||||
* GET /admin/logs
|
||||
*/
|
||||
public function index() {
|
||||
requireAdmin();
|
||||
|
||||
$logFiles = $this->getLogFiles();
|
||||
$selectedFile = $_GET['file'] ?? null;
|
||||
$logContent = '';
|
||||
$logLines = [];
|
||||
$fileSize = 0;
|
||||
$lineCount = 0;
|
||||
|
||||
if ($selectedFile && $this->isValidLogFile($selectedFile)) {
|
||||
$filePath = self::LOGS_DIR . '/' . basename($selectedFile);
|
||||
|
||||
if (file_exists($filePath)) {
|
||||
$fileSize = filesize($filePath);
|
||||
|
||||
// Read log file (last 1000 lines or complete if small)
|
||||
$logContent = $this->readLogFile($filePath);
|
||||
$logLines = array_filter(explode("\n", $logContent));
|
||||
$lineCount = count($logLines);
|
||||
}
|
||||
}
|
||||
|
||||
View::render('settings/logs.twig', [
|
||||
'log_files' => $logFiles,
|
||||
'selected_file' => $selectedFile,
|
||||
'log_content' => $logContent,
|
||||
'log_lines' => $logLines,
|
||||
'line_count' => $lineCount,
|
||||
'file_size' => $fileSize,
|
||||
'section' => 'logs'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available log files
|
||||
*/
|
||||
private function getLogFiles(): array {
|
||||
$files = [];
|
||||
|
||||
if (!is_dir(self::LOGS_DIR)) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
$dirContents = @scandir(self::LOGS_DIR, SCANDIR_SORT_DESCENDING);
|
||||
|
||||
if ($dirContents === false) {
|
||||
return $files;
|
||||
}
|
||||
|
||||
foreach ($dirContents as $filename) {
|
||||
// Skip . and ..
|
||||
if ($filename === '.' || $filename === '..') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$filePath = self::LOGS_DIR . '/' . $filename;
|
||||
|
||||
// Only regular files
|
||||
if (!is_file($filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check extension
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$size = filesize($filePath);
|
||||
$modified = filemtime($filePath);
|
||||
|
||||
$files[] = [
|
||||
'name' => $filename,
|
||||
'path' => $filename,
|
||||
'size' => $size,
|
||||
'size_formatted' => $this->formatBytes($size),
|
||||
'modified' => $modified,
|
||||
'modified_formatted' => date('Y-m-d H:i:s', $modified),
|
||||
'readable' => is_readable($filePath)
|
||||
];
|
||||
}
|
||||
|
||||
return $files;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read log file content (last N lines)
|
||||
*/
|
||||
private function readLogFile(string $filePath, int $maxLines = 1000): string {
|
||||
if (!is_readable($filePath)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$fileSize = filesize($filePath);
|
||||
|
||||
// If file is small enough, read completely
|
||||
if ($fileSize <= self::MAX_FILE_SIZE) {
|
||||
return file_get_contents($filePath) ?: '';
|
||||
}
|
||||
|
||||
// For large files, read last N lines
|
||||
$handle = fopen($filePath, 'r');
|
||||
if ($handle === false) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Seek to end and read backwards
|
||||
fseek($handle, 0, SEEK_END);
|
||||
$lines = [];
|
||||
$lineCount = 0;
|
||||
|
||||
while ($lineCount < $maxLines && ftell($handle) > 0) {
|
||||
$chunk = '';
|
||||
$pos = ftell($handle);
|
||||
|
||||
// Read backwards in chunks
|
||||
$chunkSize = min(4096, $pos);
|
||||
fseek($handle, -$chunkSize, SEEK_CUR);
|
||||
$chunk = fread($handle, $chunkSize);
|
||||
fseek($handle, -$chunkSize, SEEK_CUR);
|
||||
|
||||
$parts = explode("\n", $chunk);
|
||||
$lines = array_merge($parts, $lines);
|
||||
$lineCount = count($lines);
|
||||
|
||||
if ($pos <= $chunkSize) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
// Take last N lines and rejoin
|
||||
$lines = array_slice($lines, -$maxLines);
|
||||
return implode("\n", $lines);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download log file
|
||||
* GET /admin/logs/download?file=filename
|
||||
*/
|
||||
public function download() {
|
||||
requireAdmin();
|
||||
|
||||
$file = $_GET['file'] ?? null;
|
||||
|
||||
if (!$file || !$this->isValidLogFile($file)) {
|
||||
http_response_code(400);
|
||||
echo 'Invalid file';
|
||||
return;
|
||||
}
|
||||
|
||||
$filePath = self::LOGS_DIR . '/' . basename($file);
|
||||
|
||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||
http_response_code(404);
|
||||
echo 'File not found';
|
||||
return;
|
||||
}
|
||||
|
||||
header('Content-Type: text/plain; charset=utf-8');
|
||||
header('Content-Disposition: attachment; filename="' . basename($file) . '"');
|
||||
header('Content-Length: ' . filesize($filePath));
|
||||
|
||||
readfile($filePath);
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete log file
|
||||
* POST /admin/logs/delete
|
||||
*/
|
||||
public function delete() {
|
||||
requireAdmin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$file = $_POST['file'] ?? null;
|
||||
|
||||
if (!$file || !$this->isValidLogFile($file)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid file']);
|
||||
return;
|
||||
}
|
||||
|
||||
$filePath = self::LOGS_DIR . '/' . basename($file);
|
||||
|
||||
if (!file_exists($filePath)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'File not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (@unlink($filePath)) {
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Log file deleted successfully',
|
||||
'redirect' => '/admin/logs'
|
||||
]);
|
||||
} else {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Failed to delete file']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all log files
|
||||
* POST /admin/logs/clear-all
|
||||
*/
|
||||
public function clearAll() {
|
||||
requireAdmin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$logFiles = $this->getLogFiles();
|
||||
$deleted = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($logFiles as $file) {
|
||||
$filePath = self::LOGS_DIR . '/' . $file['path'];
|
||||
if (@unlink($filePath)) {
|
||||
$deleted++;
|
||||
} else {
|
||||
$failed++;
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'deleted' => $deleted,
|
||||
'failed' => $failed,
|
||||
'message' => "Deleted $deleted log files" . ($failed > 0 ? ", failed to delete $failed" : ''),
|
||||
'redirect' => '/admin/logs'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search logs
|
||||
* POST /admin/logs/search
|
||||
*/
|
||||
public function search() {
|
||||
requireAdmin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$query = $_POST['query'] ?? '';
|
||||
$file = $_POST['file'] ?? null;
|
||||
$caseSensitive = isset($_POST['case_sensitive']) && $_POST['case_sensitive'] === 'on';
|
||||
|
||||
if (empty($query) || strlen($query) < 2) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Search query too short']);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$file || !$this->isValidLogFile($file)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid file']);
|
||||
return;
|
||||
}
|
||||
|
||||
$filePath = self::LOGS_DIR . '/' . basename($file);
|
||||
|
||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'File not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
$content = file_get_contents($filePath);
|
||||
if ($content === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Failed to read file']);
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = explode("\n", $content);
|
||||
$results = [];
|
||||
$flags = $caseSensitive ? 0 : PREG_GREP_INVERT;
|
||||
|
||||
foreach ($lines as $lineNum => $line) {
|
||||
if (empty($line)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Case-sensitive or case-insensitive search
|
||||
$matches = $caseSensitive
|
||||
? (strpos($line, $query) !== false)
|
||||
: (stripos($line, $query) !== false);
|
||||
|
||||
if ($matches) {
|
||||
$results[] = [
|
||||
'line' => $lineNum + 1,
|
||||
'content' => $line
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'query' => $query,
|
||||
'results_count' => count($results),
|
||||
'results' => array_slice($results, 0, 100) // Limit to 100 results
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get log statistics
|
||||
* POST /admin/logs/stats
|
||||
*/
|
||||
public function stats() {
|
||||
requireAdmin();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
$file = $_POST['file'] ?? null;
|
||||
|
||||
if (!$file || !$this->isValidLogFile($file)) {
|
||||
http_response_code(400);
|
||||
echo json_encode(['success' => false, 'message' => 'Invalid file']);
|
||||
return;
|
||||
}
|
||||
|
||||
$filePath = self::LOGS_DIR . '/' . basename($file);
|
||||
|
||||
if (!file_exists($filePath) || !is_readable($filePath)) {
|
||||
http_response_code(404);
|
||||
echo json_encode(['success' => false, 'message' => 'File not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
$content = file_get_contents($filePath);
|
||||
if ($content === false) {
|
||||
http_response_code(500);
|
||||
echo json_encode(['success' => false, 'message' => 'Failed to read file']);
|
||||
return;
|
||||
}
|
||||
|
||||
$lines = array_filter(explode("\n", $content));
|
||||
$errorCount = count(preg_grep('/error|exception|fatal|fail/i', $lines));
|
||||
$warningCount = count(preg_grep('/warning|warn/i', $lines));
|
||||
$successCount = count(preg_grep('/success|completed|ok/i', $lines));
|
||||
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'total_lines' => count($lines),
|
||||
'errors' => $errorCount,
|
||||
'warnings' => $warningCount,
|
||||
'success' => $successCount,
|
||||
'file_size' => filesize($filePath),
|
||||
'file_size_formatted' => $this->formatBytes(filesize($filePath)),
|
||||
'last_modified' => date('Y-m-d H:i:s', filemtime($filePath))
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate log file name
|
||||
*/
|
||||
private function isValidLogFile(string $filename): bool {
|
||||
// Prevent directory traversal
|
||||
if (strpos($filename, '/') !== false || strpos($filename, '\\') !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (strpos($filename, '..') !== false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check extension
|
||||
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||
if (!in_array($ext, self::ALLOWED_EXTENSIONS, true)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format bytes to human readable
|
||||
*/
|
||||
private function formatBytes(int $bytes, int $precision = 2): string {
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
|
||||
return round($bytes, $precision) . ' ' . $units[$i];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,955 @@
|
||||
<?php
|
||||
|
||||
class ProtocolManagementController
|
||||
{
|
||||
|
||||
/**
|
||||
* Display protocols list and management interface
|
||||
*/
|
||||
public function index(): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$protocols = $this->getAllProtocols();
|
||||
$selectedId = isset($_GET['id']) ? (int) $_GET['id'] : null;
|
||||
$isNew = isset($_GET['new']);
|
||||
$isTemplate = isset($_GET['template']);
|
||||
|
||||
$editing = null;
|
||||
if (!$isNew && $selectedId) {
|
||||
$editing = $this->getProtocolById($selectedId);
|
||||
}
|
||||
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare("SELECT api_key FROM api_keys WHERE service_name = 'openrouter' AND is_active = 1 LIMIT 1");
|
||||
$stmt->execute();
|
||||
$openrouterKey = $stmt->fetchColumn() ?: '';
|
||||
|
||||
if ($isTemplate && $editing) {
|
||||
View::render('settings/protocol_template_editor.twig', [
|
||||
'protocol' => $editing,
|
||||
'success' => $_SESSION['protocol_success'] ?? null,
|
||||
'error' => $_SESSION['protocol_error'] ?? null,
|
||||
'openrouter_key' => $openrouterKey,
|
||||
]);
|
||||
} elseif ($editing || $isNew) {
|
||||
View::render('settings/protocol_form.twig', [
|
||||
'editing' => $editing,
|
||||
'success' => $_SESSION['protocol_success'] ?? null,
|
||||
'error' => $_SESSION['protocol_error'] ?? null,
|
||||
'openrouter_key' => $openrouterKey,
|
||||
]);
|
||||
} else {
|
||||
View::render('settings/protocols_management.twig', [
|
||||
'protocols' => $protocols,
|
||||
'success' => $_SESSION['protocol_success'] ?? null,
|
||||
'error' => $_SESSION['protocol_error'] ?? null,
|
||||
'openrouter_key' => $openrouterKey,
|
||||
]);
|
||||
}
|
||||
|
||||
unset($_SESSION['protocol_success'], $_SESSION['protocol_error']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolManagementController::index: " . $e->getMessage());
|
||||
$_SESSION['protocol_error'] = 'Failed to load protocols: ' . $e->getMessage();
|
||||
redirect('/settings/protocols-management');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create or update protocol
|
||||
*/
|
||||
public function save(): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$id = isset($_POST['id']) && $_POST['id'] !== '' ? (int) $_POST['id'] : null;
|
||||
$name = trim($_POST['name'] ?? '');
|
||||
$slug = trim($_POST['slug'] ?? '');
|
||||
$description = trim($_POST['description'] ?? '');
|
||||
$installScript = trim($_POST['install_script'] ?? '');
|
||||
$passwordCommand = trim($_POST['password_command'] ?? '');
|
||||
$uninstallScript = trim($_POST['uninstall_script'] ?? '');
|
||||
$outputTemplate = trim($_POST['output_template'] ?? '');
|
||||
$qrCodeTemplate = trim($_POST['qr_code_template'] ?? '');
|
||||
$qrCodeFormat = trim($_POST['qr_code_format'] ?? 'amnezia_compressed');
|
||||
$ubuntuCompatible = isset($_POST['ubuntu_compatible']) ? 1 : 0;
|
||||
$showTextContent = isset($_POST['show_text_content']) ? 1 : 0;
|
||||
$isActive = isset($_POST['is_active']) ? 1 : 0;
|
||||
|
||||
// Validation
|
||||
if ($name === '' || $slug === '') {
|
||||
throw new Exception('Name and slug are required');
|
||||
}
|
||||
|
||||
if (!preg_match('/^[a-z0-9_-]+$/i', $slug)) {
|
||||
throw new Exception('Slug may contain only letters, numbers, dashes, and underscores');
|
||||
}
|
||||
|
||||
// Check if slug is unique (for new protocols or when updating slug)
|
||||
if ($this->isSlugExists($slug, $id)) {
|
||||
throw new Exception('Protocol with this slug already exists');
|
||||
}
|
||||
|
||||
$protocolData = [
|
||||
'name' => $name,
|
||||
'slug' => $slug,
|
||||
'description' => $description,
|
||||
'install_script' => $installScript,
|
||||
'output_template' => $outputTemplate,
|
||||
'qr_code_template' => $qrCodeTemplate,
|
||||
'qr_code_format' => $qrCodeFormat,
|
||||
'password_command' => $passwordCommand,
|
||||
'uninstall_script' => $uninstallScript,
|
||||
'ubuntu_compatible' => $ubuntuCompatible,
|
||||
'show_text_content' => $showTextContent,
|
||||
'is_active' => $isActive,
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
if ($id) {
|
||||
// Update existing protocol
|
||||
$this->updateProtocol($id, $protocolData);
|
||||
$savedId = $id;
|
||||
$_SESSION['protocol_success'] = 'Protocol updated successfully';
|
||||
} else {
|
||||
// Create new protocol
|
||||
$protocolData['created_at'] = date('Y-m-d H:i:s');
|
||||
$savedId = $this->createProtocol($protocolData);
|
||||
$_SESSION['protocol_success'] = 'Protocol created successfully';
|
||||
}
|
||||
|
||||
redirect('/settings/protocols-management?id=' . $savedId);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$_SESSION['protocol_error'] = $e->getMessage();
|
||||
$id = isset($_POST['id']) ? (int) $_POST['id'] : null;
|
||||
redirect('/settings/protocols-management' . ($id ? '?id=' . $id : '?new=1'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete protocol
|
||||
*/
|
||||
public function delete(int $id): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$protocol = $this->getProtocolById($id);
|
||||
if (!$protocol) {
|
||||
throw new Exception('Protocol not found');
|
||||
}
|
||||
|
||||
// Check if protocol is used by any servers
|
||||
if ($this->isProtocolUsed($id)) {
|
||||
throw new Exception('Cannot delete protocol that is currently used by servers');
|
||||
}
|
||||
|
||||
$this->deleteProtocol($id);
|
||||
$_SESSION['protocol_success'] = 'Protocol deleted successfully';
|
||||
|
||||
} catch (Exception $e) {
|
||||
$_SESSION['protocol_error'] = $e->getMessage();
|
||||
}
|
||||
|
||||
redirect('/settings/protocols-management');
|
||||
}
|
||||
|
||||
/**
|
||||
* API endpoint: Get all protocols (JSON)
|
||||
*/
|
||||
public function apiGetProtocols(): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$protocols = $this->getAllProtocols();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $protocols
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API endpoint: Get single protocol (JSON)
|
||||
*/
|
||||
public function apiGetProtocol(int $id): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$protocol = $this->getProtocolById($id);
|
||||
if (!$protocol) {
|
||||
throw new Exception('Protocol not found');
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $protocol
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(404);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API endpoint: Create protocol (JSON)
|
||||
*/
|
||||
public function apiCreateProtocol(): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$input) {
|
||||
throw new Exception('Invalid JSON input');
|
||||
}
|
||||
|
||||
// Validate required fields
|
||||
$requiredFields = ['name', 'slug'];
|
||||
foreach ($requiredFields as $field) {
|
||||
if (empty($input[$field])) {
|
||||
throw new Exception("Field '$field' is required");
|
||||
}
|
||||
}
|
||||
|
||||
// Validate slug format
|
||||
if (!preg_match('/^[a-z0-9_-]+$/i', $input['slug'])) {
|
||||
throw new Exception('Slug may contain only letters, numbers, dashes, and underscores');
|
||||
}
|
||||
|
||||
// Check if slug exists
|
||||
if ($this->isSlugExists($input['slug'])) {
|
||||
throw new Exception('Protocol with this slug already exists');
|
||||
}
|
||||
|
||||
$protocolData = [
|
||||
'name' => trim($input['name']),
|
||||
'slug' => trim($input['slug']),
|
||||
'description' => trim($input['description'] ?? ''),
|
||||
'install_script' => trim($input['install_script'] ?? ''),
|
||||
'output_template' => trim($input['output_template'] ?? ''),
|
||||
'qr_code_template' => trim($input['qr_code_template'] ?? ''),
|
||||
'qr_code_format' => trim($input['qr_code_format'] ?? 'amnezia_compressed'),
|
||||
'ubuntu_compatible' => (bool) ($input['ubuntu_compatible'] ?? false),
|
||||
'show_text_content' => (bool) ($input['show_text_content'] ?? false),
|
||||
'is_active' => (bool) ($input['is_active'] ?? true),
|
||||
'created_at' => date('Y-m-d H:i:s'),
|
||||
'updated_at' => date('Y-m-d H:i:s')
|
||||
];
|
||||
|
||||
$id = $this->createProtocol($protocolData);
|
||||
$protocol = $this->getProtocolById($id);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $protocol,
|
||||
'message' => 'Protocol created successfully'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API endpoint: Update protocol (JSON)
|
||||
*/
|
||||
public function apiUpdateProtocol(int $id): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$protocol = $this->getProtocolById($id);
|
||||
if (!$protocol) {
|
||||
throw new Exception('Protocol not found');
|
||||
}
|
||||
|
||||
$input = json_decode(file_get_contents('php://input'), true);
|
||||
if (!$input) {
|
||||
throw new Exception('Invalid JSON input');
|
||||
}
|
||||
|
||||
$protocolData = [];
|
||||
$allowedFields = ['name', 'slug', 'description', 'install_script', 'output_template', 'qr_code_template', 'qr_code_format', 'ubuntu_compatible', 'show_text_content', 'is_active'];
|
||||
|
||||
foreach ($allowedFields as $field) {
|
||||
if (isset($input[$field])) {
|
||||
if ($field === 'ubuntu_compatible' || $field === 'is_active') {
|
||||
$protocolData[$field] = (bool) $input[$field];
|
||||
} elseif ($field === 'slug') {
|
||||
$slug = trim($input[$field]);
|
||||
if (!preg_match('/^[a-z0-9_-]+$/i', $slug)) {
|
||||
throw new Exception('Slug may contain only letters, numbers, dashes, and underscores');
|
||||
}
|
||||
if ($this->isSlugExists($slug, $id)) {
|
||||
throw new Exception('Protocol with this slug already exists');
|
||||
}
|
||||
$protocolData[$field] = $slug;
|
||||
} else {
|
||||
$protocolData[$field] = trim($input[$field]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($protocolData)) {
|
||||
$protocolData['updated_at'] = date('Y-m-d H:i:s');
|
||||
$this->updateProtocol($id, $protocolData);
|
||||
$protocol = $this->getProtocolById($id);
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'data' => $protocol,
|
||||
'message' => 'Protocol updated successfully'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* API endpoint: Delete protocol (JSON)
|
||||
*/
|
||||
public function apiDeleteProtocol(int $id): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
try {
|
||||
$protocol = $this->getProtocolById($id);
|
||||
if (!$protocol) {
|
||||
throw new Exception('Protocol not found');
|
||||
}
|
||||
|
||||
if ($this->isProtocolUsed($id)) {
|
||||
throw new Exception('Cannot delete protocol that is currently used by servers');
|
||||
}
|
||||
|
||||
$this->deleteProtocol($id);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Protocol deleted successfully'
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function apiTestInstallProtocol(int $id): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
// Suppress all errors and warnings to prevent HTML output before JSON
|
||||
@ini_set('display_errors', '0');
|
||||
error_reporting(0);
|
||||
|
||||
// Clean any previous output
|
||||
if (ob_get_level())
|
||||
ob_end_clean();
|
||||
ob_start();
|
||||
|
||||
header('Content-Type: application/json');
|
||||
|
||||
try {
|
||||
$protocol = $this->getProtocolById($id);
|
||||
if (!$protocol) {
|
||||
throw new Exception('Protocol not found');
|
||||
}
|
||||
|
||||
$script = trim($protocol['install_script'] ?? '');
|
||||
if ($script === '') {
|
||||
throw new Exception('Install script is empty');
|
||||
}
|
||||
|
||||
$container = 'proto-test-' . $id;
|
||||
|
||||
$this->runHostCommand('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
|
||||
$run = $this->runHostCommandChecked('docker run --privileged -d -v /var/run/docker.sock:/var/run/docker.sock --name ' . escapeshellarg($container) . ' ubuntu:22.04 sleep infinity');
|
||||
if ($run['rc'] !== 0) {
|
||||
throw new Exception('Docker not accessible: ' . trim($run['out']));
|
||||
}
|
||||
|
||||
$cliPath = '/usr/local/bin/docker';
|
||||
$try1 = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '"');
|
||||
if ($try1['rc'] !== 0 || $try1['out'] === '') {
|
||||
$cliPath = '/usr/bin/docker';
|
||||
$try2 = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '"');
|
||||
if ($try2['rc'] !== 0 || $try2['out'] === '') {
|
||||
throw new Exception('Failed to read docker CLI from docker:24-dind image');
|
||||
}
|
||||
}
|
||||
$cp = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '" | docker exec -i ' . escapeshellarg($container) . ' sh -lc "cat > /usr/local/bin/docker && chmod +x /usr/local/bin/docker"');
|
||||
if ($cp['rc'] !== 0) {
|
||||
throw new Exception('Failed to provide docker CLI to test container: ' . trim($cp['out']));
|
||||
}
|
||||
$this->execInContainerChecked($container, 'chmod +x /usr/local/bin/docker');
|
||||
$ver = $this->execInContainerChecked($container, 'docker --version');
|
||||
if ($ver['rc'] !== 0) {
|
||||
throw new Exception('Docker CLI not available in test container');
|
||||
}
|
||||
|
||||
$prelude = <<<'SH'
|
||||
set -euo pipefail
|
||||
set -x
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
wg() {
|
||||
if docker ps --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then
|
||||
docker exec -i "$CONTAINER_NAME" wg "$@"
|
||||
else
|
||||
docker pull -q amneziavpn/amnezia-wg:latest >/dev/null 2>&1 || true
|
||||
docker run --rm -i --privileged --cap-add=NET_ADMIN amneziavpn/amnezia-wg:latest wg "$@"
|
||||
fi
|
||||
}
|
||||
SH;
|
||||
$wrapped = $prelude . "\n" . $script;
|
||||
$runScript = $this->execInContainerChecked($container, $wrapped);
|
||||
if ($runScript['rc'] !== 0) {
|
||||
throw new Exception("Install script failed: " . trim($runScript['out']));
|
||||
}
|
||||
$output = $runScript['out'];
|
||||
|
||||
$extracted = $this->extractValuesFromOutput($output);
|
||||
|
||||
$variables = $this->getProtocolVariables($id);
|
||||
foreach ($extracted as $k => $v) {
|
||||
if (array_key_exists($k, $variables)) {
|
||||
$variables[$k] = $v;
|
||||
}
|
||||
}
|
||||
|
||||
$preview = ProtocolService::generateProtocolOutput($protocol, $variables);
|
||||
|
||||
// Cleanup test containers: proto-test and AWG if created
|
||||
$this->runHostCommand('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
|
||||
$this->runHostCommand('docker rm -f amnezia-awg >/dev/null 2>&1 || true');
|
||||
|
||||
// Clean buffer and output JSON
|
||||
if (ob_get_level())
|
||||
ob_end_clean();
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'logs' => $output,
|
||||
'extracted' => $extracted,
|
||||
'preview' => $preview
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
// Clean buffer and output error JSON
|
||||
if (ob_get_level())
|
||||
ob_end_clean();
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'error' => $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
public function apiTestInstallProtocolStream(int $id): void
|
||||
{
|
||||
requireAdmin();
|
||||
|
||||
// Suppress all errors and warnings to prevent HTML output
|
||||
@ini_set('display_errors', '0');
|
||||
error_reporting(0);
|
||||
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('Connection: keep-alive');
|
||||
@ini_set('output_buffering', 'off');
|
||||
@ini_set('zlib.output_compression', '0');
|
||||
@ob_implicit_flush(true);
|
||||
// CRITICAL: Use ob_end_clean() instead of ob_end_flush() to DISCARD any
|
||||
// buffered warnings/errors that would corrupt the SSE stream
|
||||
while (ob_get_level()) {
|
||||
@ob_end_clean();
|
||||
}
|
||||
|
||||
$send = function (array $data) {
|
||||
echo 'data: ' . json_encode($data) . "\n\n";
|
||||
@flush();
|
||||
};
|
||||
|
||||
try {
|
||||
$protocol = $this->getProtocolById($id);
|
||||
if (!$protocol) {
|
||||
$send(['type' => 'error', 'error' => 'Protocol not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
$script = trim($protocol['install_script'] ?? '');
|
||||
if ($script === '') {
|
||||
$send(['type' => 'error', 'error' => 'Install script is empty']);
|
||||
return;
|
||||
}
|
||||
|
||||
$container = 'proto-test-' . $id;
|
||||
$send(['type' => 'start']);
|
||||
|
||||
$send(['type' => 'cmd', 'cmd' => 'docker rm -f ' . $container]);
|
||||
$rm = $this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
|
||||
$send(['type' => 'cmd_done', 'rc' => $rm['rc']]);
|
||||
|
||||
$cmdRun = 'docker run --network host --privileged -d --name ' . $container . ' ubuntu:22.04 sleep infinity';
|
||||
$send(['type' => 'cmd', 'cmd' => $cmdRun]);
|
||||
$run = $this->runHostCommandChecked('docker run --network host --privileged -d -v /var/run/docker.sock:/var/run/docker.sock -v /opt/amnezia:/opt/amnezia -v /etc/aivpn:/etc/aivpn -v /etc/amnezia:/etc/amnezia --name ' . escapeshellarg($container) . ' ubuntu:22.04 sleep infinity');
|
||||
if ($run['rc'] !== 0) {
|
||||
$send(['type' => 'error', 'error' => 'Docker not accessible: ' . trim($run['out'])]);
|
||||
return;
|
||||
}
|
||||
$send(['type' => 'cmd_done', 'rc' => 0]);
|
||||
|
||||
$send(['type' => 'cmd', 'cmd' => 'provide docker cli']);
|
||||
$cliPath = '/usr/local/bin/docker';
|
||||
$send(['type' => 'cmd', 'cmd' => 'provide docker cli from docker:24-dind image']);
|
||||
$try1 = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '"');
|
||||
if ($try1['rc'] !== 0 || $try1['out'] === '') {
|
||||
$cliPath = '/usr/bin/docker';
|
||||
}
|
||||
$cp = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '" | docker exec -i ' . escapeshellarg($container) . ' sh -lc "cat > /usr/local/bin/docker && chmod +x /usr/local/bin/docker"');
|
||||
if ($cp['rc'] !== 0) {
|
||||
$send(['type' => 'error', 'error' => 'Failed to provide docker CLI to test container: ' . trim($cp['out'])]);
|
||||
return;
|
||||
}
|
||||
$this->execInContainerChecked($container, 'chmod +x /usr/local/bin/docker');
|
||||
$ver = $this->execInContainerChecked($container, 'docker --version');
|
||||
$send(['type' => 'out', 'line' => $ver['out']]);
|
||||
if ($ver['rc'] !== 0) {
|
||||
$send(['type' => 'error', 'error' => 'Docker CLI not available in test container']);
|
||||
return;
|
||||
}
|
||||
|
||||
$prelude = <<<'SH'
|
||||
set -euo pipefail
|
||||
set -x
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
wg() {
|
||||
if docker ps --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then
|
||||
docker exec -i "$CONTAINER_NAME" wg "$@"
|
||||
else
|
||||
docker pull -q amneziavpn/amnezia-wg:latest >/dev/null 2>&1 || true
|
||||
docker run --rm -i --privileged --cap-add=NET_ADMIN amneziavpn/amnezia-wg:latest wg "$@"
|
||||
fi
|
||||
}
|
||||
SH;
|
||||
$wrapped = $prelude . "\n" . $script;
|
||||
$send(['type' => 'cmd', 'cmd' => 'install_script']);
|
||||
$runScript = $this->execInContainerChecked($container, $wrapped);
|
||||
$outLines = explode("\n", trim($runScript['out']));
|
||||
foreach ($outLines as $line) {
|
||||
if ($line !== '')
|
||||
$send(['type' => 'out', 'line' => $line]);
|
||||
}
|
||||
if ($runScript['rc'] !== 0) {
|
||||
$send(['type' => 'error', 'error' => 'Install script failed: ' . trim($runScript['out'])]);
|
||||
$this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
|
||||
return;
|
||||
}
|
||||
$send(['type' => 'cmd_done', 'rc' => 0]);
|
||||
|
||||
$extracted = $this->extractValuesFromOutput($runScript['out']);
|
||||
$variables = $this->getProtocolVariables($id);
|
||||
// Merge all extracted variables (not just existing ones)
|
||||
$variables = array_merge($variables, $extracted);
|
||||
$preview = ProtocolService::generateProtocolOutput($protocol, $variables);
|
||||
$send(['type' => 'preview', 'preview' => $preview]);
|
||||
|
||||
$this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
|
||||
$this->runHostCommandChecked('docker rm -f amnezia-awg >/dev/null 2>&1 || true');
|
||||
$send(['type' => 'done']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo 'data: ' . json_encode(['type' => 'error', 'error' => $e->getMessage()]) . "\n\n";
|
||||
@flush();
|
||||
}
|
||||
}
|
||||
|
||||
public function apiTestUninstallProtocolStream(int $id): void
|
||||
{
|
||||
requireAdmin();
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
header('Connection: keep-alive');
|
||||
@ini_set('output_buffering', 'off');
|
||||
@ini_set('zlib.output_compression', '0');
|
||||
@ob_implicit_flush(true);
|
||||
@ob_end_flush();
|
||||
|
||||
$send = function (array $data) {
|
||||
echo 'data: ' . json_encode($data) . "\n\n";
|
||||
@flush();
|
||||
};
|
||||
|
||||
try {
|
||||
$protocol = $this->getProtocolById($id);
|
||||
if (!$protocol) {
|
||||
$send(['type' => 'error', 'error' => 'Protocol not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
$installScript = trim($protocol['install_script'] ?? '');
|
||||
$uninstallScript = trim($protocol['uninstall_script'] ?? '');
|
||||
|
||||
if ($installScript === '') {
|
||||
$send(['type' => 'error', 'error' => 'Install script is empty (required for setup)']);
|
||||
return;
|
||||
}
|
||||
if ($uninstallScript === '') {
|
||||
$send(['type' => 'error', 'error' => 'Uninstall script is empty']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Normalize uninstall script if needed? For now assume it's fine.
|
||||
|
||||
$container = 'proto-test-uninstall-' . $id;
|
||||
$send(['type' => 'start']);
|
||||
|
||||
// 1. Setup container
|
||||
$send(['type' => 'cmd', 'cmd' => 'Setting up test environment...']);
|
||||
$this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
|
||||
|
||||
$run = $this->runHostCommandChecked('docker run --privileged -d -v /var/run/docker.sock:/var/run/docker.sock --name ' . escapeshellarg($container) . ' ubuntu:22.04 sleep infinity');
|
||||
if ($run['rc'] !== 0) {
|
||||
$send(['type' => 'error', 'error' => 'Docker not accessible: ' . trim($run['out'])]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Provide docker CLI
|
||||
$cliPath = '/usr/local/bin/docker';
|
||||
$try1 = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '"');
|
||||
if ($try1['rc'] !== 0 || $try1['out'] === '') {
|
||||
$cliPath = '/usr/bin/docker';
|
||||
}
|
||||
$cp = $this->runHostCommandChecked('docker run --rm docker:24-dind sh -lc "cat ' . $cliPath . '" | docker exec -i ' . escapeshellarg($container) . ' sh -lc "cat > /usr/local/bin/docker && chmod +x /usr/local/bin/docker"');
|
||||
if ($cp['rc'] !== 0) {
|
||||
$send(['type' => 'error', 'error' => 'Failed to provide docker CLI']);
|
||||
return;
|
||||
}
|
||||
$this->execInContainerChecked($container, 'chmod +x /usr/local/bin/docker');
|
||||
|
||||
$prelude = <<<'SH'
|
||||
set -euo pipefail
|
||||
set -x
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
wg() {
|
||||
if docker ps --format '{{.Names}}' | grep -qx "$CONTAINER_NAME"; then
|
||||
docker exec -i "$CONTAINER_NAME" wg "$@"
|
||||
else
|
||||
docker pull -q amneziavpn/amnezia-wg:latest >/dev/null 2>&1 || true
|
||||
docker run --rm -i --privileged --cap-add=NET_ADMIN amneziavpn/amnezia-wg:latest wg "$@"
|
||||
fi
|
||||
}
|
||||
SH;
|
||||
|
||||
// 2. Run Install Script
|
||||
$send(['type' => 'cmd', 'cmd' => 'Running installation script...']);
|
||||
$wrappedInstall = $prelude . "\n" . $installScript;
|
||||
$runInstall = $this->execInContainerChecked($container, $wrappedInstall);
|
||||
|
||||
if ($runInstall['rc'] !== 0) {
|
||||
$send(['type' => 'error', 'error' => 'Setup (install) failed: ' . trim($runInstall['out'])]);
|
||||
$this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
|
||||
return;
|
||||
}
|
||||
$send(['type' => 'out', 'line' => 'Installation successful. Now running uninstall...']);
|
||||
|
||||
// 3. Run Uninstall Script
|
||||
$send(['type' => 'cmd', 'cmd' => 'Running uninstallation script...']);
|
||||
$wrappedUninstall = $prelude . "\n" . $uninstallScript;
|
||||
$runUninstall = $this->execInContainerChecked($container, $wrappedUninstall);
|
||||
|
||||
$outLines = explode("\n", trim($runUninstall['out']));
|
||||
foreach ($outLines as $line) {
|
||||
if ($line !== '')
|
||||
$send(['type' => 'out', 'line' => $line]);
|
||||
}
|
||||
|
||||
if ($runUninstall['rc'] !== 0) {
|
||||
$send(['type' => 'error', 'error' => 'Uninstall script failed: ' . trim($runUninstall['out'])]);
|
||||
} else {
|
||||
$send(['type' => 'cmd_done', 'rc' => 0]);
|
||||
$send(['type' => 'out', 'line' => 'Uninstallation completed successfully.']);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
$this->runHostCommandChecked('docker rm -f ' . escapeshellarg($container) . ' >/dev/null 2>&1 || true');
|
||||
$this->runHostCommandChecked('docker rm -f amnezia-awg >/dev/null 2>&1 || true'); // Cleanup potential leftover
|
||||
$send(['type' => 'done']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo 'data: ' . json_encode(['type' => 'error', 'error' => $e->getMessage()]) . "\n\n";
|
||||
@flush();
|
||||
}
|
||||
}
|
||||
|
||||
private function runHostCommand(string $cmd): void
|
||||
{
|
||||
$out = shell_exec($cmd);
|
||||
}
|
||||
|
||||
private function runHostCommandChecked(string $cmd): array
|
||||
{
|
||||
$lines = [];
|
||||
$rc = 0;
|
||||
exec($cmd . ' 2>&1', $lines, $rc);
|
||||
return ['out' => implode("\n", $lines), 'rc' => $rc];
|
||||
}
|
||||
|
||||
private function execInContainer(string $container, string $cmd): string
|
||||
{
|
||||
$full = 'docker exec ' . escapeshellarg($container) . ' bash -lc ' . escapeshellarg($cmd);
|
||||
$out = shell_exec($full . ' 2>&1');
|
||||
return $out ?? '';
|
||||
}
|
||||
|
||||
private function execInContainerChecked(string $container, string $cmd): array
|
||||
{
|
||||
$lines = [];
|
||||
$rc = 0;
|
||||
$full = 'docker exec ' . escapeshellarg($container) . ' bash -lc ' . escapeshellarg($cmd);
|
||||
exec($full . ' 2>&1', $lines, $rc);
|
||||
return ['out' => implode("\n", $lines), 'rc' => $rc];
|
||||
}
|
||||
|
||||
private function normalizeAwgInstallScript(string $script): string
|
||||
{
|
||||
// Script in DB already has #!/bin/bash and set -euo pipefail
|
||||
// Just return it as-is since variables are already defined in the script
|
||||
return $script;
|
||||
}
|
||||
|
||||
private function extractValuesFromOutput(string $output): array
|
||||
{
|
||||
$res = [];
|
||||
|
||||
// Extract port
|
||||
if (preg_match('/Port:\s*(\d+)/i', $output, $m)) {
|
||||
$res['server_port'] = $m[1];
|
||||
}
|
||||
|
||||
// Extract server public key
|
||||
if (preg_match('/Server Public Key:\s*([A-Za-z0-9+\/=]+)/i', $output, $m)) {
|
||||
$res['server_public_key'] = $m[1];
|
||||
}
|
||||
|
||||
// Extract preshared key
|
||||
if (preg_match('/PresharedKey\s*=\s*([A-Za-z0-9+\/=]+)/i', $output, $m)) {
|
||||
$res['preshared_key'] = $m[1];
|
||||
}
|
||||
|
||||
// Extract subnet (format: "Subnet: 10.8.1.1/24")
|
||||
if (preg_match('/Subnet:\s*([0-9.]+)\/(\d+)/i', $output, $m)) {
|
||||
$res['subnet_ip'] = $m[1];
|
||||
$res['subnet_cidr'] = $m[2];
|
||||
}
|
||||
|
||||
// Extract password (for non-WireGuard protocols)
|
||||
if (preg_match('/Password:\s*(\S+)/i', $output, $m)) {
|
||||
$res['password'] = $m[1];
|
||||
}
|
||||
|
||||
// Extract method (for protocols like Shadowsocks)
|
||||
if (preg_match('/Method:\s*(\S+)/i', $output, $m)) {
|
||||
$res['method'] = $m[1];
|
||||
}
|
||||
|
||||
// Extract client ID (for protocols that use it)
|
||||
if (preg_match('/ClientID\s*:\s*([0-9a-fA-F-]+)/i', $output, $m)) {
|
||||
$res['client_id'] = $m[1];
|
||||
}
|
||||
|
||||
// Extract secret (for MTProxy and similar protocols)
|
||||
if (preg_match('/Secret:\s*([a-fA-F0-9]+)/i', $output, $m)) {
|
||||
$res['secret'] = $m[1];
|
||||
}
|
||||
|
||||
// Extract server host/IP
|
||||
if (preg_match('/Server\s*Host:\s*(\S+)/i', $output, $m)) {
|
||||
$res['server_host'] = trim($m[1], "'\"");
|
||||
}
|
||||
|
||||
// Generic variable extraction (Variable: KEY=VALUE)
|
||||
if (preg_match_all('/Variable:\s*([a-zA-Z0-9_]+)=(.*)/', $output, $matches, PREG_SET_ORDER)) {
|
||||
foreach ($matches as $m) {
|
||||
$key = trim($m[1]);
|
||||
$val = trim($m[2]);
|
||||
// Remove surrounding quotes if present
|
||||
$val = trim($val, "'\"");
|
||||
$res[$key] = $val;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: parse any remaining "Key: Value" lines not yet captured
|
||||
// This catches protocol-specific variables like custom fields
|
||||
$lines = preg_split('/\r?\n/', $output);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '') continue;
|
||||
// Skip set -x trace lines (start with +)
|
||||
if (preg_match('/^\+/', $line)) continue;
|
||||
if (preg_match('/^([A-Za-z][A-Za-z0-9 _-]*?)\s*:\s*(.+)$/', $line, $m)) {
|
||||
$rawKey = trim($m[1]);
|
||||
$rawVal = trim($m[2], " \t'\"");
|
||||
$normalized = strtolower(preg_replace('/\s+/', '_', $rawKey));
|
||||
// Don't overwrite already extracted keys
|
||||
if (!array_key_exists($normalized, $res)) {
|
||||
$res[$normalized] = $rawVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
private function getProtocolVariables(int $protocolId): array
|
||||
{
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('SELECT variable_name, COALESCE(default_value, "") as val FROM protocol_variables WHERE protocol_id = ?');
|
||||
$stmt->execute([$protocolId]);
|
||||
$vars = [];
|
||||
foreach ($stmt->fetchAll(PDO::FETCH_ASSOC) as $row) {
|
||||
$vars[$row['variable_name']] = $row['val'] ?? '';
|
||||
}
|
||||
return $vars;
|
||||
}
|
||||
/**
|
||||
* Database methods
|
||||
*/
|
||||
|
||||
private function getAllProtocols(): array
|
||||
{
|
||||
return ProtocolService::getAllProtocolsWithStats();
|
||||
}
|
||||
|
||||
private function getProtocolById(int $id): ?array
|
||||
{
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT p.*,
|
||||
COUNT(DISTINCT sp.server_id) as server_count
|
||||
FROM protocols p
|
||||
LEFT JOIN server_protocols sp ON p.id = sp.protocol_id
|
||||
WHERE p.id = ?
|
||||
GROUP BY p.id
|
||||
');
|
||||
$stmt->execute([$id]);
|
||||
$protocol = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
return $protocol ?: null;
|
||||
}
|
||||
|
||||
private function createProtocol(array $data): int
|
||||
{
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('
|
||||
INSERT INTO protocols (name, slug, description, install_script, uninstall_script, password_command, output_template, ubuntu_compatible, is_active, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
');
|
||||
$stmt->execute([
|
||||
$data['name'],
|
||||
$data['slug'],
|
||||
$data['description'],
|
||||
$data['install_script'],
|
||||
$data['uninstall_script'],
|
||||
$data['password_command'] ?? '',
|
||||
$data['output_template'],
|
||||
$data['ubuntu_compatible'],
|
||||
$data['is_active'],
|
||||
$data['created_at'],
|
||||
$data['updated_at']
|
||||
]);
|
||||
|
||||
return (int) $pdo->lastInsertId();
|
||||
}
|
||||
|
||||
private function updateProtocol(int $id, array $data): void
|
||||
{
|
||||
$setParts = [];
|
||||
$values = [];
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$setParts[] = "$key = ?";
|
||||
$values[] = $value;
|
||||
}
|
||||
|
||||
$values[] = $id;
|
||||
$sql = 'UPDATE protocols SET ' . implode(', ', $setParts) . ' WHERE id = ?';
|
||||
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare($sql);
|
||||
$stmt->execute($values);
|
||||
}
|
||||
|
||||
private function deleteProtocol(int $id): void
|
||||
{
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('DELETE FROM protocols WHERE id = ?');
|
||||
$stmt->execute([$id]);
|
||||
}
|
||||
|
||||
private function isSlugExists(string $slug, ?int $excludeId = null): bool
|
||||
{
|
||||
$pdo = DB::conn();
|
||||
|
||||
if ($excludeId) {
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ? AND id != ?');
|
||||
$stmt->execute([$slug, $excludeId]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ?');
|
||||
$stmt->execute([$slug]);
|
||||
}
|
||||
|
||||
return (bool) $stmt->fetchColumn();
|
||||
}
|
||||
|
||||
private function isProtocolUsed(int $id): bool
|
||||
{
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM server_protocols WHERE protocol_id = ?');
|
||||
$stmt->execute([$id]);
|
||||
|
||||
return (bool) $stmt->fetchColumn();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,375 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* ScenarioController
|
||||
* Manages protocol installation scenarios (CRUD operations)
|
||||
* Allows administrators to view, create, edit, and delete VPN protocol deployment scenarios
|
||||
*/
|
||||
class ScenarioController {
|
||||
|
||||
/**
|
||||
* List all protocol scenarios
|
||||
* GET /admin/scenarios
|
||||
*/
|
||||
public function listScenarios() {
|
||||
requireAdmin();
|
||||
|
||||
$scenarios = InstallProtocolManager::getAll();
|
||||
|
||||
View::render('settings/scenarios.twig', [
|
||||
'scenarios' => $scenarios,
|
||||
'section' => 'scenarios'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* View single scenario details
|
||||
* GET /admin/scenario/:id
|
||||
*/
|
||||
public function viewScenario($id) {
|
||||
requireAdmin();
|
||||
|
||||
$scenario = InstallProtocolManager::getById((int)$id);
|
||||
if (!$scenario) {
|
||||
http_response_code(404);
|
||||
View::render('404.twig');
|
||||
return;
|
||||
}
|
||||
|
||||
$definition = $scenario['definition'] ?? [];
|
||||
|
||||
View::render('settings/scenario_view.twig', [
|
||||
'scenario' => $scenario,
|
||||
'definition' => $definition,
|
||||
'section' => 'scenarios'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show form to create new scenario
|
||||
* GET /admin/scenario/create
|
||||
*/
|
||||
public function createScenarioForm() {
|
||||
requireAdmin();
|
||||
|
||||
$templateDefinition = [
|
||||
'engine' => 'shell',
|
||||
'metadata' => [
|
||||
'container_name' => 'custom-container',
|
||||
'config_path' => '/opt/amnezia/custom'
|
||||
],
|
||||
'scripts' => [
|
||||
'detect' => 'echo \'{"status":"absent","message":"Custom protocol not found"}\'',
|
||||
'install' => 'echo \'{"success":true,"message":"Custom protocol installed"}\'',
|
||||
'restore' => 'echo \'{"success":true,"message":"Custom protocol restored"}\''
|
||||
]
|
||||
];
|
||||
|
||||
View::render('settings/scenario_form.twig', [
|
||||
'scenario' => null,
|
||||
'templateDefinition' => json_encode($templateDefinition, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES),
|
||||
'section' => 'scenarios'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show form to edit existing scenario
|
||||
* GET /admin/scenario/:id/edit
|
||||
*/
|
||||
public function editScenarioForm($id) {
|
||||
requireAdmin();
|
||||
|
||||
$scenario = InstallProtocolManager::getById((int)$id);
|
||||
if (!$scenario) {
|
||||
http_response_code(404);
|
||||
View::render('404.twig');
|
||||
return;
|
||||
}
|
||||
|
||||
$definitionJson = json_encode($scenario['definition'], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
|
||||
View::render('settings/scenario_form.twig', [
|
||||
'scenario' => $scenario,
|
||||
'templateDefinition' => $definitionJson,
|
||||
'section' => 'scenarios'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save scenario (create or update)
|
||||
* POST /admin/scenario
|
||||
*/
|
||||
public function saveScenario() {
|
||||
requireAdmin();
|
||||
|
||||
$data = $_POST;
|
||||
|
||||
// Validate required fields
|
||||
if (empty($data['slug']) || empty($data['name'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Slug and name are required'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse definition JSON
|
||||
if (!empty($data['definition'])) {
|
||||
if (is_string($data['definition'])) {
|
||||
$definition = json_decode($data['definition'], true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Invalid JSON in definition: ' . json_last_error_msg()
|
||||
]);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
$definition = $data['definition'];
|
||||
}
|
||||
} else {
|
||||
$definition = [];
|
||||
}
|
||||
|
||||
// Validate definition structure
|
||||
if (empty($definition['engine'])) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Definition must have "engine" field'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$saveData = [
|
||||
'slug' => $data['slug'],
|
||||
'name' => $data['name'],
|
||||
'description' => $data['description'] ?? null,
|
||||
'definition' => $definition,
|
||||
'is_active' => isset($data['is_active']) ? (int)$data['is_active'] : 1
|
||||
];
|
||||
|
||||
if (!empty($data['id'])) {
|
||||
$saveData['id'] = (int)$data['id'];
|
||||
}
|
||||
|
||||
try {
|
||||
$id = InstallProtocolManager::save($saveData);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'id' => $id,
|
||||
'message' => 'Scenario saved successfully',
|
||||
'redirect' => '/admin/scenarios'
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Error saving scenario: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete scenario
|
||||
* DELETE /admin/scenario/:id or POST /admin/scenario/:id/delete
|
||||
*/
|
||||
public function deleteScenario($id) {
|
||||
requireAdmin();
|
||||
|
||||
$id = (int)$id;
|
||||
|
||||
// Prevent deletion of default scenario
|
||||
$scenario = InstallProtocolManager::getById($id);
|
||||
if (!$scenario) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(404);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Scenario not found'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($scenario['slug'] === InstallProtocolManager::getDefaultSlug()) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Cannot delete default scenario'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
InstallProtocolManager::delete($id);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'message' => 'Scenario deleted successfully',
|
||||
'redirect' => '/admin/scenarios'
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Error deleting scenario: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test scenario script (detection)
|
||||
* POST /admin/scenario/:id/test
|
||||
*/
|
||||
public function testScenario($id) {
|
||||
requireAdmin();
|
||||
|
||||
$scenario = InstallProtocolManager::getById((int)$id);
|
||||
if (!$scenario) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(404);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Scenario not found'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$serverId = (int)($_POST['server_id'] ?? 0);
|
||||
if (!$serverId) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Server ID required'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$server = new VpnServer($serverId);
|
||||
if (!$server->getData()) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(404);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Server not found'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Test SSH connection first
|
||||
if (!$server->testConnection()) {
|
||||
throw new Exception('SSH connection to server failed');
|
||||
}
|
||||
|
||||
// Run detection script
|
||||
$result = InstallProtocolManager::runDetection($server, $scenario);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'result' => $result
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Error testing scenario: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Export scenario as JSON
|
||||
* GET /admin/scenario/:id/export
|
||||
*/
|
||||
public function exportScenario($id) {
|
||||
requireAdmin();
|
||||
|
||||
$scenario = InstallProtocolManager::getById((int)$id);
|
||||
if (!$scenario) {
|
||||
http_response_code(404);
|
||||
return;
|
||||
}
|
||||
|
||||
header('Content-Type: application/json');
|
||||
header('Content-Disposition: attachment; filename="scenario-' . $scenario['slug'] . '-' . date('Y-m-d') . '.json"');
|
||||
|
||||
// Remove database IDs from export
|
||||
unset($scenario['id']);
|
||||
|
||||
echo json_encode($scenario, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import scenario from JSON
|
||||
* POST /admin/scenario/import
|
||||
*/
|
||||
public function importScenario() {
|
||||
requireAdmin();
|
||||
|
||||
$file = $_FILES['file'] ?? null;
|
||||
if (!$file || $file['error'] !== UPLOAD_ERR_OK) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'No file provided'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
$contents = file_get_contents($file['tmp_name']);
|
||||
$scenario = json_decode($contents, true);
|
||||
|
||||
if (!is_array($scenario)) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(400);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Invalid JSON file'
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Remove ID so it creates a new entry
|
||||
unset($scenario['id']);
|
||||
|
||||
// Ensure required fields exist
|
||||
if (empty($scenario['slug']) || empty($scenario['name'])) {
|
||||
throw new Exception('Imported scenario must have slug and name');
|
||||
}
|
||||
|
||||
$id = InstallProtocolManager::save($scenario);
|
||||
|
||||
header('Content-Type: application/json');
|
||||
echo json_encode([
|
||||
'success' => true,
|
||||
'id' => $id,
|
||||
'message' => 'Scenario imported successfully',
|
||||
'redirect' => '/admin/scenarios'
|
||||
]);
|
||||
} catch (Exception $e) {
|
||||
header('Content-Type: application/json');
|
||||
http_response_code(500);
|
||||
echo json_encode([
|
||||
'success' => false,
|
||||
'message' => 'Error importing scenario: ' . $e->getMessage()
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,11 +13,48 @@ class SettingsController {
|
||||
$stats = $this->getTranslationStats();
|
||||
$users = $this->getAllUsers();
|
||||
$apiKey = $this->getApiKey('openrouter');
|
||||
|
||||
|
||||
// LDAP data for embedded tab
|
||||
$stmt = $this->pdo->query("SELECT * FROM ldap_configs WHERE id = 1");
|
||||
$config = $stmt->fetch() ?: [];
|
||||
$stmt = $this->pdo->query("SELECT * FROM ldap_group_mappings ORDER BY ldap_group");
|
||||
$mappings = $stmt->fetchAll();
|
||||
|
||||
// Protocols data for embedded tab (new management)
|
||||
$protocols = ProtocolService::getAllProtocolsWithStats();
|
||||
$selectedId = isset($_GET['id']) ? (int)$_GET['id'] : null;
|
||||
$isNew = isset($_GET['new']);
|
||||
$editing = null;
|
||||
if (!$isNew) {
|
||||
if ($selectedId) {
|
||||
try {
|
||||
$editing = ProtocolService::getProtocolWithDetails($selectedId);
|
||||
} catch (Exception $e) {
|
||||
$editing = null;
|
||||
}
|
||||
}
|
||||
if (!$editing && !empty($protocols)) {
|
||||
$firstId = (int)($protocols[0]['id'] ?? 0);
|
||||
if ($firstId) {
|
||||
try { $editing = ProtocolService::getProtocolWithDetails($firstId); } catch (Exception $e) { $editing = null; }
|
||||
}
|
||||
}
|
||||
}
|
||||
$definitionPretty = json_encode([], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||||
|
||||
$data = [
|
||||
'translation_stats' => $stats,
|
||||
'users' => $users,
|
||||
'openrouter_key' => $apiKey
|
||||
'openrouter_key' => $apiKey,
|
||||
// LDAP
|
||||
'config' => $config,
|
||||
'mappings' => $mappings,
|
||||
// Protocols
|
||||
'protocols' => $protocols,
|
||||
'editing' => $editing,
|
||||
'definition_json' => $definitionPretty,
|
||||
'is_new' => $isNew,
|
||||
'default_slug' => isset($editing['slug']) ? $editing['slug'] : (isset($protocols[0]['slug']) ? $protocols[0]['slug'] : 'amnezia-wg'),
|
||||
];
|
||||
|
||||
// Check for session messages
|
||||
@@ -29,6 +66,15 @@ class SettingsController {
|
||||
$data['error'] = $_SESSION['settings_error'];
|
||||
unset($_SESSION['settings_error']);
|
||||
}
|
||||
// Also pick up protocol messages if present
|
||||
if (isset($_SESSION['protocol_success']) && !isset($data['success'])) {
|
||||
$data['success'] = $_SESSION['protocol_success'];
|
||||
unset($_SESSION['protocol_success']);
|
||||
}
|
||||
if (isset($_SESSION['protocol_error']) && !isset($data['error'])) {
|
||||
$data['error'] = $_SESSION['protocol_error'];
|
||||
unset($_SESSION['protocol_error']);
|
||||
}
|
||||
|
||||
View::render('settings.twig', $data);
|
||||
}
|
||||
@@ -79,6 +125,29 @@ class SettingsController {
|
||||
exit;
|
||||
}
|
||||
|
||||
public function updateProfile() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /settings');
|
||||
exit;
|
||||
}
|
||||
|
||||
$user = Auth::user();
|
||||
$displayName = trim($_POST['display_name'] ?? '');
|
||||
|
||||
if ($displayName === '') {
|
||||
$_SESSION['settings_error'] = 'Display name cannot be empty';
|
||||
header('Location: /settings#profile');
|
||||
exit;
|
||||
}
|
||||
|
||||
$stmt = $this->pdo->prepare("UPDATE users SET display_name = ? WHERE id = ?");
|
||||
$stmt->execute([$displayName, $user['id']]);
|
||||
|
||||
$_SESSION['settings_success'] = 'Profile updated';
|
||||
header('Location: /settings#profile');
|
||||
exit;
|
||||
}
|
||||
|
||||
public function addUser() {
|
||||
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||
header('Location: /settings');
|
||||
@@ -422,7 +491,7 @@ class SettingsController {
|
||||
]);
|
||||
|
||||
$_SESSION['settings_success'] = 'LDAP settings saved successfully';
|
||||
header('Location: /settings/ldap');
|
||||
header('Location: /settings#ldap');
|
||||
exit;
|
||||
}
|
||||
|
||||
|
||||
+17
-2
@@ -12,13 +12,13 @@ services:
|
||||
MYSQL_USER: ${DB_USERNAME:-amnezia}
|
||||
MYSQL_PASSWORD: ${DB_PASSWORD:-amnezia}
|
||||
ports:
|
||||
- "3307:3306"
|
||||
- "3309:3306"
|
||||
volumes:
|
||||
- db_data:/var/lib/mysql
|
||||
- ./migrations:/docker-entrypoint-initdb.d
|
||||
- ./my.cnf:/etc/mysql/conf.d/my.cnf:ro
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
test: [ "CMD", "mysqladmin", "ping", "-h", "localhost" ]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
@@ -32,6 +32,8 @@ services:
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
dind:
|
||||
condition: service_started
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
@@ -42,10 +44,23 @@ services:
|
||||
DB_DATABASE: ${DB_DATABASE:-amnezia_panel}
|
||||
DB_USERNAME: ${DB_USERNAME:-amnezia}
|
||||
DB_PASSWORD: ${DB_PASSWORD:-amnezia}
|
||||
DOCKER_HOST: tcp://dind:2375
|
||||
volumes:
|
||||
- ./:/var/www/html
|
||||
ports:
|
||||
- "8082:80"
|
||||
|
||||
dind:
|
||||
image: docker:24-dind
|
||||
container_name: amnezia-panel-dind
|
||||
privileged: true
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
DOCKER_TLS_CERTDIR: ""
|
||||
command: ["dockerd", "--host=tcp://0.0.0.0:2375", "--host=unix:///var/run/docker.sock", "--tls=false", "--dns=8.8.8.8", "--dns=1.1.1.1", "--mtu=1200"]
|
||||
volumes:
|
||||
- dind_data:/var/lib/docker
|
||||
|
||||
volumes:
|
||||
db_data:
|
||||
dind_data:
|
||||
|
||||
@@ -0,0 +1,477 @@
|
||||
<?php
|
||||
/**
|
||||
* Backup library utilities for importing servers from backup files.
|
||||
*/
|
||||
class BackupLibrary {
|
||||
/**
|
||||
* Discover available backup files.
|
||||
*
|
||||
* @param bool $registerTokens Whether to register tokens in the session
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function listAvailable(bool $registerTokens = false): array {
|
||||
if (!isset($_SESSION['backup_library']) || !is_array($_SESSION['backup_library'])) {
|
||||
$_SESSION['backup_library'] = [];
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['backup_uploads']) || !is_array($_SESSION['backup_uploads'])) {
|
||||
$_SESSION['backup_uploads'] = [];
|
||||
}
|
||||
|
||||
$results = [];
|
||||
foreach (self::getDirectories() as $directory) {
|
||||
$files = glob($directory . DIRECTORY_SEPARATOR . '*.{backup,json}', GLOB_BRACE) ?: [];
|
||||
foreach ($files as $filePath) {
|
||||
if (!is_file($filePath) || !is_readable($filePath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$parsed = BackupParser::parseMetadata($filePath);
|
||||
} catch (Throwable $e) {
|
||||
// Skip invalid backup file but log for debugging
|
||||
error_log('Backup parse failed for ' . $filePath . ': ' . $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (empty($parsed['servers'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$token = hash('sha256', $filePath);
|
||||
if ($registerTokens || !isset($_SESSION['backup_library'][$token])) {
|
||||
$_SESSION['backup_library'][$token] = $filePath;
|
||||
}
|
||||
|
||||
$results[] = [
|
||||
'token' => $token,
|
||||
'file_name' => basename($filePath),
|
||||
'type' => $parsed['type'],
|
||||
'origin' => 'filesystem',
|
||||
'servers' => self::mapServerMetadata($parsed['servers'] ?? []),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($_SESSION['backup_uploads'] as $token => $upload) {
|
||||
$results[] = [
|
||||
'token' => $token,
|
||||
'file_name' => $upload['file_name'],
|
||||
'type' => $upload['type'],
|
||||
'origin' => 'upload',
|
||||
'servers' => self::mapServerMetadata($upload['data']['servers'] ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
usort($results, function ($a, $b) {
|
||||
return strcmp($a['file_name'], $b['file_name']);
|
||||
});
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load full server data from backup using the session token and server index.
|
||||
*
|
||||
* @param string $token Backup token
|
||||
* @param int $serverIndex Index of the server inside backup file
|
||||
* @return array<string, mixed>
|
||||
* @throws Exception When token or server not found
|
||||
*/
|
||||
public static function loadServer(string $token, int $serverIndex): array {
|
||||
$path = $_SESSION['backup_library'][$token] ?? null;
|
||||
if ($path) {
|
||||
if (!is_file($path) || !is_readable($path)) {
|
||||
throw new Exception('Selected backup is not available');
|
||||
}
|
||||
|
||||
$parsed = BackupParser::parse($path);
|
||||
if (!isset($parsed['servers'][$serverIndex])) {
|
||||
throw new Exception('Requested server not found in backup');
|
||||
}
|
||||
|
||||
$server = $parsed['servers'][$serverIndex];
|
||||
$server['source_file'] = $path;
|
||||
$server['type'] = $parsed['type'];
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
$upload = $_SESSION['backup_uploads'][$token] ?? null;
|
||||
if ($upload) {
|
||||
$parsed = $upload['data'];
|
||||
if (!isset($parsed['servers'][$serverIndex])) {
|
||||
throw new Exception('Requested server not found in uploaded backup');
|
||||
}
|
||||
|
||||
$server = $parsed['servers'][$serverIndex];
|
||||
$server['source_file'] = $upload['path'];
|
||||
$server['type'] = $parsed['type'];
|
||||
|
||||
return $server;
|
||||
}
|
||||
|
||||
throw new Exception('Selected backup is not available');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of directories that may contain backup files.
|
||||
*
|
||||
* @return array<int, string>
|
||||
*/
|
||||
private static function getDirectories(): array {
|
||||
$directories = [];
|
||||
|
||||
$default = realpath(__DIR__ . '/../backups');
|
||||
if ($default) {
|
||||
$directories[] = $default;
|
||||
}
|
||||
|
||||
$envDirs = Config::get('BACKUP_LIBRARY_DIRS');
|
||||
if (!empty($envDirs)) {
|
||||
foreach (preg_split('/[;,]+/', $envDirs) as $rawDir) {
|
||||
$normalized = trim($rawDir);
|
||||
if ($normalized === '') {
|
||||
continue;
|
||||
}
|
||||
if (is_dir($normalized)) {
|
||||
$real = realpath($normalized);
|
||||
if ($real) {
|
||||
$directories[] = $real;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$home = getenv('HOME');
|
||||
if ($home) {
|
||||
$candidate = realpath($home . DIRECTORY_SEPARATOR . 'Downloads' . DIRECTORY_SEPARATOR . 'infosave');
|
||||
if ($candidate) {
|
||||
$directories[] = $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
// Remove duplicates
|
||||
$directories = array_values(array_unique($directories));
|
||||
|
||||
return $directories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register uploaded backup file and return metadata for UI.
|
||||
*/
|
||||
public static function registerUploaded(string $fileName, string $storedPath, array $parsed): array {
|
||||
if (!isset($_SESSION['backup_uploads']) || !is_array($_SESSION['backup_uploads'])) {
|
||||
$_SESSION['backup_uploads'] = [];
|
||||
}
|
||||
|
||||
$token = 'upload_' . bin2hex(random_bytes(16));
|
||||
|
||||
$_SESSION['backup_uploads'][$token] = [
|
||||
'file_name' => $fileName,
|
||||
'path' => $storedPath,
|
||||
'type' => $parsed['type'],
|
||||
'data' => $parsed,
|
||||
];
|
||||
|
||||
return [
|
||||
'token' => $token,
|
||||
'file_name' => $fileName,
|
||||
'type' => $parsed['type'],
|
||||
'origin' => 'upload',
|
||||
'servers' => self::mapServerMetadata($parsed['servers'] ?? []),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether provided token belongs to uploaded backup.
|
||||
*/
|
||||
public static function isUploadToken(string $token): bool {
|
||||
return isset($_SESSION['backup_uploads'][$token]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve stored upload metadata for a token.
|
||||
*/
|
||||
public static function getUploadRecord(string $token): ?array {
|
||||
if (!isset($_SESSION['backup_uploads'][$token])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $_SESSION['backup_uploads'][$token];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get lightweight server metadata for an uploaded backup token.
|
||||
*/
|
||||
public static function getUploadServers(string $token): array {
|
||||
$upload = self::getUploadRecord($token);
|
||||
if (!$upload) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return self::mapServerMetadata($upload['data']['servers'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Forget uploaded backup token and remove temporary file.
|
||||
*/
|
||||
public static function forgetUpload(string $token): void {
|
||||
$upload = $_SESSION['backup_uploads'][$token] ?? null;
|
||||
if (!$upload) {
|
||||
return;
|
||||
}
|
||||
|
||||
unset($_SESSION['backup_uploads'][$token]);
|
||||
|
||||
$path = $upload['path'] ?? null;
|
||||
if ($path && is_file($path)) {
|
||||
@unlink($path);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Map server metadata for front-end lists.
|
||||
*/
|
||||
public static function mapServerMetadata($servers): array {
|
||||
if (!is_array($servers)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(function ($server, $index) {
|
||||
return [
|
||||
'index' => $index,
|
||||
'label' => $server['label'] ?? ('Server #' . ($index + 1)),
|
||||
'host' => $server['host'] ?? null,
|
||||
'vpn_port' => $server['vpn_port'] ?? null,
|
||||
'client_count' => isset($server['clients']) && is_array($server['clients'])
|
||||
? count($server['clients'])
|
||||
: 0
|
||||
];
|
||||
}, $servers, array_keys($servers));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse backup files and normalize into a single representation.
|
||||
*/
|
||||
class BackupParser {
|
||||
/**
|
||||
* Parse backup file metadata without storing heavy payloads.
|
||||
*
|
||||
* @param string $path
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function parseMetadata(string $path): array {
|
||||
$parsed = self::parse($path);
|
||||
|
||||
// Strip client details to keep metadata light
|
||||
$parsed['servers'] = array_map(function ($server) {
|
||||
$server['clients'] = $server['clients'] ?? [];
|
||||
return $server;
|
||||
}, $parsed['servers']);
|
||||
|
||||
return $parsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse backup file fully.
|
||||
*
|
||||
* @param string $path
|
||||
* @return array<string, mixed>
|
||||
* @throws Exception On parse errors
|
||||
*/
|
||||
public static function parse(string $path): array {
|
||||
$contents = file_get_contents($path);
|
||||
if ($contents === false) {
|
||||
throw new Exception('Unable to read backup file');
|
||||
}
|
||||
|
||||
$decoded = json_decode($contents, true);
|
||||
if (!is_array($decoded)) {
|
||||
throw new Exception('Backup file is not valid JSON');
|
||||
}
|
||||
|
||||
if (isset($decoded['server']) && isset($decoded['clients'])) {
|
||||
return self::parsePanelBackup($decoded);
|
||||
}
|
||||
|
||||
if (isset($decoded['Servers/serversList'])) {
|
||||
return self::parseAmneziaBackup($decoded);
|
||||
}
|
||||
|
||||
throw new Exception('Unsupported backup format');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse backup produced by the Amnezia mobile/desktop application (.backup files).
|
||||
*/
|
||||
private static function parseAmneziaBackup(array $decoded): array {
|
||||
$serversRaw = json_decode($decoded['Servers/serversList'] ?? '[]', true);
|
||||
if (!is_array($serversRaw)) {
|
||||
throw new Exception('Invalid Amnezia backup payload');
|
||||
}
|
||||
|
||||
$servers = [];
|
||||
foreach ($serversRaw as $serverIndex => $serverEntry) {
|
||||
$containers = $serverEntry['containers'] ?? [];
|
||||
foreach ($containers as $container) {
|
||||
if (($container['container'] ?? '') !== 'amnezia-awg') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$awg = $container['awg'] ?? [];
|
||||
if (empty($awg)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$host = $serverEntry['hostName'] ?? ($awg['hostName'] ?? null);
|
||||
if (!$host) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$awgParams = [];
|
||||
foreach (['Jc', 'Jmin', 'Jmax', 'S1', 'S2', 'H1', 'H2', 'H3', 'H4'] as $key) {
|
||||
if (isset($awg[$key])) {
|
||||
$awgParams[$key] = is_numeric($awg[$key]) ? (int)$awg[$key] : $awg[$key];
|
||||
}
|
||||
}
|
||||
|
||||
$vpnPort = isset($awg['port']) ? (int)$awg['port'] : null;
|
||||
$sshPort = isset($serverEntry['port']) ? (int)$serverEntry['port'] : 22;
|
||||
$sshUser = $serverEntry['userName'] ?? 'root';
|
||||
$sshPass = $serverEntry['password'] ?? '';
|
||||
if ($sshPass === '') {
|
||||
// Skip records without SSH credentials; these are likely client snapshots.
|
||||
continue;
|
||||
}
|
||||
$name = trim($serverEntry['description'] ?? '') ?: $host;
|
||||
|
||||
$subnet = $container['awg']['subnet_address'] ?? null;
|
||||
$clients = [];
|
||||
|
||||
if (!empty($awg['last_config'])) {
|
||||
$lastConfig = json_decode($awg['last_config'], true);
|
||||
if (is_array($lastConfig)) {
|
||||
$clientIp = $lastConfig['client_ip'] ?? null;
|
||||
if (!$subnet && $clientIp) {
|
||||
$subnet = self::inferSubnet($clientIp);
|
||||
}
|
||||
|
||||
$clients[] = [
|
||||
'name' => $lastConfig['client_ip'] ?? ($lastConfig['clientId'] ?? $host . '_client'),
|
||||
'client_ip' => $clientIp,
|
||||
'public_key' => $lastConfig['client_pub_key'] ?? '',
|
||||
'private_key' => $lastConfig['client_priv_key'] ?? '',
|
||||
'preshared_key' => $lastConfig['psk_key'] ?? ($awg['psk_key'] ?? ''),
|
||||
'config' => $lastConfig['config'] ?? '',
|
||||
'status' => 'active',
|
||||
'expires_at' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
if (!$subnet) {
|
||||
$subnet = '10.8.1.0/24';
|
||||
} elseif (!str_contains($subnet, '/')) {
|
||||
$subnet .= '/24';
|
||||
}
|
||||
|
||||
$servers[] = [
|
||||
'label' => $name . ' (' . $host . ')',
|
||||
'name' => $name,
|
||||
'host' => $host,
|
||||
'ssh_port' => $sshPort,
|
||||
'ssh_username' => $sshUser ?: 'root',
|
||||
'ssh_password' => $sshPass,
|
||||
'vpn_port' => $vpnPort,
|
||||
'container_name' => $container['container'] ?? 'amnezia-awg',
|
||||
'vpn_subnet' => $subnet,
|
||||
'server_public_key' => $awg['server_pub_key'] ?? null,
|
||||
'preshared_key' => $awg['psk_key'] ?? null,
|
||||
'awg_params' => $awgParams,
|
||||
'clients' => $clients,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'amnezia_app',
|
||||
'servers' => $servers,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse backup generated by this panel (backups/backup_*.json).
|
||||
*/
|
||||
private static function parsePanelBackup(array $decoded): array {
|
||||
$server = $decoded['server'];
|
||||
$awgParams = $server['awg_params'] ?? [];
|
||||
if (is_string($awgParams)) {
|
||||
$decodedParams = json_decode($awgParams, true);
|
||||
if (is_array($decodedParams)) {
|
||||
$awgParams = $decodedParams;
|
||||
}
|
||||
}
|
||||
|
||||
$vpnPort = isset($server['vpn_port']) ? (int)$server['vpn_port'] : null;
|
||||
$sshPort = isset($server['port']) ? (int)$server['port'] : 22;
|
||||
$sshUser = $server['username'] ?? 'root';
|
||||
$sshPass = $server['password'] ?? '';
|
||||
$host = $server['host']
|
||||
?? $server['host_name']
|
||||
?? $server['host_ip']
|
||||
?? null;
|
||||
|
||||
if (!$host) {
|
||||
throw new Exception('Panel backup is missing server host/SSH details. Create the server manually and import its clients via the panel importer.');
|
||||
}
|
||||
|
||||
$clients = [];
|
||||
foreach ($decoded['clients'] as $client) {
|
||||
$clients[] = [
|
||||
'name' => $client['name'] ?? ($client['client_ip'] ?? 'client'),
|
||||
'client_ip' => $client['client_ip'] ?? null,
|
||||
'public_key' => $client['public_key'] ?? '',
|
||||
'private_key' => $client['private_key'] ?? '',
|
||||
'preshared_key' => $client['preshared_key'] ?? ($server['preshared_key'] ?? ''),
|
||||
'config' => $client['config'] ?? '',
|
||||
'status' => $client['status'] ?? 'active',
|
||||
'expires_at' => $client['expires_at'] ?? null,
|
||||
'created_at' => $client['created_at'] ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'type' => 'panel_backup',
|
||||
'servers' => [
|
||||
[
|
||||
'label' => ($server['name'] ?? 'Server') . ' (' . $host . ')',
|
||||
'name' => $server['name'] ?? 'Server',
|
||||
'host' => $host,
|
||||
'ssh_port' => $sshPort,
|
||||
'ssh_username' => $sshUser,
|
||||
'ssh_password' => $sshPass,
|
||||
'vpn_port' => $vpnPort,
|
||||
'container_name' => $server['container_name'] ?? 'amnezia-awg',
|
||||
'vpn_subnet' => $server['vpn_subnet'] ?? '10.8.1.0/24',
|
||||
'server_public_key' => $server['server_public_key'] ?? null,
|
||||
'preshared_key' => $server['preshared_key'] ?? null,
|
||||
'awg_params' => $awgParams,
|
||||
'clients' => $clients,
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer /24 subnet from client IP.
|
||||
*/
|
||||
private static function inferSubnet(string $ip): string {
|
||||
$parts = explode('.', $ip);
|
||||
if (count($parts) === 4) {
|
||||
return $parts[0] . '.' . $parts[1] . '.' . $parts[2] . '.0/24';
|
||||
}
|
||||
return '10.8.1.0/24';
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
class Logger {
|
||||
private const DEFAULT_LOGS_DIR = __DIR__ . '/../logs';
|
||||
|
||||
private static function ensureDir(string $dir): void {
|
||||
if (!is_dir($dir)) {
|
||||
@mkdir($dir, 0777, true);
|
||||
}
|
||||
}
|
||||
|
||||
private static function getLogsDir(): string {
|
||||
// Fallback to project logs directory next to inc/
|
||||
$dir = self::DEFAULT_LOGS_DIR;
|
||||
self::ensureDir($dir);
|
||||
return $dir;
|
||||
}
|
||||
|
||||
public static function appendInstall(int $serverId, string $message): void {
|
||||
$dir = self::getLogsDir();
|
||||
$file = $dir . '/install_server_' . $serverId . '.log';
|
||||
$line = '[' . date('Y-m-d H:i:s') . '] ' . $message . "\n";
|
||||
@file_put_contents($file, $line, FILE_APPEND);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
<?php
|
||||
|
||||
class OpenRouterService {
|
||||
|
||||
private $apiKey;
|
||||
private $apiUrl = 'https://openrouter.ai/api/v1';
|
||||
private $timeout = 60; // 60 seconds timeout for AI generation
|
||||
|
||||
public function __construct() {
|
||||
$this->apiKey = $_ENV['OPENROUTER_API_KEY'] ?? null;
|
||||
if (!$this->apiKey) {
|
||||
throw new Exception('OpenRouter API key not configured');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate installation script using OpenRouter API
|
||||
*/
|
||||
public function generateScript(string $prompt, string $model = 'openai/gpt-3.5-turbo'): array {
|
||||
try {
|
||||
$messages = [
|
||||
[
|
||||
'role' => 'system',
|
||||
'content' => 'You are a helpful assistant that creates bash installation scripts for VPN protocols. Always respond with valid JSON containing the script, suggestions, ubuntu compatibility, and estimated installation time.'
|
||||
],
|
||||
[
|
||||
'role' => 'user',
|
||||
'content' => $prompt
|
||||
]
|
||||
];
|
||||
|
||||
$response = $this->makeAPICall('/chat/completions', [
|
||||
'model' => $model,
|
||||
'messages' => $messages,
|
||||
'temperature' => 0.3, // Lower temperature for more consistent results
|
||||
'max_tokens' => 4000, // Sufficient for detailed scripts
|
||||
'response_format' => ['type' => 'json_object']
|
||||
]);
|
||||
|
||||
if (!isset($response['choices'][0]['message']['content'])) {
|
||||
throw new Exception('Invalid response from OpenRouter API');
|
||||
}
|
||||
|
||||
$content = $response['choices'][0]['message']['content'];
|
||||
$parsed = json_decode($content, true);
|
||||
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
// If JSON parsing fails, try to extract script from plain text
|
||||
return $this->parsePlainTextResponse($content);
|
||||
}
|
||||
|
||||
return $this->validateAndEnhanceResponse($parsed);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in OpenRouterService::generateScript: " . $e->getMessage());
|
||||
throw new Exception('Failed to generate script: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available AI models from OpenRouter
|
||||
*/
|
||||
public function getAvailableModels(): array {
|
||||
try {
|
||||
$response = $this->makeAPICall('/models', [], 'GET');
|
||||
|
||||
if (!isset($response['data'])) {
|
||||
throw new Exception('Invalid response from OpenRouter API');
|
||||
}
|
||||
|
||||
// Filter models suitable for code generation
|
||||
$codeModels = array_filter($response['data'], function($model) {
|
||||
$codeModelIds = [
|
||||
'openai/gpt-3.5-turbo',
|
||||
'openai/gpt-4',
|
||||
'openai/gpt-4-turbo',
|
||||
'anthropic/claude-3-haiku',
|
||||
'anthropic/claude-3-sonnet',
|
||||
'anthropic/claude-3-opus',
|
||||
'google/gemini-pro',
|
||||
'meta-llama/llama-2-70b-chat',
|
||||
'meta-llama/llama-3-70b-instruct'
|
||||
];
|
||||
|
||||
return in_array($model['id'], $codeModelIds) && $model['top_provider'] === true;
|
||||
});
|
||||
|
||||
return array_values(array_map(function($model) {
|
||||
return [
|
||||
'id' => $model['id'],
|
||||
'name' => $model['name'] ?? $model['id'],
|
||||
'description' => $model['description'] ?? '',
|
||||
'pricing' => $model['pricing'] ?? null
|
||||
];
|
||||
}, $codeModels));
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in OpenRouterService::getAvailableModels: " . $e->getMessage());
|
||||
// Return default models if API call fails
|
||||
return $this->getDefaultModels();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Make API call to OpenRouter
|
||||
*/
|
||||
private function makeAPICall(string $endpoint, array $data = [], string $method = 'POST'): array {
|
||||
$ch = curl_init();
|
||||
|
||||
$url = $this->apiUrl . $endpoint;
|
||||
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_URL => $url,
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_TIMEOUT => $this->timeout,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Authorization: Bearer ' . $this->apiKey,
|
||||
'Content-Type: application/json',
|
||||
'HTTP-Referer: ' . ($_ENV['APP_URL'] ?? 'https://localhost'),
|
||||
'X-Title: Amnezia VPN Panel'
|
||||
],
|
||||
CURLOPT_SSL_VERIFYPEER => true,
|
||||
CURLOPT_SSL_VERIFYHOST => 2
|
||||
]);
|
||||
|
||||
if ($method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
|
||||
}
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$error = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($error) {
|
||||
throw new Exception('CURL error: ' . $error);
|
||||
}
|
||||
|
||||
if ($httpCode >= 400) {
|
||||
$errorData = json_decode($response, true);
|
||||
$errorMessage = $errorData['error']['message'] ?? "HTTP $httpCode error";
|
||||
throw new Exception($errorMessage);
|
||||
}
|
||||
|
||||
$decoded = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception('Invalid JSON response from OpenRouter API');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse plain text response when JSON parsing fails
|
||||
*/
|
||||
private function parsePlainTextResponse(string $content): array {
|
||||
// Try to extract bash script from plain text
|
||||
if (preg_match('/```bash\n(.*?)\n```/s', $content, $matches)) {
|
||||
$script = trim($matches[1]);
|
||||
} elseif (preg_match('/```(.*?)```/s', $content, $matches)) {
|
||||
$script = trim($matches[1]);
|
||||
} else {
|
||||
// If no code blocks found, treat the entire content as script
|
||||
$script = trim($content);
|
||||
}
|
||||
|
||||
// Add bash shebang if not present
|
||||
if (!str_starts_with($script, '#!')) {
|
||||
$script = "#!/bin/bash\n\n" . $script;
|
||||
}
|
||||
|
||||
return [
|
||||
'script' => $script,
|
||||
'suggestions' => [
|
||||
'Check the script for syntax errors',
|
||||
'Test the script in a safe environment',
|
||||
'Review security implications'
|
||||
],
|
||||
'ubuntu_compatible' => true,
|
||||
'estimated_time' => '5 minutes'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate and enhance AI response
|
||||
*/
|
||||
private function validateAndEnhanceResponse(array $response): array {
|
||||
$defaults = [
|
||||
'script' => '#!/bin/bash\n# Default installation script\necho "Installation script placeholder"',
|
||||
'suggestions' => [],
|
||||
'ubuntu_compatible' => true,
|
||||
'estimated_time' => '5 minutes'
|
||||
];
|
||||
|
||||
// Ensure all required fields are present
|
||||
foreach ($defaults as $key => $defaultValue) {
|
||||
if (!isset($response[$key])) {
|
||||
$response[$key] = $defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate script format
|
||||
if (!str_starts_with(trim($response['script']), '#!')) {
|
||||
$response['script'] = "#!/bin/bash\n\n" . $response['script'];
|
||||
}
|
||||
|
||||
// Ensure suggestions is an array
|
||||
if (!is_array($response['suggestions'])) {
|
||||
$response['suggestions'] = [];
|
||||
}
|
||||
|
||||
// Add default suggestions if none provided
|
||||
if (empty($response['suggestions'])) {
|
||||
$response['suggestions'] = [
|
||||
'Review the generated script for security implications',
|
||||
'Test the script in a development environment first',
|
||||
'Ensure all dependencies are available on your system',
|
||||
'Backup your system before running the script'
|
||||
];
|
||||
}
|
||||
|
||||
// Validate ubuntu_compatible is boolean
|
||||
if (!is_bool($response['ubuntu_compatible'])) {
|
||||
$response['ubuntu_compatible'] = true;
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get default models when API is unavailable
|
||||
*/
|
||||
private function getDefaultModels(): array {
|
||||
return [
|
||||
[
|
||||
'id' => 'openai/gpt-3.5-turbo',
|
||||
'name' => 'GPT-3.5 Turbo',
|
||||
'description' => 'Fast and cost-effective model for general purpose tasks',
|
||||
'pricing' => ['prompt' => '0.001', 'completion' => '0.002']
|
||||
],
|
||||
[
|
||||
'id' => 'openai/gpt-4',
|
||||
'name' => 'GPT-4',
|
||||
'description' => 'Most capable model for complex tasks',
|
||||
'pricing' => ['prompt' => '0.03', 'completion' => '0.06']
|
||||
],
|
||||
[
|
||||
'id' => 'anthropic/claude-3-haiku',
|
||||
'name' => 'Claude 3 Haiku',
|
||||
'description' => 'Fast and cost-effective model from Anthropic',
|
||||
'pricing' => ['prompt' => '0.00025', 'completion' => '0.00125']
|
||||
],
|
||||
[
|
||||
'id' => 'anthropic/claude-3-sonnet',
|
||||
'name' => 'Claude 3 Sonnet',
|
||||
'description' => 'Balanced performance and cost from Anthropic',
|
||||
'pricing' => ['prompt' => '0.003', 'completion' => '0.015']
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function testModelAvailability(string $modelId): array {
|
||||
if (!$this->apiKey) {
|
||||
return [
|
||||
'success' => false,
|
||||
'http_code' => 401,
|
||||
'message' => 'OpenRouter API key not configured'
|
||||
];
|
||||
}
|
||||
|
||||
$payload = [
|
||||
'model' => $modelId,
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => 'Reply with: OK']
|
||||
],
|
||||
'max_tokens' => 5,
|
||||
'temperature' => 0
|
||||
];
|
||||
|
||||
$ch = curl_init($this->apiUrl . '/chat/completions');
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($payload));
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, [
|
||||
'Content-Type: application/json',
|
||||
'Authorization: Bearer ' . $this->apiKey,
|
||||
'HTTP-Referer: https://amnez.ia',
|
||||
'X-Title: Amnezia VPN Panel'
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$curlError = curl_error($ch);
|
||||
curl_close($ch);
|
||||
|
||||
if ($curlError) {
|
||||
return [
|
||||
'success' => false,
|
||||
'http_code' => null,
|
||||
'message' => 'Network error: ' . $curlError
|
||||
];
|
||||
}
|
||||
|
||||
$json = json_decode($response, true);
|
||||
$ok = $httpCode === 200 && isset($json['choices'][0]['message']['content']);
|
||||
return [
|
||||
'success' => $ok,
|
||||
'http_code' => $httpCode,
|
||||
'message' => $ok ? 'Model is available' : ($json['error']['message'] ?? 'Model test failed')
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
<?php
|
||||
|
||||
class ProtocolService
|
||||
{
|
||||
|
||||
/**
|
||||
* Get all protocols with additional metadata
|
||||
*/
|
||||
public static function getAllProtocolsWithStats(): array
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->query('
|
||||
SELECT p.*,
|
||||
COUNT(DISTINCT sp.server_id) as server_count,
|
||||
COUNT(DISTINCT pt.id) as template_count,
|
||||
COUNT(DISTINCT pv.id) as variable_count,
|
||||
COUNT(DISTINCT ag.id) as ai_generation_count,
|
||||
MAX(ag.created_at) as last_ai_generation
|
||||
FROM protocols p
|
||||
LEFT JOIN server_protocols sp ON p.id = sp.protocol_id
|
||||
LEFT JOIN protocol_templates pt ON p.id = pt.protocol_id
|
||||
LEFT JOIN protocol_variables pv ON p.id = pv.protocol_id
|
||||
LEFT JOIN ai_generations ag ON p.id = ag.protocol_id
|
||||
GROUP BY p.id
|
||||
ORDER BY p.name ASC
|
||||
');
|
||||
|
||||
return $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::getAllProtocolsWithStats: " . $e->getMessage());
|
||||
throw new Exception('Failed to get protocols with stats');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get protocol with all related data (templates, variables, AI history)
|
||||
*/
|
||||
public static function getProtocolWithDetails(int $protocolId): array
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
|
||||
// Get protocol
|
||||
$stmt = $pdo->prepare('SELECT * FROM protocols WHERE id = ?');
|
||||
$stmt->execute([$protocolId]);
|
||||
$protocol = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||
|
||||
if (!$protocol) {
|
||||
throw new Exception('Protocol not found');
|
||||
}
|
||||
|
||||
// Get templates
|
||||
$stmt = $pdo->prepare('SELECT * FROM protocol_templates WHERE protocol_id = ? ORDER BY is_default DESC, template_name ASC');
|
||||
$stmt->execute([$protocolId]);
|
||||
$protocol['templates'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Get variables
|
||||
$stmt = $pdo->prepare('SELECT * FROM protocol_variables WHERE protocol_id = ? ORDER BY variable_name ASC');
|
||||
$stmt->execute([$protocolId]);
|
||||
$protocol['variables'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Get AI generation history (last 10)
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT ag.*, p.name as protocol_name
|
||||
FROM ai_generations ag
|
||||
LEFT JOIN protocols p ON ag.protocol_id = p.id
|
||||
WHERE ag.protocol_id = ?
|
||||
ORDER BY ag.created_at DESC
|
||||
LIMIT 10
|
||||
');
|
||||
$stmt->execute([$protocolId]);
|
||||
$protocol['ai_history'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Get server usage
|
||||
$stmt = $pdo->prepare('
|
||||
SELECT sp.*, vs.name as server_name, vs.host as server_host
|
||||
FROM server_protocols sp
|
||||
JOIN vpn_servers vs ON sp.server_id = vs.id
|
||||
WHERE sp.protocol_id = ?
|
||||
ORDER BY sp.applied_at DESC
|
||||
');
|
||||
$stmt->execute([$protocolId]);
|
||||
$protocol['server_usage'] = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return $protocol;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::getProtocolWithDetails: " . $e->getMessage());
|
||||
throw new Exception('Failed to get protocol details');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate protocol data before saving
|
||||
*/
|
||||
public static function validateProtocolData(array $data): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// Validate name
|
||||
if (empty($data['name'])) {
|
||||
$errors[] = 'Protocol name is required';
|
||||
} elseif (strlen($data['name']) > 255) {
|
||||
$errors[] = 'Protocol name must be less than 255 characters';
|
||||
}
|
||||
|
||||
// Validate slug
|
||||
if (empty($data['slug'])) {
|
||||
$errors[] = 'Protocol slug is required';
|
||||
} elseif (!preg_match('/^[a-z0-9_-]+$/i', $data['slug'])) {
|
||||
$errors[] = 'Slug may contain only letters, numbers, dashes, and underscores';
|
||||
} elseif (strlen($data['slug']) > 100) {
|
||||
$errors[] = 'Protocol slug must be less than 100 characters';
|
||||
}
|
||||
|
||||
// Validate description length
|
||||
if (isset($data['description']) && strlen($data['description']) > 65535) {
|
||||
$errors[] = 'Description is too long';
|
||||
}
|
||||
|
||||
// Validate install script
|
||||
if (isset($data['install_script']) && strlen($data['install_script']) > 16777215) { // MEDIUMTEXT limit
|
||||
$errors[] = 'Installation script is too long';
|
||||
}
|
||||
|
||||
// Validate output template
|
||||
if (isset($data['output_template']) && strlen($data['output_template']) > 16777215) { // MEDIUMTEXT limit
|
||||
$errors[] = 'Output template is too long';
|
||||
}
|
||||
|
||||
// Validate ubuntu_compatible
|
||||
if (isset($data['ubuntu_compatible']) && !is_bool($data['ubuntu_compatible']) && !in_array($data['ubuntu_compatible'], [0, 1, '0', '1'])) {
|
||||
$errors[] = 'Ubuntu compatible must be a boolean value';
|
||||
}
|
||||
|
||||
// Validate is_active
|
||||
if (isset($data['is_active']) && !is_bool($data['is_active']) && !in_array($data['is_active'], [0, 1, '0', '1'])) {
|
||||
$errors[] = 'Active status must be a boolean value';
|
||||
}
|
||||
|
||||
// Validate QR code template
|
||||
if (isset($data['qr_code_template']) && strlen($data['qr_code_template']) > 16777215) {
|
||||
$errors[] = 'QR code template is too long';
|
||||
}
|
||||
|
||||
// Validate QR code format
|
||||
if (isset($data['qr_code_format']) && !in_array($data['qr_code_format'], ['raw', 'amnezia_compressed'])) {
|
||||
$errors[] = 'Invalid QR code format';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if slug is unique
|
||||
*/
|
||||
public static function isSlugUnique(string $slug, ?int $excludeId = null): bool
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
|
||||
if ($excludeId) {
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ? AND id != ?');
|
||||
$stmt->execute([$slug, $excludeId]);
|
||||
} else {
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM protocols WHERE slug = ?');
|
||||
$stmt->execute([$slug]);
|
||||
}
|
||||
|
||||
return (int) $stmt->fetchColumn() === 0;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::isSlugUnique: " . $e->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if protocol can be deleted
|
||||
*/
|
||||
public static function canDeleteProtocol(int $protocolId): array
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
|
||||
// Check if protocol is used by any servers
|
||||
$stmt = $pdo->prepare('SELECT COUNT(*) FROM server_protocols WHERE protocol_id = ?');
|
||||
$stmt->execute([$protocolId]);
|
||||
$serverCount = (int) $stmt->fetchColumn();
|
||||
|
||||
$canDelete = $serverCount === 0;
|
||||
$reason = '';
|
||||
|
||||
if (!$canDelete) {
|
||||
$reason = "Protocol is currently used by $serverCount server(s)";
|
||||
}
|
||||
|
||||
return [
|
||||
'can_delete' => $canDelete,
|
||||
'reason' => $reason,
|
||||
'server_count' => $serverCount
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::canDeleteProtocol: " . $e->getMessage());
|
||||
return [
|
||||
'can_delete' => false,
|
||||
'reason' => 'Database error occurred',
|
||||
'server_count' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate protocol template with variables
|
||||
*/
|
||||
public static function generateProtocolOutput(array $protocol, array $variables): string
|
||||
{
|
||||
try {
|
||||
$template = $protocol['output_template'] ?? '';
|
||||
|
||||
if (empty($template)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
foreach ($variables as $key => $value) {
|
||||
$template = str_replace('{{' . $key . '}}', $value ?? '', $template);
|
||||
}
|
||||
$template = preg_replace('/(\w+:\/\/[^\/:]+):(?=\/|\?|$)/', '$1', $template);
|
||||
$template = preg_replace('/(@[^\/:]+):(?=\/|\?|$)/', '$1', $template);
|
||||
$template = preg_replace('/(\w+:\/\/)@(?=[^\/]{1})/', '$1', $template);
|
||||
$template = preg_replace('/\{\{[^}]+\}\}/', '', $template);
|
||||
|
||||
// Check for unreplaced variables
|
||||
if (preg_match('/\{\{([^}]+)\}\}/', $template, $matches)) {
|
||||
error_log("Unreplaced variables in protocol template: " . implode(', ', $matches));
|
||||
}
|
||||
|
||||
return $template;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::generateProtocolOutput: " . $e->getMessage());
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate QR code payload from template
|
||||
*/
|
||||
public static function generateQrCodePayload(array $protocol, array $variables): string
|
||||
{
|
||||
try {
|
||||
$template = $protocol['qr_code_template'] ?? '';
|
||||
$format = $protocol['qr_code_format'] ?? 'amnezia_compressed';
|
||||
|
||||
if (empty($template)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Render template using the same logic as output template
|
||||
// We temporarily wrap it to use the existing method
|
||||
$rendered = self::generateProtocolOutput(['output_template' => $template], $variables);
|
||||
|
||||
if ($format === 'amnezia_compressed') {
|
||||
require_once __DIR__ . '/QrUtil.php';
|
||||
return QrUtil::encodeOldPayloadFromJson($rendered);
|
||||
}
|
||||
|
||||
// For 'raw' and 'text' formats, return rendered template directly
|
||||
return $rendered;
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::generateQrCodePayload: " . $e->getMessage());
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get protocol statistics for dashboard
|
||||
*/
|
||||
public static function getProtocolStatistics(): array
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
|
||||
// Total protocols
|
||||
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols');
|
||||
$totalProtocols = (int) $stmt->fetchColumn();
|
||||
|
||||
// Active protocols
|
||||
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols WHERE is_active = 1');
|
||||
$activeProtocols = (int) $stmt->fetchColumn();
|
||||
|
||||
// Ubuntu compatible protocols
|
||||
$stmt = $pdo->query('SELECT COUNT(*) FROM protocols WHERE ubuntu_compatible = 1');
|
||||
$ubuntuCompatibleProtocols = (int) $stmt->fetchColumn();
|
||||
|
||||
// Protocols with AI generations
|
||||
$stmt = $pdo->query('
|
||||
SELECT COUNT(DISTINCT protocol_id)
|
||||
FROM ai_generations
|
||||
WHERE protocol_id IS NOT NULL
|
||||
');
|
||||
$protocolsWithAI = (int) $stmt->fetchColumn();
|
||||
|
||||
// Recent AI generations
|
||||
$stmt = $pdo->query('
|
||||
SELECT COUNT(*)
|
||||
FROM ai_generations
|
||||
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 7 DAY)
|
||||
');
|
||||
$recentAIGenerations = (int) $stmt->fetchColumn();
|
||||
|
||||
// Server usage by protocol
|
||||
$stmt = $pdo->query('
|
||||
SELECT p.name, COUNT(sp.server_id) as server_count
|
||||
FROM protocols p
|
||||
LEFT JOIN server_protocols sp ON p.id = sp.protocol_id
|
||||
GROUP BY p.id, p.name
|
||||
ORDER BY server_count DESC
|
||||
LIMIT 10
|
||||
');
|
||||
$serverUsageByProtocol = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
return [
|
||||
'total_protocols' => $totalProtocols,
|
||||
'active_protocols' => $activeProtocols,
|
||||
'ubuntu_compatible_protocols' => $ubuntuCompatibleProtocols,
|
||||
'protocols_with_ai' => $protocolsWithAI,
|
||||
'recent_ai_generations' => $recentAIGenerations,
|
||||
'server_usage_by_protocol' => $serverUsageByProtocol,
|
||||
'ai_usage_percentage' => $totalProtocols > 0 ? round(($protocolsWithAI / $totalProtocols) * 100, 2) : 0
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::getProtocolStatistics: " . $e->getMessage());
|
||||
return [
|
||||
'total_protocols' => 0,
|
||||
'active_protocols' => 0,
|
||||
'ubuntu_compatible_protocols' => 0,
|
||||
'protocols_with_ai' => 0,
|
||||
'recent_ai_generations' => 0,
|
||||
'server_usage_by_protocol' => [],
|
||||
'ai_usage_percentage' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get AI generation statistics
|
||||
*/
|
||||
public static function getAIGenerationStatistics(): array
|
||||
{
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
|
||||
// Total AI generations
|
||||
$stmt = $pdo->query('SELECT COUNT(*) FROM ai_generations');
|
||||
$totalGenerations = (int) $stmt->fetchColumn();
|
||||
|
||||
// AI generations this month
|
||||
$stmt = $pdo->query('
|
||||
SELECT COUNT(*)
|
||||
FROM ai_generations
|
||||
WHERE MONTH(created_at) = MONTH(NOW()) AND YEAR(created_at) = YEAR(NOW())
|
||||
');
|
||||
$thisMonthGenerations = (int) $stmt->fetchColumn();
|
||||
|
||||
// AI generations by model
|
||||
$stmt = $pdo->query('
|
||||
SELECT model_used, COUNT(*) as count
|
||||
FROM ai_generations
|
||||
GROUP BY model_used
|
||||
ORDER BY count DESC
|
||||
');
|
||||
$generationsByModel = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
// Ubuntu compatible generations
|
||||
$stmt = $pdo->query('
|
||||
SELECT COUNT(*)
|
||||
FROM ai_generations
|
||||
WHERE ubuntu_compatible = 1
|
||||
');
|
||||
$ubuntuCompatibleGenerations = (int) $stmt->fetchColumn();
|
||||
|
||||
return [
|
||||
'total_generations' => $totalGenerations,
|
||||
'this_month_generations' => $thisMonthGenerations,
|
||||
'generations_by_model' => $generationsByModel,
|
||||
'ubuntu_compatible_generations' => $ubuntuCompatibleGenerations,
|
||||
'ubuntu_compatible_percentage' => $totalGenerations > 0 ? round(($ubuntuCompatibleGenerations / $totalGenerations) * 100, 2) : 0
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
error_log("Error in ProtocolService::getAIGenerationStatistics: " . $e->getMessage());
|
||||
return [
|
||||
'total_generations' => 0,
|
||||
'this_month_generations' => 0,
|
||||
'generations_by_model' => [],
|
||||
'ubuntu_compatible_generations' => 0,
|
||||
'ubuntu_compatible_percentage' => 0
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
+303
-51
@@ -7,8 +7,10 @@ use Endroid\QrCode\Label\Label;
|
||||
use Endroid\QrCode\Label\LabelAlignment;
|
||||
use Endroid\QrCode\Encoding\Encoding;
|
||||
|
||||
class QrUtil {
|
||||
public static function pngBase64(string $text, int $size = 300, int $margin = 1, string $label = 'Amnezia QR (old)') : string {
|
||||
class QrUtil
|
||||
{
|
||||
public static function pngBase64(string $text, int $size = 300, int $margin = 1, string $label = 'Amnezia QR (old)'): string
|
||||
{
|
||||
// Try to load Composer autoload if not yet loaded
|
||||
if (!class_exists(QrCode::class)) {
|
||||
$autoload = __DIR__ . '/vendor/autoload.php';
|
||||
@@ -53,31 +55,34 @@ class QrUtil {
|
||||
throw new RuntimeException('QR library not available');
|
||||
}
|
||||
|
||||
private static function urlsafe_b64_encode(string $bytes): string {
|
||||
private static function urlsafe_b64_encode(string $bytes): string
|
||||
{
|
||||
return rtrim(strtr(base64_encode($bytes), '+/', '-_'), '=');
|
||||
}
|
||||
|
||||
public static function encodeOldPayloadFromJson(string $jsonText): string {
|
||||
public static function encodeOldPayloadFromJson(string $jsonText): string
|
||||
{
|
||||
$json = self::normalizeJson($jsonText);
|
||||
// Old format uses zlib (gzcompress) with header [version, compressed_len, uncompressed_len]
|
||||
$compressed = gzcompress($json, 9);
|
||||
if ($compressed === false) {
|
||||
throw new RuntimeException('gzcompress failed');
|
||||
}
|
||||
$uncompressedLen = strlen($json);
|
||||
$compressedLen = strlen($compressed) + 4;
|
||||
$version = 0x07C00100; // align with working payload header (big-endian)
|
||||
$compressedLen = strlen($compressed) + 4; // +4 for the uncompressed length field
|
||||
$version = 0x07C00100; // Amnezia magic version number
|
||||
$header = pack('N3', $version, $compressedLen, $uncompressedLen);
|
||||
return self::urlsafe_b64_encode($header . $compressed);
|
||||
}
|
||||
|
||||
public static function encodeOldPayloadFromConf(string $confText): string {
|
||||
public static function encodeOldPayloadFromConf(string $confText): string
|
||||
{
|
||||
$payload = self::buildOldEnvelopeFromConf($confText);
|
||||
return self::encodeOldPayloadFromJson(json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||
}
|
||||
|
||||
private static function resolveServerDescription(?string $endpointHost): string {
|
||||
$desc = (string)($endpointHost ?? '');
|
||||
private static function resolveServerDescription(?string $endpointHost): string
|
||||
{
|
||||
$desc = (string) ($endpointHost ?? '');
|
||||
try {
|
||||
$cfgPath = __DIR__ . '/config.php';
|
||||
$dbPath = __DIR__ . '/Database.php';
|
||||
@@ -98,21 +103,32 @@ class QrUtil {
|
||||
return $desc;
|
||||
}
|
||||
|
||||
private static function buildOldEnvelopeFromConf(string $conf): array {
|
||||
$endpointHost = null; $endpointPort = null; $mtu = null; $dns = []; $keepAlive = null;
|
||||
$privKey = null; $pubKeyServer = null; $psk = null; $address = null; $allowedIps = [];
|
||||
public static function parseWireGuardConfig(string $conf): array
|
||||
{
|
||||
$endpointHost = null;
|
||||
$endpointPort = null;
|
||||
$mtu = null;
|
||||
$dns = [];
|
||||
$keepAlive = null;
|
||||
$privKey = null;
|
||||
$pubKeyServer = null;
|
||||
$psk = null;
|
||||
$address = null;
|
||||
$allowedIps = [];
|
||||
foreach (explode("\n", $conf) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') { continue; }
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
if (stripos($line, 'Endpoint') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
if (preg_match('/^\[?([^\]]+)\]?:([0-9]{2,5})$/', $v, $m)) {
|
||||
$endpointHost = $m[1];
|
||||
$endpointPort = (int)$m[2];
|
||||
$endpointPort = (int) $m[2];
|
||||
}
|
||||
} elseif (stripos($line, 'MTU') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$mtu = (int)$v;
|
||||
$mtu = (int) $v;
|
||||
} elseif (stripos($line, 'DNS') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$dns = array_map('trim', preg_split('/[,\s]+/', $v));
|
||||
@@ -133,13 +149,19 @@ class QrUtil {
|
||||
$allowedIps = array_map('trim', preg_split('/[,\s]+/', $v));
|
||||
} elseif (stripos($line, 'PersistentKeepalive') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$keepAlive = (int)$v;
|
||||
$keepAlive = (int) $v;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$endpointPort) { $endpointPort = 51820; }
|
||||
if (!$mtu) { $mtu = 1280; }
|
||||
if (!$keepAlive) { $keepAlive = 25; }
|
||||
if (!$endpointPort) {
|
||||
$endpointPort = 51820;
|
||||
}
|
||||
if (!$mtu) {
|
||||
$mtu = 1280;
|
||||
}
|
||||
if (!$keepAlive) {
|
||||
$keepAlive = 25;
|
||||
}
|
||||
$dns1 = $dns[0] ?? '1.1.1.1';
|
||||
$dns2 = $dns[1] ?? '1.0.0.1';
|
||||
|
||||
@@ -155,9 +177,15 @@ class QrUtil {
|
||||
|
||||
// Collect obfuscation params from conf if present
|
||||
$params = [
|
||||
'H1' => null, 'H2' => null, 'H3' => null, 'H4' => null,
|
||||
'Jc' => null, 'Jmin' => null, 'Jmax' => null,
|
||||
'S1' => null, 'S2' => null,
|
||||
'H1' => null,
|
||||
'H2' => null,
|
||||
'H3' => null,
|
||||
'H4' => null,
|
||||
'Jc' => null,
|
||||
'Jmin' => null,
|
||||
'Jmax' => null,
|
||||
'S1' => null,
|
||||
'S2' => null,
|
||||
];
|
||||
foreach (explode("\n", $conf) as $line) {
|
||||
$line = trim($line);
|
||||
@@ -171,27 +199,173 @@ class QrUtil {
|
||||
|
||||
// Build last_config JSON object (stringified, pretty-printed)
|
||||
$lastConfigObj = [
|
||||
'H1' => (string)($params['H1'] ?? ''),
|
||||
'H2' => (string)($params['H2'] ?? ''),
|
||||
'H3' => (string)($params['H3'] ?? ''),
|
||||
'H4' => (string)($params['H4'] ?? ''),
|
||||
'Jc' => (string)($params['Jc'] ?? ''),
|
||||
'Jmax' => (string)($params['Jmax'] ?? ''),
|
||||
'Jmin' => (string)($params['Jmin'] ?? ''),
|
||||
'S1' => (string)($params['S1'] ?? ''),
|
||||
'S2' => (string)($params['S2'] ?? ''),
|
||||
'H1' => (string) ($params['H1'] ?? ''),
|
||||
'H2' => (string) ($params['H2'] ?? ''),
|
||||
'H3' => (string) ($params['H3'] ?? ''),
|
||||
'H4' => (string) ($params['H4'] ?? ''),
|
||||
'Jc' => (string) ($params['Jc'] ?? ''),
|
||||
'Jmax' => (string) ($params['Jmax'] ?? ''),
|
||||
'Jmin' => (string) ($params['Jmin'] ?? ''),
|
||||
'S1' => (string) ($params['S1'] ?? ''),
|
||||
'S2' => (string) ($params['S2'] ?? ''),
|
||||
'allowed_ips' => $allowedIps ?: ['0.0.0.0/0', '::/0'],
|
||||
'clientId' => $clientPubKey ?: '',
|
||||
'client_ip' => preg_replace('/\/(\d{1,2})$/', '', (string)($address ?? '')),
|
||||
'client_priv_key' => (string)($privKey ?? ''),
|
||||
'client_ip' => preg_replace('/\/(\d{1,2})$/', '', (string) ($address ?? '')),
|
||||
'client_priv_key' => (string) ($privKey ?? ''),
|
||||
'client_pub_key' => $clientPubKey ?: '',
|
||||
'config' => $conf,
|
||||
'hostName' => (string)($endpointHost ?? ''),
|
||||
'mtu' => (string)$mtu,
|
||||
'persistent_keep_alive' => (string)$keepAlive,
|
||||
'hostName' => (string) ($endpointHost ?? ''),
|
||||
'mtu' => (string) $mtu,
|
||||
'persistent_keep_alive' => (string) $keepAlive,
|
||||
'port' => $endpointPort,
|
||||
'psk_key' => (string)($psk ?? ''),
|
||||
'server_pub_key' => (string)($pubKeyServer ?? ''),
|
||||
'psk_key' => (string) ($psk ?? ''),
|
||||
'server_pub_key' => (string) ($pubKeyServer ?? ''),
|
||||
];
|
||||
|
||||
$serverDesc = self::resolveServerDescription($endpointHost);
|
||||
|
||||
$vars = [
|
||||
'last_config_json' => json_encode($lastConfigObj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
|
||||
'port' => (string) $endpointPort,
|
||||
'description' => $serverDesc,
|
||||
'dns1' => $dns1,
|
||||
'dns2' => $dns2,
|
||||
'hostName' => $endpointHost,
|
||||
'client_pub_key' => $clientPubKey,
|
||||
'client_priv_key' => $privKey,
|
||||
'client_ip' => preg_replace('/\/(\d{1,2})$/', '', (string) ($address ?? '')),
|
||||
'psk_key' => $psk,
|
||||
'server_pub_key' => $pubKeyServer,
|
||||
'mtu' => $mtu,
|
||||
'persistent_keep_alive' => $keepAlive,
|
||||
'config' => $conf,
|
||||
];
|
||||
|
||||
// Add params to vars
|
||||
foreach ($params as $k => $v) {
|
||||
$vars[$k] = (string) ($v ?? '');
|
||||
}
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
private static function buildOldEnvelopeFromConf(string $conf): array
|
||||
{
|
||||
$endpointHost = null;
|
||||
$endpointPort = null;
|
||||
$mtu = null;
|
||||
$dns = [];
|
||||
$keepAlive = null;
|
||||
$privKey = null;
|
||||
$pubKeyServer = null;
|
||||
$psk = null;
|
||||
$address = null;
|
||||
$allowedIps = [];
|
||||
foreach (explode("\n", $conf) as $line) {
|
||||
$line = trim($line);
|
||||
if ($line === '' || $line[0] === '#') {
|
||||
continue;
|
||||
}
|
||||
if (stripos($line, 'Endpoint') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
if (preg_match('/^\[?([^\]]+)\]?:([0-9]{2,5})$/', $v, $m)) {
|
||||
$endpointHost = $m[1];
|
||||
$endpointPort = (int) $m[2];
|
||||
}
|
||||
} elseif (stripos($line, 'MTU') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$mtu = (int) $v;
|
||||
} elseif (stripos($line, 'DNS') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$dns = array_map('trim', preg_split('/[,\s]+/', $v));
|
||||
} elseif (stripos($line, 'PrivateKey') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$privKey = $v;
|
||||
} elseif (stripos($line, 'PublicKey') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$pubKeyServer = $v;
|
||||
} elseif (stripos($line, 'PresharedKey') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$psk = $v;
|
||||
} elseif (stripos($line, 'Address') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$address = $v;
|
||||
} elseif (stripos($line, 'AllowedIPs') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$allowedIps = array_map('trim', preg_split('/[,\s]+/', $v));
|
||||
} elseif (stripos($line, 'PersistentKeepalive') === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$keepAlive = (int) $v;
|
||||
}
|
||||
}
|
||||
|
||||
if (!$endpointPort) {
|
||||
$endpointPort = 51820;
|
||||
}
|
||||
if (!$mtu) {
|
||||
$mtu = 1280;
|
||||
}
|
||||
if (!$keepAlive) {
|
||||
$keepAlive = 25;
|
||||
}
|
||||
$dns1 = $dns[0] ?? '1.1.1.1';
|
||||
$dns2 = $dns[1] ?? '1.0.0.1';
|
||||
|
||||
// Derive client public key if sodium available
|
||||
$clientPubKey = '';
|
||||
if ($privKey && function_exists('sodium_crypto_scalarmult_base')) {
|
||||
$bin = base64_decode($privKey, true);
|
||||
if ($bin !== false && strlen($bin) === 32) {
|
||||
$pub = sodium_crypto_scalarmult_base($bin);
|
||||
$clientPubKey = base64_encode($pub);
|
||||
}
|
||||
}
|
||||
|
||||
// Collect obfuscation params from conf if present
|
||||
$params = [
|
||||
'H1' => null,
|
||||
'H2' => null,
|
||||
'H3' => null,
|
||||
'H4' => null,
|
||||
'Jc' => null,
|
||||
'Jmin' => null,
|
||||
'Jmax' => null,
|
||||
'S1' => null,
|
||||
'S2' => null,
|
||||
];
|
||||
foreach (explode("\n", $conf) as $line) {
|
||||
$line = trim($line);
|
||||
foreach (array_keys($params) as $k) {
|
||||
if (stripos($line, $k) === 0 && strpos($line, '=') !== false) {
|
||||
[, $v] = array_map('trim', explode('=', $line, 2));
|
||||
$params[$k] = $v;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build last_config JSON object (stringified, pretty-printed)
|
||||
$lastConfigObj = [
|
||||
'H1' => (string) ($params['H1'] ?? ''),
|
||||
'H2' => (string) ($params['H2'] ?? ''),
|
||||
'H3' => (string) ($params['H3'] ?? ''),
|
||||
'H4' => (string) ($params['H4'] ?? ''),
|
||||
'Jc' => (string) ($params['Jc'] ?? ''),
|
||||
'Jmax' => (string) ($params['Jmax'] ?? ''),
|
||||
'Jmin' => (string) ($params['Jmin'] ?? ''),
|
||||
'S1' => (string) ($params['S1'] ?? ''),
|
||||
'S2' => (string) ($params['S2'] ?? ''),
|
||||
'allowed_ips' => $allowedIps ?: ['0.0.0.0/0', '::/0'],
|
||||
'clientId' => $clientPubKey ?: '',
|
||||
'client_ip' => preg_replace('/\/(\d{1,2})$/', '', (string) ($address ?? '')),
|
||||
'client_priv_key' => (string) ($privKey ?? ''),
|
||||
'client_pub_key' => $clientPubKey ?: '',
|
||||
'config' => $conf,
|
||||
'hostName' => (string) ($endpointHost ?? ''),
|
||||
'mtu' => (string) $mtu,
|
||||
'persistent_keep_alive' => (string) $keepAlive,
|
||||
'port' => $endpointPort,
|
||||
'psk_key' => (string) ($psk ?? ''),
|
||||
'server_pub_key' => (string) ($pubKeyServer ?? ''),
|
||||
];
|
||||
|
||||
$serverDesc = self::resolveServerDescription($endpointHost);
|
||||
@@ -202,17 +376,17 @@ class QrUtil {
|
||||
[
|
||||
// awg first, then container (as in the working QR)
|
||||
'awg' => [
|
||||
'H1' => (string)($params['H1'] ?? ''),
|
||||
'H2' => (string)($params['H2'] ?? ''),
|
||||
'H3' => (string)($params['H3'] ?? ''),
|
||||
'H4' => (string)($params['H4'] ?? ''),
|
||||
'Jc' => (string)($params['Jc'] ?? ''),
|
||||
'Jmax' => (string)($params['Jmax'] ?? ''),
|
||||
'Jmin' => (string)($params['Jmin'] ?? ''),
|
||||
'S1' => (string)($params['S1'] ?? ''),
|
||||
'S2' => (string)($params['S2'] ?? ''),
|
||||
'H1' => (string) ($params['H1'] ?? ''),
|
||||
'H2' => (string) ($params['H2'] ?? ''),
|
||||
'H3' => (string) ($params['H3'] ?? ''),
|
||||
'H4' => (string) ($params['H4'] ?? ''),
|
||||
'Jc' => (string) ($params['Jc'] ?? ''),
|
||||
'Jmax' => (string) ($params['Jmax'] ?? ''),
|
||||
'Jmin' => (string) ($params['Jmin'] ?? ''),
|
||||
'S1' => (string) ($params['S1'] ?? ''),
|
||||
'S2' => (string) ($params['S2'] ?? ''),
|
||||
'last_config' => json_encode($lastConfigObj, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
|
||||
'port' => (string)$endpointPort,
|
||||
'port' => (string) $endpointPort,
|
||||
'transport_proto' => 'udp',
|
||||
],
|
||||
'container' => 'amnezia-awg',
|
||||
@@ -227,9 +401,87 @@ class QrUtil {
|
||||
return $envelope;
|
||||
}
|
||||
|
||||
private static function normalizeJson(string $text): string {
|
||||
private static function normalizeJson(string $text): string
|
||||
{
|
||||
$decoded = json_decode($text, true);
|
||||
if (!is_array($decoded)) throw new InvalidArgumentException('Invalid JSON');
|
||||
if (!is_array($decoded))
|
||||
throw new InvalidArgumentException('Invalid JSON');
|
||||
return json_encode($decoded, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
|
||||
}
|
||||
|
||||
public static function encodeXrayPayload(string $host, int $port, string $clientId, string $description = '', ?array $reality = null, string $rawConfig = '', string $flow = ''): string
|
||||
{
|
||||
$desc = $description !== '' ? $description : self::resolveServerDescription($host);
|
||||
|
||||
// Construct full Client XRay config (Amnezia native format expects this structure)
|
||||
$outbound = [
|
||||
'protocol' => 'vless',
|
||||
'settings' => [
|
||||
'vnext' => [
|
||||
[
|
||||
'address' => $host,
|
||||
'port' => $port,
|
||||
'users' => [
|
||||
[
|
||||
'id' => $clientId,
|
||||
'encryption' => 'none'
|
||||
]
|
||||
]
|
||||
]
|
||||
]
|
||||
],
|
||||
'streamSettings' => [
|
||||
'network' => 'tcp',
|
||||
'security' => ($reality ? 'reality' : 'none')
|
||||
]
|
||||
];
|
||||
|
||||
if ($flow !== '') {
|
||||
$outbound['settings']['vnext'][0]['users'][0]['flow'] = $flow;
|
||||
}
|
||||
|
||||
if ($reality) {
|
||||
$outbound['streamSettings']['realitySettings'] = [
|
||||
'fingerprint' => $reality['fingerprint'] ?? 'chrome',
|
||||
'serverName' => $reality['serverName'] ?? '',
|
||||
'publicKey' => $reality['publicKey'] ?? '',
|
||||
'shortId' => $reality['shortId'] ?? '',
|
||||
'spiderX' => ''
|
||||
];
|
||||
}
|
||||
|
||||
$fullConfig = [
|
||||
'log' => ['loglevel' => 'warning'],
|
||||
'inbounds' => [
|
||||
[
|
||||
'listen' => '127.0.0.1',
|
||||
'port' => 10808,
|
||||
'protocol' => 'socks',
|
||||
'settings' => ['udp' => true]
|
||||
]
|
||||
],
|
||||
'outbounds' => [$outbound]
|
||||
];
|
||||
|
||||
$envelope = [
|
||||
'containers' => [
|
||||
[
|
||||
'xray' => [
|
||||
// No isThirdPartyConfig flag - treats as native container
|
||||
'last_config' => json_encode($fullConfig, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT),
|
||||
'port' => (string) $port,
|
||||
'transport_proto' => 'tcp'
|
||||
],
|
||||
'container' => 'amnezia-xray'
|
||||
]
|
||||
],
|
||||
'defaultContainer' => 'amnezia-xray',
|
||||
'description' => $desc,
|
||||
'dns1' => '1.1.1.1',
|
||||
'dns2' => '1.0.0.1',
|
||||
'hostName' => $host,
|
||||
];
|
||||
|
||||
return self::encodeOldPayloadFromJson(json_encode($envelope, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT));
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ class Router {
|
||||
}
|
||||
public static function get(string $pattern, callable $handler): void { self::add('GET', $pattern, $handler); }
|
||||
public static function post(string $pattern, callable $handler): void { self::add('POST', $pattern, $handler); }
|
||||
public static function put(string $pattern, callable $handler): void { self::add('PUT', $pattern, $handler); }
|
||||
public static function delete(string $pattern, callable $handler): void { self::add('DELETE', $pattern, $handler); }
|
||||
|
||||
private static function normalizePattern(string $pattern): string {
|
||||
|
||||
+967
-97
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
use Twig\TwigFunction;
|
||||
use Twig\TwigFilter;
|
||||
|
||||
class View {
|
||||
private static ?Environment $twig = null;
|
||||
@@ -36,6 +37,22 @@ class View {
|
||||
});
|
||||
self::$twig->addFunction($flagFunc);
|
||||
|
||||
// Add bytes format filter
|
||||
$bytesFilter = new TwigFilter('bytes_format', function (int $bytes, int $precision = 2): string {
|
||||
$units = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
|
||||
$bytes /= 1024;
|
||||
}
|
||||
return round($bytes, $precision) . ' ' . $units[$i];
|
||||
});
|
||||
self::$twig->addFilter($bytesFilter);
|
||||
|
||||
// Add translation filter (alias: trans)
|
||||
$transFilter = new TwigFilter('trans', function (string $key, array $params = []) {
|
||||
return Translator::t($key, $params);
|
||||
});
|
||||
self::$twig->addFilter($transFilter);
|
||||
|
||||
// Add globals
|
||||
foreach ($globals as $k => $v) self::$twig->addGlobal($k, $v);
|
||||
}
|
||||
|
||||
+1727
-292
File diff suppressed because it is too large
Load Diff
+502
-151
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,788 @@
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en','servers','backup_upload_type','Backup type'),
|
||||
('en','servers','backup_type_auto','Auto detect'),
|
||||
('en','servers','backup_type_amnezia','Amnezia app (.backup)'),
|
||||
('en','servers','backup_type_panel','Panel export (.json)'),
|
||||
('en','servers','backup_upload_hint','Upload a .backup or .json file. After upload, pick a server entry above.'),
|
||||
('ru','servers','backup_upload_type','Тип бэкапа'),
|
||||
('ru','servers','backup_type_auto','Определить автоматически'),
|
||||
('ru','servers','backup_type_amnezia','Приложение Amnezia (.backup)'),
|
||||
('ru','servers','backup_type_panel','Экспорт панели (.json)'),
|
||||
('ru','servers','backup_upload_hint','Загрузите файл .backup или .json. После загрузки выберите сервер выше.'),
|
||||
('es','servers','backup_upload_type','Tipo de copia de seguridad'),
|
||||
('es','servers','backup_type_auto','Detectar automáticamente'),
|
||||
('es','servers','backup_type_amnezia','Aplicación Amnezia (.backup)'),
|
||||
('es','servers','backup_type_panel','Exportación del panel (.json)'),
|
||||
('es','servers','backup_upload_hint','Suba un archivo .backup o .json. Después seleccione el servidor arriba.'),
|
||||
('de','servers','backup_upload_type','Backup-Typ'),
|
||||
('de','servers','backup_type_auto','Automatisch erkennen'),
|
||||
('de','servers','backup_type_amnezia','Amnezia-App (.backup)'),
|
||||
('de','servers','backup_type_panel','Panel-Export (.json)'),
|
||||
('de','servers','backup_upload_hint','Laden Sie eine .backup- oder .json-Datei hoch. Wählen Sie anschließend oben einen Server aus.'),
|
||||
('fr','servers','backup_upload_type','Type de sauvegarde'),
|
||||
('fr','servers','backup_type_auto','Détection automatique'),
|
||||
('fr','servers','backup_type_amnezia','Application Amnezia (.backup)'),
|
||||
('fr','servers','backup_type_panel','Export du panneau (.json)'),
|
||||
('fr','servers','backup_upload_hint','Téléversez un fichier .backup ou .json, puis sélectionnez un serveur ci-dessus.'),
|
||||
('zh','servers','backup_upload_type','备份类型'),
|
||||
('zh','servers','backup_type_auto','自动检测'),
|
||||
('zh','servers','backup_type_amnezia','Amnezia 应用 (.backup)'),
|
||||
('zh','servers','backup_type_panel','面板导出 (.json)'),
|
||||
('zh','servers','backup_upload_hint','上传 .backup 或 .json 文件,随后在上方选择服务器。')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en', 'servers', 'config_import_title', 'Import configuration'),
|
||||
('en', 'servers', 'config_import_hint', 'Upload a configuration backup to update this server and its clients.'),
|
||||
('en', 'servers', 'config_import_type_label', 'Backup type'),
|
||||
('en', 'servers', 'config_import_type_panel', 'Panel backup (.json)'),
|
||||
('en', 'servers', 'config_import_type_amnezia', 'Amnezia app backup (.backup)'),
|
||||
('en', 'servers', 'config_import_file_label', 'Configuration file'),
|
||||
('en', 'servers', 'config_import_file_hint', 'Our panel uses .json files. The Amnezia app uses .backup files.'),
|
||||
('en', 'servers', 'config_import_submit', 'Import configuration'),
|
||||
('ru', 'servers', 'config_import_title', 'Импорт конфигурации'),
|
||||
('ru', 'servers', 'config_import_hint', 'Загрузите файл бэкапа, чтобы обновить настройки сервера и список клиентов.'),
|
||||
('ru', 'servers', 'config_import_type_label', 'Источник бэкапа'),
|
||||
('ru', 'servers', 'config_import_type_panel', 'Бэкап панели (.json)'),
|
||||
('ru', 'servers', 'config_import_type_amnezia', 'Бэкап приложения Amnezia (.backup)'),
|
||||
('ru', 'servers', 'config_import_file_label', 'Файл конфигурации'),
|
||||
('ru', 'servers', 'config_import_file_hint', 'Панель использует файлы .json. Приложение Amnezia — файлы .backup.'),
|
||||
('ru', 'servers', 'config_import_submit', 'Импортировать конфигурацию'),
|
||||
('es', 'servers', 'config_import_title', 'Importar configuración'),
|
||||
('es', 'servers', 'config_import_hint', 'Cargue un respaldo para actualizar este servidor y sus clientes.'),
|
||||
('es', 'servers', 'config_import_type_label', 'Tipo de backup'),
|
||||
('es', 'servers', 'config_import_type_panel', 'Backup del panel (.json)'),
|
||||
('es', 'servers', 'config_import_type_amnezia', 'Backup de la app Amnezia (.backup)'),
|
||||
('es', 'servers', 'config_import_file_label', 'Archivo de configuración'),
|
||||
('es', 'servers', 'config_import_file_hint', 'El panel usa archivos .json. La app Amnezia usa archivos .backup.'),
|
||||
('es', 'servers', 'config_import_submit', 'Importar configuración'),
|
||||
('de', 'servers', 'config_import_title', 'Konfiguration importieren'),
|
||||
('de', 'servers', 'config_import_hint', 'Laden Sie eine Sicherung hoch, um diesen Server und seine Clients zu aktualisieren.'),
|
||||
('de', 'servers', 'config_import_type_label', 'Backup-Typ'),
|
||||
('de', 'servers', 'config_import_type_panel', 'Panel-Backup (.json)'),
|
||||
('de', 'servers', 'config_import_type_amnezia', 'Amnezia-App-Backup (.backup)'),
|
||||
('de', 'servers', 'config_import_file_label', 'Konfigurationsfile'),
|
||||
('de', 'servers', 'config_import_file_hint', 'Die Panel-Backups sind .json. Die Amnezia-App nutzt .backup-Dateien.'),
|
||||
('de', 'servers', 'config_import_submit', 'Konfiguration importieren'),
|
||||
('fr', 'servers', 'config_import_title', 'Importer la configuration'),
|
||||
('fr', 'servers', 'config_import_hint', 'Téléversez un fichier de sauvegarde pour mettre à jour ce serveur et ses clients.'),
|
||||
('fr', 'servers', 'config_import_type_label', 'Type de sauvegarde'),
|
||||
('fr', 'servers', 'config_import_type_panel', 'Sauvegarde du panneau (.json)'),
|
||||
('fr', 'servers', 'config_import_type_amnezia', 'Sauvegarde de l’application Amnezia (.backup)'),
|
||||
('fr', 'servers', 'config_import_file_label', 'Fichier de configuration'),
|
||||
('fr', 'servers', 'config_import_file_hint', 'Notre panneau utilise des fichiers .json. L’application Amnezia utilise des fichiers .backup.'),
|
||||
('fr', 'servers', 'config_import_submit', 'Importer la configuration'),
|
||||
('zh', 'servers', 'config_import_title', '导入配置'),
|
||||
('zh', 'servers', 'config_import_hint', '上传备份文件以更新此服务器及其客户端。'),
|
||||
('zh', 'servers', 'config_import_type_label', '备份类型'),
|
||||
('zh', 'servers', 'config_import_type_panel', '面板备份 (.json)'),
|
||||
('zh', 'servers', 'config_import_type_amnezia', 'Amnezia 应用备份 (.backup)'),
|
||||
('zh', 'servers', 'config_import_file_label', '配置文件'),
|
||||
('zh', 'servers', 'config_import_file_hint', '面板使用 .json 文件,Amnezia 应用使用 .backup 文件。'),
|
||||
('zh', 'servers', 'config_import_submit', '导入配置')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en','servers','creation_mode','Creation mode'),
|
||||
('en','servers','creation_mode_manual','Manual setup'),
|
||||
('en','servers','creation_mode_backup','Import from backup'),
|
||||
('en','servers','upload_backup_file','Upload backup file'),
|
||||
('en','servers','backup_upload_hint','Supported formats: panel JSON export or Amnezia application .backup'),
|
||||
('en','servers','backup_server_entry','Select server entry'),
|
||||
('en','servers','backup_summary_host','Host'),
|
||||
('en','servers','backup_summary_clients','Clients'),
|
||||
('en','servers','config_import_title','Restore configuration from backup'),
|
||||
('en','servers','config_import_hint','Import server configuration (and optional clients) from a panel export or Amnezia application backup.'),
|
||||
('en','servers','config_import_type_label','Backup type'),
|
||||
('en','servers','config_import_type_panel','Panel export (.json)'),
|
||||
('en','servers','config_import_type_amnezia','Amnezia app backup (.backup)'),
|
||||
('en','servers','config_import_file_label','Configuration file'),
|
||||
('en','servers','config_import_file_hint','The file remains on the server only during import and is deleted afterwards.'),
|
||||
('en','servers','config_import_submit','Import configuration'),
|
||||
('ru','servers','creation_mode','Режим создания'),
|
||||
('ru','servers','creation_mode_manual','Ручная настройка'),
|
||||
('ru','servers','creation_mode_backup','Импорт из бэкапа'),
|
||||
('ru','servers','upload_backup_file','Загрузите файл бэкапа'),
|
||||
('ru','servers','backup_upload_hint','Поддерживаются форматы: экспорт панели JSON или бэкап приложения Amnezia (.backup)'),
|
||||
('ru','servers','backup_server_entry','Выберите запись сервера'),
|
||||
('ru','servers','backup_summary_host','Хост'),
|
||||
('ru','servers','backup_summary_clients','Клиенты'),
|
||||
('ru','servers','config_import_title','Восстановление конфигурации из бэкапа'),
|
||||
('ru','servers','config_import_hint','Импортируйте конфигурацию сервера (и при необходимости клиентов) из экспорта панели или бэкапа приложения Amnezia.'),
|
||||
('ru','servers','config_import_type_label','Тип бэкапа'),
|
||||
('ru','servers','config_import_type_panel','Экспорт панели (.json)'),
|
||||
('ru','servers','config_import_type_amnezia','Бэкап приложения Amnezia (.backup)'),
|
||||
('ru','servers','config_import_file_label','Файл конфигурации'),
|
||||
('ru','servers','config_import_file_hint','Файл хранится на сервере только во время импорта и удаляется сразу после завершения.'),
|
||||
('ru','servers','config_import_submit','Импортировать конфигурацию')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS protocols (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
slug VARCHAR(100) UNIQUE NOT NULL,
|
||||
description TEXT,
|
||||
install_script TEXT,
|
||||
output_template TEXT,
|
||||
ubuntu_compatible BOOLEAN DEFAULT false,
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_protocols_slug (slug),
|
||||
INDEX idx_protocols_active (is_active),
|
||||
INDEX idx_protocols_ubuntu_compatible (ubuntu_compatible)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS protocol_templates (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
protocol_id INT UNSIGNED NOT NULL,
|
||||
template_name VARCHAR(255) NOT NULL,
|
||||
template_content TEXT NOT NULL,
|
||||
is_default BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_protocol_templates_protocol (protocol_id),
|
||||
INDEX idx_protocol_templates_default (is_default),
|
||||
FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS protocol_variables (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
protocol_id INT UNSIGNED NOT NULL,
|
||||
variable_name VARCHAR(100) NOT NULL,
|
||||
variable_type VARCHAR(50) NOT NULL DEFAULT 'string',
|
||||
default_value TEXT,
|
||||
description TEXT,
|
||||
required BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_protocol_variables_protocol (protocol_id),
|
||||
INDEX idx_protocol_variables_name (variable_name),
|
||||
FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS server_protocols (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
server_id INT UNSIGNED NOT NULL,
|
||||
protocol_id INT UNSIGNED NOT NULL,
|
||||
config_data JSON,
|
||||
applied_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_server_protocols_server (server_id),
|
||||
INDEX idx_server_protocols_protocol (protocol_id),
|
||||
INDEX idx_server_protocols_applied (applied_at),
|
||||
UNIQUE KEY unique_server_protocol (server_id, protocol_id),
|
||||
FOREIGN KEY (server_id) REFERENCES vpn_servers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS ai_generations (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
protocol_id INT UNSIGNED NULL,
|
||||
model_used VARCHAR(100) NOT NULL,
|
||||
prompt TEXT NOT NULL,
|
||||
generated_script TEXT,
|
||||
suggestions JSON,
|
||||
ubuntu_compatible BOOLEAN,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_ai_generations_protocol (protocol_id),
|
||||
INDEX idx_ai_generations_model (model_used),
|
||||
INDEX idx_ai_generations_created (created_at DESC),
|
||||
FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active)
|
||||
SELECT 'AmneziaWG Advanced', 'amnezia-wg-advanced', 'AmneziaWG protocol with advanced junk packet obfuscation parameters', '#!/bin/bash
|
||||
echo "AmneziaWG Advanced installed"
|
||||
', '[Interface]
|
||||
PrivateKey = {{private_key}}
|
||||
Address = {{client_ip}}/32
|
||||
DNS = 8.8.8.8, 8.8.4.4
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{server_public_key}}
|
||||
PresharedKey = {{preshared_key}}
|
||||
Endpoint = {{server_host}}:{{server_port}}
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
PersistentKeepalive = 25
|
||||
|
||||
Jc = {{jc}}
|
||||
Jmin = {{jmin}}
|
||||
Jmax = {{jmax}}
|
||||
S1 = {{s1}}
|
||||
S2 = {{s2}}
|
||||
H1 = {{h1}}
|
||||
H2 = {{h2}}
|
||||
H3 = {{h3}}
|
||||
H4 = {{h4}}', true, true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='amnezia-wg-advanced');
|
||||
|
||||
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active)
|
||||
SELECT 'WireGuard Standard', 'wireguard-standard', 'Standard WireGuard VPN protocol', '#!/bin/bash
|
||||
CONTAINER_NAME="wireguard"
|
||||
VPN_SUBNET="10.8.2.0/24"
|
||||
PRIVATE_KEY=$(wg genkey)
|
||||
PUBLIC_KEY=$(echo $PRIVATE_KEY | wg pubkey)
|
||||
PRESHARED_KEY=$(wg genpsk)
|
||||
docker run -d \
|
||||
--name $CONTAINER_NAME \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=SYS_MODULE \
|
||||
-v /opt/wireguard:/etc/wireguard \
|
||||
linuxserver/wireguard
|
||||
cat > /opt/wireguard/wg0.conf << EOF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.2.1/24
|
||||
ListenPort = 51820
|
||||
|
||||
[Peer]
|
||||
PublicKey =
|
||||
PresharedKey = $PRESHARED_KEY
|
||||
AllowedIPs = 10.8.2.2/32
|
||||
EOF
|
||||
echo "WireGuard Standard installed successfully"
|
||||
echo "Server Public Key: $PUBLIC_KEY"', '[Interface]
|
||||
PrivateKey = {{private_key}}
|
||||
Address = {{client_ip}}/32
|
||||
DNS = 8.8.8.8, 8.8.4.4
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{server_public_key}}
|
||||
PresharedKey = {{preshared_key}}
|
||||
Endpoint = {{server_host}}:{{server_port}}
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
PersistentKeepalive = 25', true, true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='wireguard-standard');
|
||||
|
||||
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active)
|
||||
SELECT 'OpenVPN', 'openvpn', 'OpenVPN protocol with TCP/UDP support', '#!/bin/bash
|
||||
CONTAINER_NAME="openvpn"
|
||||
VPN_SUBNET="10.8.3.0/24"
|
||||
docker run -d \
|
||||
--name $CONTAINER_NAME \
|
||||
--cap-add=NET_ADMIN \
|
||||
-p 1194:1194/udp \
|
||||
-p 1194:1194/tcp \
|
||||
-v /opt/openvpn:/etc/openvpn \
|
||||
kylemanna/openvpn
|
||||
docker exec -it $CONTAINER_NAME ovpn_genconfig -u udp://{{server_host}}:1194
|
||||
docker exec -it $CONTAINER_NAME ovpn_initpki
|
||||
echo "OpenVPN installed successfully"
|
||||
echo "Available on ports: 1194/udp, 1194/tcp"', 'client
|
||||
dev tun
|
||||
proto {{protocol}}
|
||||
remote {{server_host}} {{server_port}}
|
||||
resolv-retry infinite
|
||||
nobind
|
||||
persist-key
|
||||
persist-tun
|
||||
ca ca.crt
|
||||
cert client.crt
|
||||
key client.key
|
||||
remote-cert-tls server
|
||||
cipher AES-256-GCM
|
||||
auth SHA256
|
||||
verb 3', true, true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='openvpn');
|
||||
|
||||
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active)
|
||||
SELECT 'Shadowsocks', 'shadowsocks', 'Shadowsocks proxy protocol', '#!/bin/bash
|
||||
CONTAINER_NAME="shadowsocks"
|
||||
PASSWORD=$(openssl rand -base64 12)
|
||||
docker run -d \
|
||||
--name $CONTAINER_NAME \
|
||||
-p 8388:8388 \
|
||||
-e METHOD=aes-256-gcm \
|
||||
-e PASSWORD=$PASSWORD \
|
||||
shadowsocks/shadowsocks-libev
|
||||
echo "Shadowsocks installed successfully"
|
||||
echo "Port: 8388"
|
||||
echo "Method: aes-256-gcm"
|
||||
echo "Password: $PASSWORD"', '{
|
||||
"server": "{{server_host}}",
|
||||
"server_port": {{server_port}},
|
||||
"password": "{{password}}",
|
||||
"method": "{{method}}"
|
||||
}', true, true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='shadowsocks');
|
||||
|
||||
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
|
||||
SELECT p.id, 'Default AmneziaWG', '[Interface]
|
||||
PrivateKey = {{private_key}}
|
||||
Address = {{client_ip}}/32
|
||||
DNS = 8.8.8.8, 8.8.4.4
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{server_public_key}}
|
||||
PresharedKey = {{preshared_key}}
|
||||
Endpoint = {{server_host}}:{{server_port}}
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
PersistentKeepalive = 25
|
||||
|
||||
Jc = {{jc}}
|
||||
Jmin = {{jmin}}
|
||||
Jmax = {{jmax}}
|
||||
S1 = {{s1}}
|
||||
S2 = {{s2}}
|
||||
H1 = {{h1}}
|
||||
H2 = {{h2}}
|
||||
H3 = {{h3}}
|
||||
H4 = {{h4}}', true
|
||||
FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id=p.id AND template_name='Default AmneziaWG');
|
||||
|
||||
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
|
||||
SELECT p.id, 'Default WireGuard', '[Interface]
|
||||
PrivateKey = {{private_key}}
|
||||
Address = {{client_ip}}/32
|
||||
DNS = 8.8.8.8, 8.8.4.4
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{server_public_key}}
|
||||
PresharedKey = {{preshared_key}}
|
||||
Endpoint = {{server_host}}:{{server_port}}
|
||||
AllowedIPs = 0.0.0.0/0
|
||||
PersistentKeepalive = 25', true
|
||||
FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id=p.id AND template_name='Default WireGuard');
|
||||
|
||||
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
|
||||
SELECT p.id, 'Default OpenVPN', 'client
|
||||
dev tun
|
||||
proto {{protocol}}
|
||||
remote {{server_host}} {{server_port}}
|
||||
resolv-retry infinite
|
||||
nobind
|
||||
persist-key
|
||||
persist-tun
|
||||
ca ca.crt
|
||||
cert client.crt
|
||||
key client.key
|
||||
remote-cert-tls server
|
||||
cipher AES-256-GCM
|
||||
auth SHA256
|
||||
verb 3', true
|
||||
FROM protocols p WHERE p.slug='openvpn' AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id=p.id AND template_name='Default OpenVPN');
|
||||
|
||||
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
|
||||
SELECT p.id, 'Default Shadowsocks', '{
|
||||
"server": "{{server_host}}",
|
||||
"server_port": {{server_port}},
|
||||
"password": "{{password}}",
|
||||
"method": "{{method}}"
|
||||
}', true
|
||||
FROM protocols p WHERE p.slug='shadowsocks' AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id=p.id AND template_name='Default Shadowsocks');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'private_key', 'string', '', 'Client private key', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='private_key');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'client_ip', 'string', '10.8.1.2', 'Client IP address', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='client_ip');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_public_key', 'string', '', 'Server public key', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_public_key');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'preshared_key', 'string', '', 'Pre-shared key for additional security', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='preshared_key');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_host');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_port', 'number', '51820', 'Server port', true FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_port');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'jc', 'number', '4', 'Junk packet count', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='jc');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'jmin', 'number', '50', 'Minimum junk packet size', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='jmin');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'jmax', 'number', '1000', 'Maximum junk packet size', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='jmax');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 's1', 'number', '148', 'Junk packet size 1', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='s1');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 's2', 'number', '450', 'Junk packet size 2', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='s2');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'h1', 'number', '320121696', 'Junk packet header 1', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='h1');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'h2', 'number', '51525354', 'Junk packet header 2', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='h2');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'h3', 'number', '13141516', 'Junk packet header 3', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='h3');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'h4', 'number', '92435495', 'Junk packet header 4', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='h4');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'private_key', 'string', '', 'Client private key', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='private_key');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'client_ip', 'string', '10.8.2.2', 'Client IP address', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='client_ip');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_public_key', 'string', '', 'Server public key', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_public_key');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'preshared_key', 'string', '', 'Pre-shared key for additional security', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='preshared_key');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_host');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_port', 'number', '51820', 'Server port', true FROM protocols p WHERE p.slug='wireguard-standard' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_port');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'protocol', 'string', 'udp', 'Connection protocol (udp/tcp)', true FROM protocols p WHERE p.slug='openvpn' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='protocol');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true FROM protocols p WHERE p.slug='openvpn' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_host');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_port', 'number', '1194', 'Server port', true FROM protocols p WHERE p.slug='openvpn' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_port');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true FROM protocols p WHERE p.slug='shadowsocks' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_host');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_port', 'number', '8388', 'Server port', true FROM protocols p WHERE p.slug='shadowsocks' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='server_port');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'password', 'string', '', 'Connection password', true FROM protocols p WHERE p.slug='shadowsocks' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='password');
|
||||
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en','common','cancel','Cancel'),
|
||||
('ru','common','cancel','Отмена'),
|
||||
('es','common','cancel','Cancelar'),
|
||||
('de','common','cancel','Abbrechen'),
|
||||
('fr','common','cancel','Annuler'),
|
||||
('zh','common','cancel','取消'),
|
||||
('en','common','format','Format'),
|
||||
('ru','common','format','Форматировать'),
|
||||
('es','common','format','Formatear'),
|
||||
('de','common','format','Formatieren'),
|
||||
('fr','common','format','Formater'),
|
||||
('zh','common','format','格式化'),
|
||||
('en','common','clear','Clear'),
|
||||
('ru','common','clear','Очистить'),
|
||||
('es','common','clear','Borrar'),
|
||||
('de','common','clear','Leeren'),
|
||||
('fr','common','clear','Effacer'),
|
||||
('zh','common','clear','清空'),
|
||||
('en','protocols','template_editor_help','Use placeholders like {{variable}} and preview client output'),
|
||||
('ru','protocols','template_editor_help','Используйте плейсхолдеры вида {{variable}} и просматривайте вывод клиента'),
|
||||
('es','protocols','template_editor_help','Usa marcadores como {{variable}} y previsualiza la salida del cliente'),
|
||||
('de','protocols','template_editor_help','Verwenden Sie Platzhalter wie {{variable}} und sehen Sie die Client‑Ausgabe in der Vorschau'),
|
||||
('fr','protocols','template_editor_help','Utilisez des placeholders comme {{variable}} et prévisualisez la sortie client'),
|
||||
('zh','protocols','template_editor_help','使用如 {{variable}} 的占位符并预览客户端输出'),
|
||||
('en','protocols','variable_private_key_help','Client private key'),
|
||||
('ru','protocols','variable_private_key_help','Приватный ключ клиента'),
|
||||
('es','protocols','variable_private_key_help','Clave privada del cliente'),
|
||||
('de','protocols','variable_private_key_help','Privater Schlüssel des Clients'),
|
||||
('fr','protocols','variable_private_key_help','Clé privée du client'),
|
||||
('zh','protocols','variable_private_key_help','客户端私钥'),
|
||||
('en','protocols','variable_public_key_help','Server public key'),
|
||||
('ru','protocols','variable_public_key_help','Публичный ключ сервера'),
|
||||
('es','protocols','variable_public_key_help','Clave pública del servidor'),
|
||||
('de','protocols','variable_public_key_help','Öffentlicher Schlüssel des Servers'),
|
||||
('fr','protocols','variable_public_key_help','Clé publique du serveur'),
|
||||
('zh','protocols','variable_public_key_help','服务器公钥'),
|
||||
('en','protocols','variable_client_ip_help','Client IP address'),
|
||||
('ru','protocols','variable_client_ip_help','IP‑адрес клиента'),
|
||||
('es','protocols','variable_client_ip_help','Dirección IP del cliente'),
|
||||
('de','protocols','variable_client_ip_help','IP‑Adresse des Clients'),
|
||||
('fr','protocols','variable_client_ip_help','Adresse IP du client'),
|
||||
('zh','protocols','variable_client_ip_help','客户端 IP 地址'),
|
||||
('en','protocols','variable_server_host_help','VPN server host'),
|
||||
('ru','protocols','variable_server_host_help','Хост VPN‑сервера'),
|
||||
('es','protocols','variable_server_host_help','Host del servidor VPN'),
|
||||
('de','protocols','variable_server_host_help','VPN‑Server‑Host'),
|
||||
('fr','protocols','variable_server_host_help','Hôte du serveur VPN'),
|
||||
('zh','protocols','variable_server_host_help','VPN 服务器主机'),
|
||||
('en','protocols','variable_server_port_help','VPN server port'),
|
||||
('ru','protocols','variable_server_port_help','Порт VPN‑сервера'),
|
||||
('es','protocols','variable_server_port_help','Puerto del servidor VPN'),
|
||||
('de','protocols','variable_server_port_help','VPN‑Server‑Port'),
|
||||
('fr','protocols','variable_server_port_help','Port du serveur VPN'),
|
||||
('zh','protocols','variable_server_port_help','VPN 服务器端口'),
|
||||
('en','protocols','variable_preshared_key_help','WireGuard preshared key'),
|
||||
('ru','protocols','variable_preshared_key_help','Предварительно общий ключ WireGuard'),
|
||||
('es','protocols','variable_preshared_key_help','Clave precompartida de WireGuard'),
|
||||
('de','protocols','variable_preshared_key_help','WireGuard‑vorausgeteilter Schlüssel'),
|
||||
('fr','protocols','variable_preshared_key_help','Clé prépartagée WireGuard'),
|
||||
('zh','protocols','variable_preshared_key_help','WireGuard 预共享密钥')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
|
||||
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en','ai','enter_protocol_id_to_apply','Enter protocol ID to apply'),
|
||||
('ru','ai','enter_protocol_id_to_apply','Введите ID протокола для применения'),
|
||||
('es','ai','enter_protocol_id_to_apply','Introduce el ID de protocolo para aplicar'),
|
||||
('de','ai','enter_protocol_id_to_apply','Protokoll‑ID zum Anwenden eingeben'),
|
||||
('fr','ai','enter_protocol_id_to_apply','Saisissez l’ID du protocole à appliquer'),
|
||||
('zh','ai','enter_protocol_id_to_apply','输入要应用的协议 ID'),
|
||||
('en','ai','improve_protocol','Improve protocol script for'),
|
||||
('ru','ai','improve_protocol','Улучшить скрипт протокола для'),
|
||||
('es','ai','improve_protocol','Mejorar script del protocolo для'),
|
||||
('de','ai','improve_protocol','Protokollskript verbessern für'),
|
||||
('fr','ai','improve_protocol','Améliorer le script du protocole pour'),
|
||||
('zh','ai','improve_protocol','改进协议脚本:'),
|
||||
('en','protocols','enter_protocol_name','Enter protocol name'),
|
||||
('ru','protocols','enter_protocol_name','Введите имя протокола'),
|
||||
('es','protocols','enter_protocol_name','Introduce el nombre del protocolo'),
|
||||
('de','protocols','enter_protocol_name','Protokollnamen eingeben'),
|
||||
('fr','protocols','enter_protocol_name','Saisissez le nom du protocole'),
|
||||
('zh','protocols','enter_protocol_name','输入协议名称'),
|
||||
('en','protocols','enter_protocol_slug','Enter protocol slug'),
|
||||
('ru','protocols','enter_protocol_slug','Введите slug протокола'),
|
||||
('es','protocols','enter_protocol_slug','Introduce el slug del protocolo'),
|
||||
('de','protocols','enter_protocol_slug','Protokoll‑Slug eingeben'),
|
||||
('fr','protocols','enter_protocol_slug','Saisissez le slug du protocole'),
|
||||
('zh','protocols','enter_protocol_slug','输入协议 slug'),
|
||||
('en','protocols','protocol_created_successfully','Protocol created successfully'),
|
||||
('ru','protocols','protocol_created_successfully','Протокол успешно создан'),
|
||||
('es','protocols','protocol_created_successfully','Protocolo creado correctamente'),
|
||||
('de','protocols','protocol_created_successfully','Protokoll erfolgreich erstellt'),
|
||||
('fr','protocols','protocol_created_successfully','Protocole créé avec succès'),
|
||||
('zh','protocols','protocol_created_successfully','协议创建成功'),
|
||||
('en','protocols','error_creating_protocol','Error creating protocol'),
|
||||
('ru','protocols','error_creating_protocol','Ошибка создания протокола'),
|
||||
('es','protocols','error_creating_protocol','Error al crear el protocolo'),
|
||||
('de','protocols','error_creating_protocol','Fehler beim Erstellen des Protokolls'),
|
||||
('fr','protocols','error_creating_protocol','Erreur lors de la création du protocole'),
|
||||
('zh','protocols','error_creating_protocol','创建协议时出错')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en','settings','protocols','Protocols'),
|
||||
('ru','settings','protocols','Протоколы'),
|
||||
('es','settings','protocols','Protocolos'),
|
||||
('de','settings','protocols','Protokolle'),
|
||||
('fr','settings','protocols','Protocoles'),
|
||||
('zh','settings','protocols','协议'),
|
||||
('en','settings','protocol_management','Protocol Management'),
|
||||
('ru','settings','protocol_management','Управление протоколами'),
|
||||
('es','settings','protocol_management','Gestión de protocolos'),
|
||||
('de','settings','protocol_management','Protokollverwaltung'),
|
||||
('fr','settings','protocol_management','Gestion des protocoles'),
|
||||
('zh','settings','protocol_management','协议管理')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en','protocols','test_install','Test install'),
|
||||
('ru','protocols','test_install','Протестировать установку'),
|
||||
('es','protocols','test_install','Probar instalación'),
|
||||
('de','protocols','test_install','Installation testen'),
|
||||
('fr','protocols','test_install','Tester l’installation'),
|
||||
('zh','protocols','test_install','测试安装'),
|
||||
('en','protocols','testing_on_ubuntu22','Testing on Ubuntu 22.04 in isolated Docker'),
|
||||
('ru','protocols','testing_on_ubuntu22','Тест на Ubuntu 22.04 в изолированном Docker'),
|
||||
('es','protocols','testing_on_ubuntu22','Prueba en Ubuntu 22.04 en Docker aislado'),
|
||||
('de','protocols','testing_on_ubuntu22','Test auf Ubuntu 22.04 in isoliertem Docker'),
|
||||
('fr','protocols','testing_on_ubuntu22','Test sur Ubuntu 22.04 dans Docker isolé'),
|
||||
('zh','protocols','testing_on_ubuntu22','在隔离的 Docker 中于 Ubuntu 22.04 测试'),
|
||||
('en','protocols','test_result','Test result'),
|
||||
('ru','protocols','test_result','Результат теста'),
|
||||
('es','protocols','test_result','Resultado de la prueba'),
|
||||
('de','protocols','test_result','Testergebnis'),
|
||||
('fr','protocols','test_result','Résultat du test'),
|
||||
('zh','protocols','test_result','测试结果'),
|
||||
('en','protocols','client_output_preview','Client output preview'),
|
||||
('ru','protocols','client_output_preview','Предпросмотр ответа клиенту'),
|
||||
('es','protocols','client_output_preview','Vista previa de salida del cliente'),
|
||||
('de','protocols','client_output_preview','Client‑Ausgabevorschau'),
|
||||
('fr','protocols','client_output_preview','Aperçu de la sortie client'),
|
||||
('zh','protocols','client_output_preview','客户端输出预览'),
|
||||
('en','protocols','test_failed','Test failed'),
|
||||
('ru','protocols','test_failed','Ошибка теста'),
|
||||
('es','protocols','test_failed','La prueba falló'),
|
||||
('de','protocols','test_failed','Test fehlgeschlagen'),
|
||||
('fr','protocols','test_failed','Échec du test'),
|
||||
('zh','protocols','test_failed','测试失败')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
|
||||
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active, created_at, updated_at)
|
||||
SELECT 'SMB Server', 'smb', 'Samba SMB file share inside Docker with random host port', '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-smb}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nSMB_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\n\n docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/smb/share\n docker run -d \\\n --name "$CONTAINER_NAME" \\\n -p "${SMB_PORT}:445" \\\n -v /opt/amnezia/smb/share:/share \\\n dperson/samba -p -u "amnezia;amnezia" -s "share;/share;yes;no;no;amnezia"\n echo "Port: ${SMB_PORT}"\n echo "Password: amnezia"\n', 'smb://{{server_host}}:{{server_port}}/share\nUsername: amnezia\nPassword: {{password}}', true, true, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='smb');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT id, 'server_host', 'string', '127.0.0.1', 'Server hostname or IP', true FROM protocols WHERE slug = 'smb' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='smb') AND variable_name='server_host');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT id, 'server_port', 'number', '445', 'Server port', true FROM protocols WHERE slug = 'smb' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='smb') AND variable_name='server_port');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT id, 'password', 'string', '', 'Connection password', true FROM protocols WHERE slug = 'smb' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='smb') AND variable_name='password');
|
||||
|
||||
INSERT INTO protocols (name, slug, description, install_script, output_template, ubuntu_compatible, is_active, created_at, updated_at)
|
||||
SELECT 'XRay VLESS', 'xray-vless', 'XRay VLESS server inside Docker with generated client UUID', '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n cat > /opt/amnezia/xray/config.json << EOF\n{\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": 443,\n "protocol": "vless",\n "settings": {\n "clients": [\n { "id": "${CLIENT_ID}" }\n ],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "none"\n }\n }\n ],\n "outbounds": [\n { "protocol": "freedom" }\n ]\n}\nEOF\n docker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:443" \\\n -v /opt/amnezia/xray:/etc/xray \\\n teddysun/xray\n echo "Port: ${XRAY_PORT}"\n echo "ClientID: ${CLIENT_ID}"\n', 'vless://{{client_id}}@{{server_host}}:{{server_port}}?security=none&type=tcp', true, true, NOW(), NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug='xray-vless');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT id, 'server_host', 'string', '127.0.0.1', 'Server hostname or IP', true FROM protocols WHERE slug = 'xray-vless' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='xray-vless') AND variable_name='server_host');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT id, 'server_port', 'number', '443', 'Server port', true FROM protocols WHERE slug = 'xray-vless' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='xray-vless') AND variable_name='server_port');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT id, 'client_id', 'string', '', 'VLESS client ID (UUID)', true FROM protocols WHERE slug = 'xray-vless' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = (SELECT id FROM protocols WHERE slug='xray-vless') AND variable_name='client_id');
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE add_protocol_column_and_constraints()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='vpn_clients' AND COLUMN_NAME='protocol_id') = 0 THEN
|
||||
ALTER TABLE vpn_clients ADD COLUMN protocol_id INT UNSIGNED NULL AFTER user_id;
|
||||
END IF;
|
||||
|
||||
IF (SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='vpn_clients' AND INDEX_NAME='idx_protocol_id') = 0 THEN
|
||||
ALTER TABLE vpn_clients ADD INDEX idx_protocol_id (protocol_id);
|
||||
END IF;
|
||||
|
||||
IF (SELECT COUNT(*) FROM information_schema.TABLE_CONSTRAINTS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='vpn_clients' AND CONSTRAINT_NAME='fk_vpn_clients_protocol') = 0 THEN
|
||||
ALTER TABLE vpn_clients ADD CONSTRAINT fk_vpn_clients_protocol FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE SET NULL;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL add_protocol_column_and_constraints();
|
||||
DROP PROCEDURE add_protocol_column_and_constraints;
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE ensure_users_role_column_and_index()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='users' AND COLUMN_NAME='role') = 0 THEN
|
||||
ALTER TABLE users ADD COLUMN role ENUM('admin','user') DEFAULT 'user' AFTER name;
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.STATISTICS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='users' AND INDEX_NAME='idx_role') = 0 THEN
|
||||
ALTER TABLE users ADD INDEX idx_role (role);
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL ensure_users_role_column_and_index();
|
||||
DROP PROCEDURE ensure_users_role_column_and_index;
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE add_protocols_optional_columns()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='protocols' AND COLUMN_NAME='uninstall_script') = 0 THEN
|
||||
ALTER TABLE protocols ADD COLUMN uninstall_script MEDIUMTEXT NULL AFTER install_script;
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='protocols' AND COLUMN_NAME='password_command') = 0 THEN
|
||||
ALTER TABLE protocols ADD COLUMN password_command TEXT NULL AFTER uninstall_script;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL add_protocols_optional_columns();
|
||||
DROP PROCEDURE add_protocols_optional_columns;
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE ensure_users_display_name_column()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='users' AND COLUMN_NAME='display_name') = 0 THEN
|
||||
ALTER TABLE users ADD COLUMN display_name VARCHAR(255) NULL AFTER name;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL ensure_users_display_name_column();
|
||||
DROP PROCEDURE ensure_users_display_name_column;
|
||||
UPDATE users SET display_name = name WHERE (display_name IS NULL OR display_name = '') AND name IS NOT NULL;
|
||||
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
EXISTING=$(docker ps -aq -f "name=$CONTAINER_NAME" 2>/dev/null | head -1)
|
||||
if [ -z "$EXISTING" ]; then
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules amneziavpn/amnezia-wg:latest
|
||||
sleep 2
|
||||
else
|
||||
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "")
|
||||
if [ "$STATUS" != "running" ]; then
|
||||
docker start "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
docker exec -i "$CONTAINER_NAME" sh -lc "mkdir -p /opt/amnezia/awg"
|
||||
|
||||
HAS_CONF=$(docker exec "$CONTAINER_NAME" sh -lc "[ -f /opt/amnezia/awg/wg0.conf ] && echo yes || echo no")
|
||||
if [ "$HAS_CONF" = "yes" ]; then
|
||||
PORT=$(docker exec "$CONTAINER_NAME" sh -lc "grep -E \"^ListenPort\" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d \"[:space:]\"")
|
||||
PSK=$(docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(docker exec "$CONTAINER_NAME" sh -lc "grep -E \"^PresharedKey\" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d \"[:space:]\"")
|
||||
fi
|
||||
PUBKEY=$(docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(docker exec "$CONTAINER_NAME" cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
cat > /opt/amnezia/awg/wg0.conf << EOF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
S3 = 20
|
||||
S4 = 10
|
||||
H1 = 0xDEADBEEF
|
||||
H2 = 0xCAFEBABE
|
||||
H3 = 0x12345678
|
||||
H4 = 0x9ABCDEF0
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
|
||||
[Peer]
|
||||
PublicKey =
|
||||
PresharedKey = $PRESHARED_KEY
|
||||
AllowedIPs = 10.8.1.2/32
|
||||
EOF
|
||||
|
||||
docker exec "$CONTAINER_NAME" sh -lc "echo $PRIVATE_KEY > /opt/amnezia/awg/wireguard_server_private_key.key"
|
||||
docker exec "$CONTAINER_NAME" sh -lc "echo $PUBLIC_KEY > /opt/amnezia/awg/wireguard_server_public_key.key"
|
||||
docker exec "$CONTAINER_NAME" sh -lc "echo $PRESHARED_KEY > /opt/amnezia/awg/wireguard_psk.key"
|
||||
docker exec "$CONTAINER_NAME" sh -lc "echo [] > /opt/amnezia/awg/clientsTable"
|
||||
|
||||
docker exec "$CONTAINER_NAME" wg-quick up /opt/amnezia/awg/wg0.conf || true
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
|
||||
UPDATE protocols SET
|
||||
uninstall_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
|
||||
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||
docker rm -fv "$CONTAINER_NAME" 2>/dev/null || true
|
||||
docker image rm amneziavpn/amnezia-wg:latest 2>/dev/null || true
|
||||
docker network rm amnezia-dns-net 2>/dev/null || true
|
||||
rm -rf /opt/amnezia/amnezia-awg 2>/dev/null || true
|
||||
rm -rf /opt/amnezia/awg 2>/dev/null || true
|
||||
|
||||
echo "{\"success\":true,\"message\":\"AmneziaWG uninstalled\"}"'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE ensure_vpn_servers_install_columns()
|
||||
BEGIN
|
||||
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='vpn_servers' AND COLUMN_NAME='install_protocol') = 0 THEN
|
||||
ALTER TABLE vpn_servers ADD COLUMN install_protocol VARCHAR(100) NULL AFTER container_name;
|
||||
END IF;
|
||||
IF (SELECT COUNT(*) FROM information_schema.COLUMNS
|
||||
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME='vpn_servers' AND COLUMN_NAME='install_options') = 0 THEN
|
||||
ALTER TABLE vpn_servers ADD COLUMN install_options JSON NULL AFTER install_protocol;
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL ensure_vpn_servers_install_columns();
|
||||
DROP PROCEDURE ensure_vpn_servers_install_columns;
|
||||
@@ -0,0 +1,108 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
EXISTING=$(docker ps -aq -f "name=$CONTAINER_NAME" 2>/dev/null | head -1)
|
||||
if [ -z "$EXISTING" ]; then
|
||||
# Run container with volume mount and keepalive command
|
||||
# Waits for config file to appear before starting WireGuard
|
||||
docker run -d --name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
--privileged \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=SYS_MODULE \
|
||||
-p "${VPN_PORT}:${VPN_PORT}/udp" \
|
||||
-v /lib/modules:/lib/modules \
|
||||
-v /opt/amnezia/awg:/opt/amnezia/awg \
|
||||
amneziavpn/amnezia-wg:latest \
|
||||
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
else
|
||||
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "")
|
||||
if [ "$STATUS" != "running" ]; then
|
||||
docker start "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for existing config
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
# Extract existing configuration
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate keys using the container
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# Write config to HOST file (mounted to container)
|
||||
cat > /opt/amnezia/awg/wg0.conf << EOF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
S3 = 20
|
||||
S4 = 10
|
||||
H1 = 0xDEADBEEF
|
||||
H2 = 0xCAFEBABE
|
||||
H3 = 0x12345678
|
||||
H4 = 0x9ABCDEF0
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
|
||||
[Peer]
|
||||
PublicKey =
|
||||
PresharedKey = $PRESHARED_KEY
|
||||
AllowedIPs = 10.8.1.2/32
|
||||
EOF
|
||||
|
||||
# Save keys to files on host
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
# Container is already waiting for config (loop), so it should pick it up automatically.
|
||||
# But we can also force it if needed, or just wait a moment.
|
||||
# The loop is: while [ ! -f ... ]; do sleep 1; done; wg-quick up ...
|
||||
# Since we just wrote the file, the loop will exit and run wg-quick up.
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,220 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
# Ensure container is running correctly
|
||||
check_container
|
||||
STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is in restart loop. Recreating..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
elif [ $STATUS -eq 1 ]; then
|
||||
# If stopped but exists, remove to recreate with correct flags (just in case)
|
||||
# Or just start it? Better to recreate to ensure volume mounts are correct.
|
||||
# But if we recreate, we must ensure we mount the volume.
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# If container is missing (or we just removed it), create it
|
||||
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
docker run -d --name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
--privileged \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=SYS_MODULE \
|
||||
-p "${VPN_PORT}:${VPN_PORT}/udp" \
|
||||
-v /lib/modules:/lib/modules \
|
||||
-v /opt/amnezia/awg:/opt/amnezia/awg \
|
||||
amneziavpn/amnezia-wg:latest \
|
||||
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
# Wait a moment for it to start
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Extract config from HOST file
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If no host config, check if container exists and try to rescue config
|
||||
check_container
|
||||
STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is restarting and no host config found. Attempting to rescue config..."
|
||||
# Try to copy from container even if restarting (might fail if container is crashing too fast)
|
||||
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
echo "Rescued config from broken container."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
|
||||
# Now recreate container with rescue config
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
# Re-run script to pick up host config (recursive call or just fall through? Fall through requires logic jump)
|
||||
# Easier to just exit and let user run again? No, let''s proceed.
|
||||
# We have config on host now, so the logic below for "new install" needs to be skipped or we need to jump back.
|
||||
# Let''s just restart the script logic by execing self? No, complex.
|
||||
# Let''s just set a flag.
|
||||
HAS_RESCUED=1
|
||||
else
|
||||
echo "Could not rescue config. Removing broken container."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
HAS_RESCUED=0
|
||||
fi
|
||||
elif [ $STATUS -eq 0 ]; then
|
||||
# Running. Check if it has config inside but not on host (old version)
|
||||
if docker exec "$CONTAINER_NAME" [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Container running with internal config. Migrating to host..."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
|
||||
# Recreate to add volume mount
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
HAS_RESCUED=1
|
||||
else
|
||||
# Running but no config? Weird. Treat as fresh.
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
HAS_RESCUED=0
|
||||
fi
|
||||
else
|
||||
HAS_RESCUED=0
|
||||
fi
|
||||
|
||||
# If we rescued config, we need to start the container with mounts
|
||||
if [ "$HAS_RESCUED" = "1" ]; then
|
||||
docker run -d --name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
--privileged \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=SYS_MODULE \
|
||||
-p "${VPN_PORT}:${VPN_PORT}/udp" \
|
||||
-v /lib/modules:/lib/modules \
|
||||
-v /opt/amnezia/awg:/opt/amnezia/awg \
|
||||
amneziavpn/amnezia-wg:latest \
|
||||
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
# Extract and exit (same logic as top)
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# FRESH INSTALL
|
||||
docker run -d --name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
--privileged \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=SYS_MODULE \
|
||||
-p "${VPN_PORT}:${VPN_PORT}/udp" \
|
||||
-v /lib/modules:/lib/modules \
|
||||
-v /opt/amnezia/awg:/opt/amnezia/awg \
|
||||
amneziavpn/amnezia-wg:latest \
|
||||
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
cat > /opt/amnezia/awg/wg0.conf << EOF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
S3 = 20
|
||||
S4 = 10
|
||||
H1 = 0xDEADBEEF
|
||||
H2 = 0xCAFEBABE
|
||||
H3 = 0x12345678
|
||||
H4 = 0x9ABCDEF0
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
|
||||
[Peer]
|
||||
PublicKey =
|
||||
PresharedKey = $PRESHARED_KEY
|
||||
AllowedIPs = 10.8.1.2/32
|
||||
EOF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,213 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
# Ensure container is running correctly
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is in restart loop. Recreating..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
elif [ $STATUS -eq 1 ]; then
|
||||
# If stopped but exists, remove to recreate with correct flags
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# If container is missing (or we just removed it), create it
|
||||
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
docker run -d --name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
--privileged \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=SYS_MODULE \
|
||||
-p "${VPN_PORT}:${VPN_PORT}/udp" \
|
||||
-v /lib/modules:/lib/modules \
|
||||
-v /opt/amnezia/awg:/opt/amnezia/awg \
|
||||
amneziavpn/amnezia-wg:latest \
|
||||
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
# Wait a moment for it to start
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
# Extract config from HOST file
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# If no host config, check if container exists and try to rescue config
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is restarting and no host config found. Attempting to rescue config..."
|
||||
# Try to copy from container even if restarting (might fail if container is crashing too fast)
|
||||
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
echo "Rescued config from broken container."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
|
||||
# Now recreate container with rescue config
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
HAS_RESCUED=1
|
||||
else
|
||||
echo "Could not rescue config. Removing broken container."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
HAS_RESCUED=0
|
||||
fi
|
||||
elif [ $STATUS -eq 0 ]; then
|
||||
# Running. Check if it has config inside but not on host (old version)
|
||||
if docker exec "$CONTAINER_NAME" [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Container running with internal config. Migrating to host..."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
|
||||
# Recreate to add volume mount
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
HAS_RESCUED=1
|
||||
else
|
||||
# Running but no config? Weird. Treat as fresh.
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
HAS_RESCUED=0
|
||||
fi
|
||||
else
|
||||
HAS_RESCUED=0
|
||||
fi
|
||||
|
||||
# If we rescued config, we need to start the container with mounts
|
||||
if [ "$HAS_RESCUED" = "1" ]; then
|
||||
docker run -d --name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
--privileged \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=SYS_MODULE \
|
||||
-p "${VPN_PORT}:${VPN_PORT}/udp" \
|
||||
-v /lib/modules:/lib/modules \
|
||||
-v /opt/amnezia/awg:/opt/amnezia/awg \
|
||||
amneziavpn/amnezia-wg:latest \
|
||||
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
# Extract and exit (same logic as top)
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# FRESH INSTALL
|
||||
docker run -d --name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
--privileged \
|
||||
--cap-add=NET_ADMIN \
|
||||
--cap-add=SYS_MODULE \
|
||||
-p "${VPN_PORT}:${VPN_PORT}/udp" \
|
||||
-v /lib/modules:/lib/modules \
|
||||
-v /opt/amnezia/awg:/opt/amnezia/awg \
|
||||
amneziavpn/amnezia-wg:latest \
|
||||
sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
cat > /opt/amnezia/awg/wg0.conf << EOF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
S3 = 20
|
||||
S4 = 10
|
||||
H1 = 0xDEADBEEF
|
||||
H2 = 0xCAFEBABE
|
||||
H3 = 0x12345678
|
||||
H4 = 0x9ABCDEF0
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
|
||||
[Peer]
|
||||
PublicKey =
|
||||
PresharedKey = $PRESHARED_KEY
|
||||
AllowedIPs = 10.8.1.2/32
|
||||
EOF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,156 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is in restart loop. Recreating..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
elif [ $STATUS -eq 1 ]; then
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
# Run container with volume mount - SINGLE LINE to avoid syntax issues
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Rescue logic
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
HAS_RESCUED=0
|
||||
|
||||
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
|
||||
echo "Checking for config in existing container..."
|
||||
# Stop container to ensure stable copy
|
||||
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
echo "Rescued config from container."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
HAS_RESCUED=1
|
||||
fi
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Start container (Fresh or Rescued)
|
||||
# SINGLE LINE command
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
if [ "$HAS_RESCUED" = "1" ]; then
|
||||
# Extract and exit
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate new config
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
cat > /opt/amnezia/awg/wg0.conf << EOF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
S3 = 20
|
||||
S4 = 10
|
||||
H1 = 0xDEADBEEF
|
||||
H2 = 0xCAFEBABE
|
||||
H3 = 0x12345678
|
||||
H4 = 0x9ABCDEF0
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
|
||||
[Peer]
|
||||
PublicKey =
|
||||
PresharedKey = $PRESHARED_KEY
|
||||
AllowedIPs = 10.8.1.2/32
|
||||
EOF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,172 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate existing config
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
if grep -Fq "\$PRIVATE_KEY" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected broken configuration (unexpanded variables). Removing..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
rm -f /opt/amnezia/awg/wireguard_psk.key
|
||||
rm -f /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
rm -f /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is in restart loop. Recreating..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
elif [ $STATUS -eq 1 ]; then
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
# Run container with volume mount - SINGLE LINE
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Rescue logic
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
HAS_RESCUED=0
|
||||
|
||||
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
|
||||
echo "Checking for config in existing container..."
|
||||
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
# Validate rescued config
|
||||
if grep -Fq "\$PRIVATE_KEY" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Rescued config is broken. Discarding."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
else
|
||||
echo "Rescued config from container."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
HAS_RESCUED=1
|
||||
fi
|
||||
fi
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Start container (Fresh or Rescued)
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
if [ "$HAS_RESCUED" = "1" ]; then
|
||||
# Extract and exit
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate new config
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# Use WG_CONF delimiter to avoid EOF replacement in PHP
|
||||
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
S3 = 20
|
||||
S4 = 10
|
||||
H1 = 0xDEADBEEF
|
||||
H2 = 0xCAFEBABE
|
||||
H3 = 0x12345678
|
||||
H4 = 0x9ABCDEF0
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
|
||||
[Peer]
|
||||
PublicKey =
|
||||
PresharedKey = $PRESHARED_KEY
|
||||
AllowedIPs = 10.8.1.2/32
|
||||
WG_CONF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,177 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate existing config
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
# Check for unexpanded variables
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected broken configuration (unexpanded variables). Removing..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for invalid parameters S3/S4
|
||||
if grep -Eiq "^S3\s*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4\s*=" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is in restart loop. Recreating..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
elif [ $STATUS -eq 1 ]; then
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
# Run container with volume mount - SINGLE LINE
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Rescue logic
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
HAS_RESCUED=0
|
||||
|
||||
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
|
||||
echo "Checking for config in existing container..."
|
||||
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
# Validate rescued config
|
||||
IS_BROKEN=0
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^S3\s*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
|
||||
if [ "$IS_BROKEN" = "1" ]; then
|
||||
echo "Rescued config is broken. Discarding."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
else
|
||||
echo "Rescued config from container."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
HAS_RESCUED=1
|
||||
fi
|
||||
fi
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Start container (Fresh or Rescued)
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
if [ "$HAS_RESCUED" = "1" ]; then
|
||||
# Extract and exit
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate new config
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# Use WG_CONF delimiter to avoid EOF replacement in PHP
|
||||
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
H1 = 0xDEADBEEF
|
||||
H2 = 0xCAFEBABE
|
||||
H3 = 0x12345678
|
||||
H4 = 0x9ABCDEF0
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
|
||||
[Peer]
|
||||
PublicKey =
|
||||
PresharedKey = $PRESHARED_KEY
|
||||
AllowedIPs = 10.8.1.2/32
|
||||
WG_CONF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,183 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate existing config
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
# Check for unexpanded variables
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected broken configuration (unexpanded variables). Removing..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for invalid parameters S3/S4
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for hex H-params
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid hex parameters (H1-H4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is in restart loop. Recreating..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
elif [ $STATUS -eq 1 ]; then
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
# Run container with volume mount - SINGLE LINE
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Rescue logic
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
HAS_RESCUED=0
|
||||
|
||||
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
|
||||
echo "Checking for config in existing container..."
|
||||
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
# Validate rescued config
|
||||
IS_BROKEN=0
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
|
||||
if [ "$IS_BROKEN" = "1" ]; then
|
||||
echo "Rescued config is broken. Discarding."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
else
|
||||
echo "Rescued config from container."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
HAS_RESCUED=1
|
||||
fi
|
||||
fi
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Start container (Fresh or Rescued)
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
if [ "$HAS_RESCUED" = "1" ]; then
|
||||
# Extract and exit
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate new config
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# Use WG_CONF delimiter to avoid EOF replacement in PHP
|
||||
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
H1 = 1
|
||||
H2 = 2
|
||||
H3 = 3
|
||||
H4 = 4
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
|
||||
[Peer]
|
||||
PublicKey =
|
||||
PresharedKey = $PRESHARED_KEY
|
||||
AllowedIPs = 10.8.1.2/32
|
||||
WG_CONF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,184 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate existing config
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
# Check for unexpanded variables
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected broken configuration (unexpanded variables). Removing..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for invalid parameters S3/S4
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for hex H-params
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid hex parameters (H1-H4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for empty PublicKey
|
||||
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected empty PublicKey. Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is in restart loop. Recreating..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
elif [ $STATUS -eq 1 ]; then
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
if ! docker ps -q -f name="$CONTAINER_NAME" >/dev/null 2>&1; then
|
||||
# Run container with volume mount - SINGLE LINE
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Rescue logic
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
HAS_RESCUED=0
|
||||
|
||||
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
|
||||
echo "Checking for config in existing container..."
|
||||
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
# Validate rescued config
|
||||
IS_BROKEN=0
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
|
||||
if [ "$IS_BROKEN" = "1" ]; then
|
||||
echo "Rescued config is broken. Discarding."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
else
|
||||
echo "Rescued config from container."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
HAS_RESCUED=1
|
||||
fi
|
||||
fi
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Start container (Fresh or Rescued)
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
if [ "$HAS_RESCUED" = "1" ]; then
|
||||
# Extract and exit
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate new config
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# Use WG_CONF delimiter to avoid EOF replacement in PHP
|
||||
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
H1 = 1
|
||||
H2 = 2
|
||||
H3 = 3
|
||||
H4 = 4
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
WG_CONF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,188 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate existing config
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
# Check for unexpanded variables
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected broken configuration (unexpanded variables). Removing..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for invalid parameters S3/S4
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for hex H-params
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid hex parameters (H1-H4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for empty PublicKey
|
||||
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected empty PublicKey. Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
VPN_PORT=${PORT:-$VPN_PORT}
|
||||
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is in restart loop. Recreating..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
STATUS=1
|
||||
elif [ $STATUS -eq 0 ]; then
|
||||
echo "Container is running."
|
||||
fi
|
||||
|
||||
# Ensure container is running
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo "Starting container..."
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: $VPN_PORT"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Rescue logic
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
HAS_RESCUED=0
|
||||
|
||||
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
|
||||
echo "Checking for config in existing container..."
|
||||
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
# Validate rescued config
|
||||
IS_BROKEN=0
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
|
||||
if [ "$IS_BROKEN" = "1" ]; then
|
||||
echo "Rescued config is broken. Discarding."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
else
|
||||
echo "Rescued config from container."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
HAS_RESCUED=1
|
||||
fi
|
||||
fi
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Start container (Fresh or Rescued)
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
if [ "$HAS_RESCUED" = "1" ]; then
|
||||
# Extract and exit
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate new config
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# Use WG_CONF delimiter to avoid EOF replacement in PHP
|
||||
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
H1 = 1
|
||||
H2 = 2
|
||||
H3 = 3
|
||||
H4 = 4
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
WG_CONF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey: $PRESHARED_KEY"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,22 @@
|
||||
-- Backfill XRay server_port from extras.result keys produced by set -x
|
||||
UPDATE server_protocols sp
|
||||
JOIN protocols p ON p.id = sp.protocol_id
|
||||
SET sp.config_data = JSON_SET(sp.config_data, '$.server_port', JSON_EXTRACT(sp.config_data, '$.extras.result."+_xray_port"')),
|
||||
sp.applied_at = NOW()
|
||||
WHERE p.slug = 'xray-vless'
|
||||
AND (
|
||||
JSON_EXTRACT(sp.config_data, '$.server_port') IS NULL OR
|
||||
JSON_UNQUOTE(JSON_EXTRACT(sp.config_data, '$.server_port')) = ''
|
||||
)
|
||||
AND JSON_EXTRACT(sp.config_data, '$.extras.result."+_xray_port"') IS NOT NULL;
|
||||
|
||||
UPDATE server_protocols sp
|
||||
JOIN protocols p ON p.id = sp.protocol_id
|
||||
SET sp.config_data = JSON_SET(sp.config_data, '$.server_port', JSON_EXTRACT(sp.config_data, '$.extras.result.xray_port')),
|
||||
sp.applied_at = NOW()
|
||||
WHERE p.slug = 'xray-vless'
|
||||
AND (
|
||||
JSON_EXTRACT(sp.config_data, '$.server_port') IS NULL OR
|
||||
JSON_UNQUOTE(JSON_EXTRACT(sp.config_data, '$.server_port')) = ''
|
||||
)
|
||||
AND JSON_EXTRACT(sp.config_data, '$.extras.result.xray_port') IS NOT NULL;
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Update XRay VLESS protocol to Reality/Vision setup
|
||||
UPDATE protocols SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\n\nPRIVATE_KEY=$(docker run --rm teddysun/xray xray x25519 | grep "Private key:" | awk ''{print $3}'')\nPUBLIC_KEY=$(docker run --rm teddysun/xray xray x25519 -i "$PRIVATE_KEY" | grep "Public key:" | awk ''{print $3}'')\nSHORT_ID=$(openssl rand -hex 8)\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="www.googletagmanager.com"\nFINGERPRINT="chrome"\nSPIDER_X="/"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}" } ],\n "decryption": "none",\n "fallbacks": [ { "dest": 80 } ]\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n }\n ],\n "outbounds": [ { "protocol": "freedom", "tag": "direct" } ]\n}\nEOF\n\n# start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:${XRAY_PORT}" \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# panel output\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"',
|
||||
output_template = 'vless://{{client_id}}@{{server_host}}:{{server_port}}?encryption=none&flow=xtls-rprx-vision&security=reality&sni={{reality_server_name}}&fp=chrome&pbk={{reality_public_key}}&sid={{reality_short_id}}&type=tcp'
|
||||
WHERE slug = 'xray-vless';
|
||||
|
||||
-- Ensure protocol variables exist
|
||||
SET @pid = (SELECT id FROM protocols WHERE slug = 'xray-vless');
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, description, required)
|
||||
SELECT @pid, 'reality_public_key', 'string', 'Reality public key (base64url)', true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=@pid AND variable_name='reality_public_key');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, description, required)
|
||||
SELECT @pid, 'reality_short_id', 'string', 'Reality shortId', true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=@pid AND variable_name='reality_short_id');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, description, required)
|
||||
SELECT @pid, 'reality_server_name', 'string', 'SNI server name for Reality', true
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=@pid AND variable_name='reality_server_name');
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Set uninstall script for XRay VLESS protocol
|
||||
UPDATE protocols
|
||||
SET uninstall_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${SERVER_CONTAINER:-${CONTAINER_NAME:-amnezia-xray}}"\n\ndocker rm -f "${CONTAINER_NAME}" >/dev/null 2>&1 || true\nrm -rf /opt/amnezia/xray || true\n\necho "Uninstalled: ${CONTAINER_NAME}"\n'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Make XRay Reality install script robust (keys generation and container name)
|
||||
UPDATE protocols
|
||||
SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="amnezia-xray"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\n\n# ensure image exists to avoid pull logs mixing with key output\ndocker pull teddysun/xray >/dev/null 2>&1 || true\n\nGEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)\nPRIVATE_KEY=$(printf "%s\n" "$GEN" | awk -F": " "/[Pp]rivate/ {print \\$2}" | tr -d " \\t\\r\\n")\nPUBLIC_KEY=$(printf "%s\n" "$GEN" | awk -F": " "/[Pp]ublic/ {print \\$2}" | tr -d " \\t\\r\\n")\n\nif [ -z "$PRIVATE_KEY" ]; then\n PRIVATE_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null | awk -F": " "/[Pp]rivate/ {print \\$2}" | tr -d " \\t\\r\\n" || true)\nfi\nif [ -z "$PUBLIC_KEY" ]; then\n PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | awk -F": " "/[Pp]ublic/ {print \\$2}" | tr -d " \\t\\r\\n" || true)\nfi\n\nSHORT_ID=$(openssl rand -hex 8)\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"\nFINGERPRINT="${FINGERPRINT:-chrome}"\nSPIDER_X="${SPIDER_X:-/}"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}" } ],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n }\n ],\n "outbounds": [ { "protocol": "freedom", "tag": "direct" } ]\n}\nEOF\n\n# start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:${XRAY_PORT}" \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# panel output\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"\necho "ContainerName: ${CONTAINER_NAME}"'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,4 @@
|
||||
-- Fix PublicKey extraction and ShortID generation in XRay install script
|
||||
UPDATE protocols
|
||||
SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))\n\n# Ensure image present to avoid noisy pull output\ndocker pull teddysun/xray >/dev/null 2>&1 || true\n\n# Generate keys\nGEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)\nPRIVATE_KEY=$(printf "%s\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \t\r\n")\nPUBLIC_KEY=$(printf "%s\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \t\r\n")\n\nif [ -z "$PUBLIC_KEY" ]; then\n PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \t\r\n" || true)\nfi\n\n# Generate shortId without openssl\nSHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \n")\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"\nFINGERPRINT="${FINGERPRINT:-chrome}"\nSPIDER_X="${SPIDER_X:-/}"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}" } ],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n }\n ],\n "outbounds": [ { "protocol": "freedom", "tag": "direct" } ]\n}\nEOF\n\n# Start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:${XRAY_PORT}" \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# Output configuration\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"\necho "ContainerName: ${CONTAINER_NAME}"'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE protocols
|
||||
SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nPORT_RANGE_START=${PORT_RANGE_START:-30000}\nPORT_RANGE_END=${PORT_RANGE_END:-65000}\nXRAY_PORT=${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}\n\n# Ensure image present to avoid noisy pull output\ndocker pull teddysun/xray >/dev/null 2>&1 || true\n\n# Generate keys\nGEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)\nPRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")\nPUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")\n\nif [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then\n PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)\nfi\n\nSHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"\nFINGERPRINT="${FINGERPRINT:-chrome}"\nSPIDER_X="${SPIDER_X:-/}"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}" } ],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n }\n ],\n "outbounds": [ { "protocol": "freedom", "tag": "direct" } ]\n}\nEOF\n\n# Start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:${XRAY_PORT}" \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# Output configuration\necho "XrayPort: ${XRAY_PORT}"\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"\necho "ContainerName: ${CONTAINER_NAME}"'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,3 @@
|
||||
UPDATE protocols
|
||||
SET install_script = '#!/bin/bash\n\nset -euo pipefail\nset -x\n\nCONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"\nXRAY_PORT=${SERVER_PORT:-443}\n\n# Ensure image present to avoid noisy pull output\ndocker pull teddysun/xray >/dev/null 2>&1 || true\n\n# Generate keys\nGEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)\nPRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")\nPUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")\n\nif [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then\n PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)\nfi\n\nSHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")\nCLIENT_ID=$(cat /proc/sys/kernel/random/uuid)\n\nSERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"\nFINGERPRINT="${FINGERPRINT:-chrome}"\nSPIDER_X="${SPIDER_X:-/}"\n\ndocker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true\nmkdir -p /opt/amnezia/xray\n\ncat > /opt/amnezia/xray/server.json << EOF\n{\n "log": { "loglevel": "warning" },\n "inbounds": [\n {\n "listen": "0.0.0.0",\n "port": ${XRAY_PORT},\n "protocol": "vless",\n "settings": {\n "clients": [ { "id": "${CLIENT_ID}" } ],\n "decryption": "none"\n },\n "streamSettings": {\n "network": "tcp",\n "security": "reality",\n "realitySettings": {\n "show": false,\n "dest": "${SERVER_NAME}:443",\n "xver": 0,\n "serverNames": [ "${SERVER_NAME}" ],\n "privateKey": "${PRIVATE_KEY}",\n "shortIds": [ "${SHORT_ID}" ],\n "fingerprint": "${FINGERPRINT}",\n "spiderX": "${SPIDER_X}"\n }\n }\n }\n ],\n "outbounds": [ { "protocol": "freedom", "tag": "direct" } ]\n}\nEOF\n\n# Start container\ndocker run -d \\\n --name "$CONTAINER_NAME" \\\n --restart always \\\n -p "${XRAY_PORT}:${XRAY_PORT}" \\\n -v /opt/amnezia/xray:/opt/amnezia/xray \\\n teddysun/xray xray run -c /opt/amnezia/xray/server.json\n\nsleep 2\n\n# Output configuration\necho "XrayPort: ${XRAY_PORT}"\necho "Port: ${XRAY_PORT}"\necho "ClientID: ${CLIENT_ID}"\necho "PublicKey: ${PUBLIC_KEY}"\necho "PrivateKey: ${PRIVATE_KEY}"\necho "ShortID: ${SHORT_ID}"\necho "ServerName: ${SERVER_NAME}"\necho "ContainerName: ${CONTAINER_NAME}"'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,49 @@
|
||||
ALTER TABLE protocols ADD COLUMN qr_code_template MEDIUMTEXT DEFAULT NULL;
|
||||
ALTER TABLE protocols ADD COLUMN qr_code_format VARCHAR(50) DEFAULT 'amnezia_compressed';
|
||||
|
||||
-- Update AmneziaWG and WireGuard
|
||||
UPDATE protocols SET qr_code_template = '{
|
||||
"containers": [
|
||||
{
|
||||
"awg": {
|
||||
"H1": "{{H1}}",
|
||||
"H2": "{{H2}}",
|
||||
"H3": "{{H3}}",
|
||||
"H4": "{{H4}}",
|
||||
"Jc": "{{Jc}}",
|
||||
"Jmax": "{{Jmax}}",
|
||||
"Jmin": "{{Jmin}}",
|
||||
"S1": "{{S1}}",
|
||||
"S2": "{{S2}}",
|
||||
"last_config": {{last_config_json}},
|
||||
"port": "{{port}}",
|
||||
"transport_proto": "udp"
|
||||
},
|
||||
"container": "amnezia-awg"
|
||||
}
|
||||
],
|
||||
"defaultContainer": "amnezia-awg",
|
||||
"description": "{{description}}",
|
||||
"dns1": "{{dns1}}",
|
||||
"dns2": "{{dns2}}",
|
||||
"hostName": "{{hostName}}"
|
||||
}' WHERE slug IN ('amnezia-wg', 'wireguard', 'amnezia-wg-advanced');
|
||||
|
||||
-- Update XRay
|
||||
UPDATE protocols SET qr_code_template = '{
|
||||
"containers": [
|
||||
{
|
||||
"container": "amnezia-xray",
|
||||
"xray": {
|
||||
"last_config": {{last_config_json}},
|
||||
"port": "{{port}}",
|
||||
"transport_proto": "tcp"
|
||||
}
|
||||
}
|
||||
],
|
||||
"defaultContainer": "amnezia-xray",
|
||||
"description": "{{description}}",
|
||||
"dns1": "1.1.1.1",
|
||||
"dns2": "1.0.0.1",
|
||||
"hostName": "{{hostName}}"
|
||||
}' WHERE slug LIKE '%xray%';
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Add translations for QR code template UI
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en', 'protocols', 'qr_code_template', 'QR Code Template'),
|
||||
('en', 'protocols', 'qr_code_format', 'QR Code Format'),
|
||||
('en', 'protocols', 'qr_code_format_help', 'Select the format for the QR code payload. "Amnezia Compressed" uses the legacy Qt/QDataStream format. "Raw Content" uses the template output directly.'),
|
||||
('en', 'protocols', 'qr_code_template_help', 'Template for the QR code payload. Use {{last_config_json}} to include the full configuration as a JSON object.'),
|
||||
('en', 'protocols', 'variable_last_config_json_help', 'Full configuration as a JSON object (required for Amnezia format)'),
|
||||
('en', 'protocols', 'plus_all_output_variables', 'Plus all variables from the Output Template section'),
|
||||
('en', 'ai', 'prompt_placeholder_qr_template', 'Describe how the QR code payload should be structured (e.g., "Standard WireGuard config format" or "JSON with specific fields")')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
@@ -0,0 +1,70 @@
|
||||
-- Add Russian translations for Protocol Editor
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('ru', 'protocols', 'edit_protocol', 'Редактирование протокола'),
|
||||
('ru', 'protocols', 'create_protocol', 'Создание протокола'),
|
||||
('ru', 'protocols', 'edit_protocol_description', 'Изменение настроек и скриптов протокола'),
|
||||
('ru', 'protocols', 'create_protocol_description', 'Добавление нового протокола в систему'),
|
||||
('ru', 'protocols', 'back_to_protocols', 'К списку протоколов'),
|
||||
('ru', 'protocols', 'basic_information', 'Основная информация'),
|
||||
('ru', 'protocols', 'name_label', 'Название'),
|
||||
('ru', 'protocols', 'name_help', 'Отображаемое имя протокола'),
|
||||
('ru', 'protocols', 'slug_label', 'Слаг (ID)'),
|
||||
('ru', 'protocols', 'slug_help', 'Уникальный идентификатор (латиница, цифры, дефис)'),
|
||||
('ru', 'protocols', 'description_help', 'Краткое описание протокола'),
|
||||
('ru', 'protocols', 'installation_script', 'Скрипт установки'),
|
||||
('ru', 'protocols', 'install_script_help', 'Bash скрипт, который будет выполнен при установке протокола'),
|
||||
('ru', 'protocols', 'uninstallation_script', 'Скрипт удаления'),
|
||||
('ru', 'protocols', 'uninstall_script_help', 'Bash скрипт, который будет выполнен при удалении протокола'),
|
||||
('ru', 'protocols', 'test_install', 'Тест установки'),
|
||||
('ru', 'protocols', 'test_uninstall', 'Тест удаления'),
|
||||
('ru', 'protocols', 'testing_on_ubuntu22', 'Тестирование на Ubuntu 22.04 (Docker)'),
|
||||
('ru', 'protocols', 'test_result', 'Результат выполнения'),
|
||||
('ru', 'protocols', 'client_output_preview', 'Предпросмотр конфига клиента'),
|
||||
('ru', 'protocols', 'output_template', 'Шаблон конфигурации'),
|
||||
('ru', 'protocols', 'output_template_help', 'Шаблон для генерации файла конфигурации клиента. Используйте переменные {{variable}}'),
|
||||
('ru', 'protocols', 'available_variables', 'Доступные переменные'),
|
||||
('ru', 'protocols', 'variable_private_key_help', 'Приватный ключ клиента'),
|
||||
('ru', 'protocols', 'variable_public_key_help', 'Публичный ключ сервера'),
|
||||
('ru', 'protocols', 'variable_client_ip_help', 'IP-адрес клиента'),
|
||||
('ru', 'protocols', 'variable_server_host_help', 'Хост сервера (IP или домен)'),
|
||||
('ru', 'protocols', 'variable_server_port_help', 'Порт сервера'),
|
||||
('ru', 'protocols', 'variable_preshared_key_help', 'Дополнительный ключ шифрования (PSK)'),
|
||||
('ru', 'protocols', 'variable_last_config_json_help', 'Полная конфигурация в формате JSON (для Amnezia)'),
|
||||
('ru', 'protocols', 'plus_all_output_variables', 'Плюс все переменные из шаблона конфигурации'),
|
||||
('ru', 'protocols', 'qr_code_template', 'Шаблон QR-кода'),
|
||||
('ru', 'protocols', 'qr_code_template_help', 'Шаблон для формирования содержимого QR-кода'),
|
||||
('ru', 'protocols', 'qr_code_format', 'Формат QR-кода'),
|
||||
('ru', 'protocols', 'qr_code_format_help', 'Выберите формат данных в QR-коде'),
|
||||
('ru', 'protocols', 'password_generation', 'Генерация пароля'),
|
||||
('ru', 'protocols', 'password_command_help', 'Команда для генерации пароля/ключа (выполняется перед установкой)'),
|
||||
('ru', 'protocols', 'ubuntu_compatible', 'Совместим с Ubuntu'),
|
||||
('ru', 'protocols', 'active_label', 'Активен'),
|
||||
('ru', 'protocols', 'update_protocol', 'Обновить протокол'),
|
||||
('ru', 'protocols', 'save_protocol', 'Сохранить протокол'),
|
||||
('ru', 'protocols', 'please_fill_required_fields', 'Пожалуйста, заполните обязательные поля'),
|
||||
('ru', 'protocols', 'invalid_slug_format', 'Неверный формат слага'),
|
||||
('ru', 'ai', 'get_ai_help', 'Помощь AI'),
|
||||
('ru', 'ai', 'assistant', 'AI Ассистент'),
|
||||
('ru', 'ai', 'select_model', 'Выберите модель'),
|
||||
('ru', 'ai', 'model_gpt35_turbo', 'GPT-3.5 Turbo'),
|
||||
('ru', 'ai', 'model_gpt4', 'GPT-4'),
|
||||
('ru', 'ai', 'model_claude3_haiku', 'Claude 3 Haiku'),
|
||||
('ru', 'ai', 'model_claude3_sonnet', 'Claude 3 Sonnet'),
|
||||
('ru', 'ai', 'custom_model_placeholder', 'Или введите имя модели вручную'),
|
||||
('ru', 'ai', 'check_availability', 'Проверить'),
|
||||
('ru', 'ai', 'protocol_type', 'Тип протокола'),
|
||||
('ru', 'ai', 'general_vpn', 'Общий VPN'),
|
||||
('ru', 'ai', 'describe_requirements', 'Опишите требования'),
|
||||
('ru', 'ai', 'prompt_placeholder', 'Например: Скрипт для установки Shadowsocks на порт 8388...'),
|
||||
('ru', 'ai', 'prompt_placeholder_template', 'Например: Конфиг в формате JSON с полями server, port, password...'),
|
||||
('ru', 'ai', 'prompt_placeholder_qr_template', 'Например: Ссылка вида vless://uuid@host:port...'),
|
||||
('ru', 'ai', 'prompt_placeholder_uninstall', 'Например: Остановить docker контейнер и удалить файлы...'),
|
||||
('ru', 'ai', 'generate_script', 'Сгенерировать'),
|
||||
('ru', 'ai', 'generating_script', 'Генерация...'),
|
||||
('ru', 'ai', 'generated_script', 'Результат'),
|
||||
('ru', 'ai', 'suggestions', 'Предложения'),
|
||||
('ru', 'ai', 'apply_to_current_protocol', 'Применить'),
|
||||
('ru', 'ai', 'confirm_apply_script', 'Это заменит текущее содержимое поля. Продолжить?'),
|
||||
('ru', 'ai', 'please_enter_requirements', 'Пожалуйста, введите требования'),
|
||||
('ru', 'ai', 'error_generating_script', 'Ошибка генерации')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
@@ -0,0 +1,10 @@
|
||||
-- Add show_text_content column to protocols
|
||||
ALTER TABLE protocols ADD COLUMN show_text_content TINYINT(1) NOT NULL DEFAULT 0;
|
||||
|
||||
-- Add translations
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en', 'protocols', 'show_text_content', 'Show text content on client page'),
|
||||
('en', 'protocols', 'qr_code_format_text', 'Simple Text'),
|
||||
('ru', 'protocols', 'show_text_content', 'Показывать текстовое содержимое на странице клиента'),
|
||||
('ru', 'protocols', 'qr_code_format_text', 'Простой текст')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
@@ -0,0 +1,188 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate existing config
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
# Check for unexpanded variables
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected broken configuration (unexpanded variables). Removing..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for invalid parameters S3/S4
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for hex H-params
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid hex parameters (H1-H4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for empty PublicKey
|
||||
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected empty PublicKey. Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
VPN_PORT=${PORT:-$VPN_PORT}
|
||||
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is in restart loop. Recreating..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
STATUS=1
|
||||
elif [ $STATUS -eq 0 ]; then
|
||||
echo "Container is running."
|
||||
fi
|
||||
|
||||
# Ensure container is running
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo "Starting container..."
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: $VPN_PORT"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Rescue logic
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
HAS_RESCUED=0
|
||||
|
||||
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
|
||||
echo "Checking for config in existing container..."
|
||||
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
# Validate rescued config
|
||||
IS_BROKEN=0
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
|
||||
if [ "$IS_BROKEN" = "1" ]; then
|
||||
echo "Rescued config is broken. Discarding."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
else
|
||||
echo "Rescued config from container."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
HAS_RESCUED=1
|
||||
fi
|
||||
fi
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Start container (Fresh or Rescued)
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
if [ "$HAS_RESCUED" = "1" ]; then
|
||||
# Extract and exit
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate new config
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# Use WG_CONF delimiter to avoid EOF replacement in PHP
|
||||
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
H1 = 1
|
||||
H2 = 2
|
||||
H3 = 3
|
||||
H4 = 4
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
WG_CONF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey: $PRESHARED_KEY"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,288 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate existing config
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
# Check for unexpanded variables
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected broken configuration (unexpanded variables). Removing..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for invalid parameters S3/S4
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf || grep -Eiq "^S4[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid parameters (S3/S4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for hex H-params
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected invalid hex parameters (H1-H4). Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for empty PublicKey
|
||||
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then
|
||||
echo "Detected empty PublicKey. Removing config to regenerate..."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
VPN_PORT=${PORT:-$VPN_PORT}
|
||||
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -eq 2 ]; then
|
||||
echo "Container is in restart loop. Recreating..."
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
STATUS=1
|
||||
elif [ $STATUS -eq 0 ]; then
|
||||
echo "Container is running."
|
||||
fi
|
||||
|
||||
# Ensure container is running
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo "Starting container..."
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: $VPN_PORT"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
|
||||
|
||||
# Output variables for preview
|
||||
echo "Variable: server_port=$VPN_PORT"
|
||||
echo "Variable: server_public_key=$PUBKEY"
|
||||
echo "Variable: preshared_key=$PSK"
|
||||
echo "Variable: server_host=YOUR_IP"
|
||||
|
||||
# Dummy client vars for preview
|
||||
CLIENT_PRIV_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
echo "Variable: private_key=$CLIENT_PRIV_KEY"
|
||||
echo "Variable: client_ip=10.8.1.2"
|
||||
echo "Variable: dns_servers=1.1.1.1"
|
||||
|
||||
# Obfuscation params (extract from config if possible, else defaults)
|
||||
JC=$(grep -E "^Jc" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
JMIN=$(grep -E "^Jmin" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
JMAX=$(grep -E "^Jmax" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
S1=$(grep -E "^S1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
S2=$(grep -E "^S2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
H1=$(grep -E "^H1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
H2=$(grep -E "^H2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
H3=$(grep -E "^H3" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
H4=$(grep -E "^H4" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
|
||||
echo "Variable: Jc=${JC:-5}"
|
||||
echo "Variable: JC=${JC:-5}"
|
||||
echo "Variable: Jmin=${JMIN:-100}"
|
||||
echo "Variable: JMIN=${JMIN:-100}"
|
||||
echo "Variable: Jmax=${JMAX:-200}"
|
||||
echo "Variable: JMAX=${JMAX:-200}"
|
||||
echo "Variable: S1=${S1:-50}"
|
||||
echo "Variable: S2=${S2:-100}"
|
||||
echo "Variable: H1=${H1:-1}"
|
||||
echo "Variable: H2=${H2:-2}"
|
||||
echo "Variable: H3=${H3:-3}"
|
||||
echo "Variable: H4=${H4:-4}"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Rescue logic
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
HAS_RESCUED=0
|
||||
|
||||
if [ $STATUS -eq 2 ] || [ $STATUS -eq 0 ]; then
|
||||
echo "Checking for config in existing container..."
|
||||
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
if docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wg0.conf /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
# Validate rescued config
|
||||
IS_BROKEN=0
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
if grep -Eiq "^PublicKey[[:space:]]*=[[:space:]]*$" /opt/amnezia/awg/wg0.conf; then IS_BROKEN=1; fi
|
||||
|
||||
if [ "$IS_BROKEN" = "1" ]; then
|
||||
echo "Rescued config is broken. Discarding."
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
else
|
||||
echo "Rescued config from container."
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_psk.key /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_public_key.key /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true
|
||||
docker cp "$CONTAINER_NAME":/opt/amnezia/awg/wireguard_server_private_key.key /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true
|
||||
HAS_RESCUED=1
|
||||
fi
|
||||
fi
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Start container (Fresh or Rescued)
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
|
||||
if [ "$HAS_RESCUED" = "1" ]; then
|
||||
# Extract and exit
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
|
||||
|
||||
# Output variables for preview
|
||||
echo "Variable: server_port=$VPN_PORT"
|
||||
echo "Variable: server_public_key=$PUBKEY"
|
||||
echo "Variable: preshared_key=$PSK"
|
||||
echo "Variable: server_host=YOUR_IP"
|
||||
|
||||
# Dummy client vars for preview
|
||||
CLIENT_PRIV_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
echo "Variable: private_key=$CLIENT_PRIV_KEY"
|
||||
echo "Variable: client_ip=10.8.1.2"
|
||||
echo "Variable: dns_servers=1.1.1.1"
|
||||
|
||||
# Obfuscation params (extract from config if possible, else defaults)
|
||||
JC=$(grep -E "^Jc" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
JMIN=$(grep -E "^Jmin" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
JMAX=$(grep -E "^Jmax" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
S1=$(grep -E "^S1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
S2=$(grep -E "^S2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
H1=$(grep -E "^H1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
H2=$(grep -E "^H2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
H3=$(grep -E "^H3" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
H4=$(grep -E "^H4" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
|
||||
echo "Variable: Jc=${JC:-5}"
|
||||
echo "Variable: JC=${JC:-5}"
|
||||
echo "Variable: JMIN=${JMIN:-100}"
|
||||
echo "Variable: Jmin=${JMIN:-100}"
|
||||
echo "Variable: JMAX=${JMAX:-200}"
|
||||
echo "Variable: Jmax=${JMAX:-200}"
|
||||
echo "Variable: S1=${S1:-50}"
|
||||
echo "Variable: S2=${S2:-100}"
|
||||
echo "Variable: H1=${H1:-1}"
|
||||
echo "Variable: H2=${H2:-2}"
|
||||
echo "Variable: H3=${H3:-3}"
|
||||
echo "Variable: H4=${H4:-4}"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate new config
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# Use WG_CONF delimiter to avoid EOF replacement in PHP
|
||||
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = 5
|
||||
Jmin = 100
|
||||
Jmax = 200
|
||||
S1 = 50
|
||||
S2 = 100
|
||||
H1 = 1
|
||||
H2 = 2
|
||||
H3 = 3
|
||||
H4 = 4
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
WG_CONF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey: $PRESHARED_KEY"
|
||||
|
||||
# Output variables for preview
|
||||
echo "Variable: server_port=$VPN_PORT"
|
||||
echo "Variable: server_public_key=$PUBLIC_KEY"
|
||||
echo "Variable: preshared_key=$PRESHARED_KEY"
|
||||
echo "Variable: server_host=YOUR_IP"
|
||||
|
||||
# Dummy client vars for preview
|
||||
CLIENT_PRIV_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
echo "Variable: private_key=$CLIENT_PRIV_KEY"
|
||||
echo "Variable: client_ip=10.8.1.2"
|
||||
echo "Variable: dns_servers=1.1.1.1"
|
||||
|
||||
# Obfuscation params (hardcoded in new config)
|
||||
echo "Variable: Jc=5"
|
||||
echo "Variable: JC=5"
|
||||
echo "Variable: Jmin=100"
|
||||
echo "Variable: JMIN=100"
|
||||
echo "Variable: Jmax=200"
|
||||
echo "Variable: JMAX=200"
|
||||
echo "Variable: S1=50"
|
||||
echo "Variable: S2=100"
|
||||
echo "Variable: H1=1"
|
||||
echo "Variable: H2=2"
|
||||
echo "Variable: H3=3"
|
||||
echo "Variable: H4=4"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Fix AmneziaWG MTU to 1280 for better compatibility
|
||||
-- This resolves connection issues with PPPoE, mobile networks, and tunnels
|
||||
UPDATE protocols SET
|
||||
install_script = REPLACE(install_script, 'MTU=${MTU:-1420}', 'MTU=${MTU:-1280}')
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,52 @@
|
||||
-- Fix AmneziaWG client config template: add MTU and use capital letter variables
|
||||
UPDATE protocols SET
|
||||
output_template = '[Interface]
|
||||
PrivateKey = {{private_key}}
|
||||
Address = {{client_ip}}/32
|
||||
DNS = {{dns_servers}}
|
||||
MTU = 1280
|
||||
Jc = {{Jc}}
|
||||
Jmin = {{Jmin}}
|
||||
Jmax = {{Jmax}}
|
||||
S1 = {{S1}}
|
||||
S2 = {{S2}}
|
||||
H1 = {{H1}}
|
||||
H2 = {{H2}}
|
||||
H3 = {{H3}}
|
||||
H4 = {{H4}}
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{server_public_key}}
|
||||
PresharedKey = {{preshared_key}}
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = {{server_host}}:{{server_port}}
|
||||
PersistentKeepalive = 25'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
|
||||
-- Create capital letter variables that map to lowercase ones
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'Jc', 'number', '5', 'Junk packet count', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='Jc');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'Jmin', 'number', '100', 'Minimum junk packet size', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='Jmin');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'Jmax', 'number', '200', 'Maximum junk packet size', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='Jmax');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'S1', 'number', '50', 'Junk packet size 1', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='S1');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'S2', 'number', '100', 'Junk packet size 2', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='S2');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'H1', 'number', '1', 'Obfuscation header 1', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='H1');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'H2', 'number', '2', 'Obfuscation header 2', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='H2');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'H3', 'number', '3', 'Obfuscation header 3', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='H3');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'H4', 'number', '4', 'Obfuscation header 4', false FROM protocols p WHERE p.slug='amnezia-wg-advanced' AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id=p.id AND variable_name='H4');
|
||||
@@ -0,0 +1,17 @@
|
||||
-- Remove fully uppercase variable outputs from AmneziaWG install script
|
||||
-- Keep only Jc, Jmin, Jmax format (first letter uppercase)
|
||||
UPDATE protocols SET
|
||||
install_script = REPLACE(
|
||||
REPLACE(
|
||||
REPLACE(
|
||||
install_script,
|
||||
'echo "Variable: JC=${JC:-5}"',
|
||||
''
|
||||
),
|
||||
'echo "Variable: JMIN=${JMIN:-100}"',
|
||||
''
|
||||
),
|
||||
'echo "Variable: JMAX=${JMAX:-200}"',
|
||||
''
|
||||
)
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE vpn_servers ADD COLUMN ssh_key TEXT NULL;
|
||||
@@ -0,0 +1,85 @@
|
||||
-- Fix X-Ray install script variable substitution
|
||||
-- The heredoc was preserving ${VAR} as literals instead of expanding them
|
||||
UPDATE protocols
|
||||
SET install_script = '#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
XRAY_PORT=${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
|
||||
# Ensure image present
|
||||
docker pull teddysun/xray >/dev/null 2>&1 || true
|
||||
|
||||
# Generate keys
|
||||
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
|
||||
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
PUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
|
||||
if [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then
|
||||
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
|
||||
fi
|
||||
|
||||
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
|
||||
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
|
||||
|
||||
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
|
||||
FINGERPRINT="${FINGERPRINT:-chrome}"
|
||||
SPIDER_X="${SPIDER_X:-/}"
|
||||
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
mkdir -p /opt/amnezia/xray
|
||||
|
||||
# Write config using printf to ensure variable expansion
|
||||
printf ''%s\\n'' "{
|
||||
\"log\": { \"loglevel\": \"warning\" },
|
||||
\"inbounds\": [
|
||||
{
|
||||
\"listen\": \"0.0.0.0\",
|
||||
\"port\": $XRAY_PORT,
|
||||
\"protocol\": \"vless\",
|
||||
\"settings\": {
|
||||
\"clients\": [ { \"id\": \"$CLIENT_ID\" } ],
|
||||
\"decryption\": \"none\"
|
||||
},
|
||||
\"streamSettings\": {
|
||||
\"network\": \"tcp\",
|
||||
\"security\": \"reality\",
|
||||
\"realitySettings\": {
|
||||
\"show\": false,
|
||||
\"dest\": \"$SERVER_NAME:443\",
|
||||
\"xver\": 0,
|
||||
\"serverNames\": [ \"$SERVER_NAME\" ],
|
||||
\"privateKey\": \"$PRIVATE_KEY\",
|
||||
\"shortIds\": [ \"$SHORT_ID\" ],
|
||||
\"fingerprint\": \"$FINGERPRINT\",
|
||||
\"spiderX\": \"$SPIDER_X\"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
\"outbounds\": [ { \"protocol\": \"freedom\", \"tag\": \"direct\" } ]
|
||||
}" > /opt/amnezia/xray/server.json
|
||||
|
||||
# Start container
|
||||
docker run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
-p "${XRAY_PORT}:${XRAY_PORT}" \
|
||||
-v /opt/amnezia/xray:/opt/amnezia/xray \
|
||||
teddysun/xray xray run -c /opt/amnezia/xray/server.json
|
||||
|
||||
sleep 2
|
||||
|
||||
# Output configuration
|
||||
echo "XrayPort: ${XRAY_PORT}"
|
||||
echo "Port: ${XRAY_PORT}"
|
||||
echo "ClientID: ${CLIENT_ID}"
|
||||
echo "PublicKey: ${PUBLIC_KEY}"
|
||||
echo "PrivateKey: ${PRIVATE_KEY}"
|
||||
echo "ShortID: ${SHORT_ID}"
|
||||
echo "ServerName: ${SERVER_NAME}"
|
||||
echo "ContainerName: ${CONTAINER_NAME}"'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Fix X-Ray install script JSON quotes
|
||||
-- Previous migration caused missing quotes in JSON because MySQL consumed one level of escaping
|
||||
-- We need \\\" in SQL to get \" in Bash, which echo outputs as " in the file
|
||||
UPDATE protocols
|
||||
SET install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
XRAY_PORT=${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
|
||||
# Ensure image present
|
||||
docker pull teddysun/xray >/dev/null 2>&1 || true
|
||||
|
||||
# Generate keys
|
||||
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
|
||||
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
PUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
|
||||
if [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then
|
||||
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
|
||||
fi
|
||||
|
||||
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
|
||||
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
|
||||
|
||||
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
|
||||
FINGERPRINT="${FINGERPRINT:-chrome}"
|
||||
SPIDER_X="${SPIDER_X:-/}"
|
||||
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
mkdir -p /opt/amnezia/xray
|
||||
|
||||
C="/opt/amnezia/xray/server.json"
|
||||
echo "{" > "$C"
|
||||
echo " \\\"log\\\": { \\\"loglevel\\\": \\\"warning\\\" }," >> "$C"
|
||||
echo " \\\"inbounds\\\": [" >> "$C"
|
||||
echo " {" >> "$C"
|
||||
echo " \\\"listen\\\": \\\"0.0.0.0\\\"," >> "$C"
|
||||
echo " \\\"port\\\": $XRAY_PORT," >> "$C"
|
||||
echo " \\\"protocol\\\": \\\"vless\\\"," >> "$C"
|
||||
echo " \\\"settings\\\": {" >> "$C"
|
||||
echo " \\\"clients\\\": [ { \\\"id\\\": \\\"$CLIENT_ID\\\" } ]," >> "$C"
|
||||
echo " \\\"decryption\\\": \\\"none\\\"" >> "$C"
|
||||
echo " }," >> "$C"
|
||||
echo " \\\"streamSettings\\\": {" >> "$C"
|
||||
echo " \\\"network\\\": \\\"tcp\\\"," >> "$C"
|
||||
echo " \\\"security\\\": \\\"reality\\\"," >> "$C"
|
||||
echo " \\\"realitySettings\\\": {" >> "$C"
|
||||
echo " \\\"show\\\": false," >> "$C"
|
||||
echo " \\\"dest\\\": \\\"$SERVER_NAME:443\\\"," >> "$C"
|
||||
echo " \\\"xver\\\": 0," >> "$C"
|
||||
echo " \\\"serverNames\\\": [ \\\"$SERVER_NAME\\\" ]," >> "$C"
|
||||
echo " \\\"privateKey\\\": \\\"$PRIVATE_KEY\\\"," >> "$C"
|
||||
echo " \\\"shortIds\\\": [ \\\"$SHORT_ID\\\" ]," >> "$C"
|
||||
echo " \\\"fingerprint\\\": \\\"$FINGERPRINT\\\"," >> "$C"
|
||||
echo " \\\"spiderX\\\": \\\"$SPIDER_X\\\"" >> "$C"
|
||||
echo " }" >> "$C"
|
||||
echo " }" >> "$C"
|
||||
echo " }" >> "$C"
|
||||
echo " ]," >> "$C"
|
||||
echo " \\\"outbounds\\\": [ { \\\"protocol\\\": \\\"freedom\\\", \\\"tag\\\": \\\"direct\\\" } ]" >> "$C"
|
||||
echo "}" >> "$C"
|
||||
|
||||
docker run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
-p "${XRAY_PORT}:${XRAY_PORT}" \
|
||||
-v /opt/amnezia/xray:/opt/amnezia/xray \
|
||||
teddysun/xray xray run -c /opt/amnezia/xray/server.json
|
||||
|
||||
sleep 2
|
||||
|
||||
echo "XrayPort: ${XRAY_PORT}"
|
||||
echo "Port: ${XRAY_PORT}"
|
||||
echo "ClientID: ${CLIENT_ID}"
|
||||
echo "PublicKey: ${PUBLIC_KEY}"
|
||||
echo "PrivateKey: ${PRIVATE_KEY}"
|
||||
echo "ShortID: ${SHORT_ID}"
|
||||
echo "ServerName: ${SERVER_NAME}"
|
||||
echo "ContainerName: ${CONTAINER_NAME}"'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,83 @@
|
||||
-- Fix X-Ray server config to include flow "xtls-rprx-vision"
|
||||
-- This parameter is present in the client config template but was missing in the server config
|
||||
-- causing connection failures with newer clients and X-Ray versions.
|
||||
UPDATE protocols
|
||||
SET install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
XRAY_PORT=${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
|
||||
# Ensure image present
|
||||
docker pull teddysun/xray >/dev/null 2>&1 || true
|
||||
|
||||
# Generate keys
|
||||
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
|
||||
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
PUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
|
||||
if [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then
|
||||
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
|
||||
fi
|
||||
|
||||
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
|
||||
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
|
||||
|
||||
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
|
||||
FINGERPRINT="${FINGERPRINT:-chrome}"
|
||||
SPIDER_X="${SPIDER_X:-/}"
|
||||
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
mkdir -p /opt/amnezia/xray
|
||||
|
||||
C="/opt/amnezia/xray/server.json"
|
||||
echo "{" > "$C"
|
||||
echo " \\\"log\\\": { \\\"loglevel\\\": \\\"warning\\\" }," >> "$C"
|
||||
echo " \\\"inbounds\\\": [" >> "$C"
|
||||
echo " {" >> "$C"
|
||||
echo " \\\"listen\\\": \\\"0.0.0.0\\\"," >> "$C"
|
||||
echo " \\\"port\\\": $XRAY_PORT," >> "$C"
|
||||
echo " \\\"protocol\\\": \\\"vless\\\"," >> "$C"
|
||||
echo " \\\"settings\\\": {" >> "$C"
|
||||
echo " \\\"clients\\\": [ { \\\"id\\\": \\\"$CLIENT_ID\\\", \\\"flow\\\": \\\"xtls-rprx-vision\\\" } ]," >> "$C"
|
||||
echo " \\\"decryption\\\": \\\"none\\\"" >> "$C"
|
||||
echo " }," >> "$C"
|
||||
echo " \\\"streamSettings\\\": {" >> "$C"
|
||||
echo " \\\"network\\\": \\\"tcp\\\"," >> "$C"
|
||||
echo " \\\"security\\\": \\\"reality\\\"," >> "$C"
|
||||
echo " \\\"realitySettings\\\": {" >> "$C"
|
||||
echo " \\\"show\\\": false," >> "$C"
|
||||
echo " \\\"dest\\\": \\\"$SERVER_NAME:443\\\"," >> "$C"
|
||||
echo " \\\"xver\\\": 0," >> "$C"
|
||||
echo " \\\"serverNames\\\": [ \\\"$SERVER_NAME\\\" ]," >> "$C"
|
||||
echo " \\\"privateKey\\\": \\\"$PRIVATE_KEY\\\"," >> "$C"
|
||||
echo " \\\"shortIds\\\": [ \\\"$SHORT_ID\\\" ]," >> "$C"
|
||||
echo " \\\"fingerprint\\\": \\\"$FINGERPRINT\\\"," >> "$C"
|
||||
echo " \\\"spiderX\\\": \\\"$SPIDER_X\\\"" >> "$C"
|
||||
echo " }" >> "$C"
|
||||
echo " }" >> "$C"
|
||||
echo " }" >> "$C"
|
||||
echo " ]," >> "$C"
|
||||
echo " \\\"outbounds\\\": [ { \\\"protocol\\\": \\\"freedom\\\", \\\"tag\\\": \\\"direct\\\" } ]" >> "$C"
|
||||
echo "}" >> "$C"
|
||||
|
||||
docker run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
-p "${XRAY_PORT}:${XRAY_PORT}" \
|
||||
-v /opt/amnezia/xray:/opt/amnezia/xray \
|
||||
teddysun/xray xray run -c /opt/amnezia/xray/server.json
|
||||
|
||||
sleep 2
|
||||
|
||||
echo "XrayPort: ${XRAY_PORT}"
|
||||
echo "Port: ${XRAY_PORT}"
|
||||
echo "ClientID: ${CLIENT_ID}"
|
||||
echo "PublicKey: ${PUBLIC_KEY}"
|
||||
echo "PrivateKey: ${PRIVATE_KEY}"
|
||||
echo "ShortID: ${SHORT_ID}"
|
||||
echo "ServerName: ${SERVER_NAME}"
|
||||
echo "ContainerName: ${CONTAINER_NAME}"'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,81 @@
|
||||
-- Fix X-Ray port to 443 to match Android client and avoid firewall issues.
|
||||
-- Previous usage of random ports caused connection failures on restricted networks.
|
||||
UPDATE protocols
|
||||
SET install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
|
||||
# Default to port 443 if SERVER_PORT is not provided
|
||||
XRAY_PORT=${SERVER_PORT:-443}
|
||||
|
||||
# Ensure image present
|
||||
docker pull teddysun/xray >/dev/null 2>&1 || true
|
||||
|
||||
# Generate keys
|
||||
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
|
||||
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
PUBLIC_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
|
||||
if [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then
|
||||
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
|
||||
fi
|
||||
|
||||
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
|
||||
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
|
||||
|
||||
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
|
||||
FINGERPRINT="${FINGERPRINT:-chrome}"
|
||||
SPIDER_X="${SPIDER_X:-/}"
|
||||
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
mkdir -p /opt/amnezia/xray
|
||||
|
||||
C="/opt/amnezia/xray/server.json"
|
||||
echo "{" > "$C"
|
||||
echo " \\\"log\\\": { \\\"loglevel\\\": \\\"warning\\\" }," >> "$C"
|
||||
echo " \\\"inbounds\\\": [" >> "$C"
|
||||
echo " {" >> "$C"
|
||||
echo " \\\"listen\\\": \\\"0.0.0.0\\\"," >> "$C"
|
||||
echo " \\\"port\\\": $XRAY_PORT," >> "$C"
|
||||
echo " \\\"protocol\\\": \\\"vless\\\"," >> "$C"
|
||||
echo " \\\"settings\\\": {" >> "$C"
|
||||
echo " \\\"clients\\\": [ { \\\"id\\\": \\\"$CLIENT_ID\\\", \\\"flow\\\": \\\"xtls-rprx-vision\\\" } ]," >> "$C"
|
||||
echo " \\\"decryption\\\": \\\"none\\\"" >> "$C"
|
||||
echo " }," >> "$C"
|
||||
echo " \\\"streamSettings\\\": {" >> "$C"
|
||||
echo " \\\"network\\\": \\\"tcp\\\"," >> "$C"
|
||||
echo " \\\"security\\\": \\\"reality\\\"," >> "$C"
|
||||
echo " \\\"realitySettings\\\": {" >> "$C"
|
||||
echo " \\\"show\\\": false," >> "$C"
|
||||
echo " \\\"dest\\\": \\\"$SERVER_NAME:443\\\"," >> "$C"
|
||||
echo " \\\"xver\\\": 0," >> "$C"
|
||||
echo " \\\"serverNames\\\": [ \\\"$SERVER_NAME\\\" ]," >> "$C"
|
||||
echo " \\\"privateKey\\\": \\\"$PRIVATE_KEY\\\"," >> "$C"
|
||||
echo " \\\"shortIds\\\": [ \\\"$SHORT_ID\\\" ]," >> "$C"
|
||||
echo " \\\"fingerprint\\\": \\\"$FINGERPRINT\\\"," >> "$C"
|
||||
echo " \\\"spiderX\\\": \\\"$SPIDER_X\\\"" >> "$C"
|
||||
echo " }" >> "$C"
|
||||
echo " }" >> "$C"
|
||||
echo " }" >> "$C"
|
||||
echo " ]," >> "$C"
|
||||
echo " \\\"outbounds\\\": [ { \\\"protocol\\\": \\\"freedom\\\", \\\"tag\\\": \\\"direct\\\" } ]" >> "$C"
|
||||
echo "}" >> "$C"
|
||||
|
||||
docker run -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
--restart always \
|
||||
-p "${XRAY_PORT}:${XRAY_PORT}" \
|
||||
-v /opt/amnezia/xray:/opt/amnezia/xray \
|
||||
teddysun/xray xray run -c /opt/amnezia/xray/server.json
|
||||
|
||||
sleep 2
|
||||
|
||||
echo "XrayPort: ${XRAY_PORT}"
|
||||
echo "Port: ${XRAY_PORT}"
|
||||
echo "ClientID: ${CLIENT_ID}"
|
||||
echo "PublicKey: ${PUBLIC_KEY}"
|
||||
echo "PrivateKey: ${PRIVATE_KEY}"
|
||||
echo "ShortID: ${SHORT_ID}"
|
||||
echo "ServerName: ${SERVER_NAME}"
|
||||
echo "ContainerName: ${CONTAINER_NAME}"'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,77 @@
|
||||
-- Fix X-Ray install script:
|
||||
-- 1) Use single-line docker run (backslash continuations break in MySQL)
|
||||
-- 2) Handle new xray x25519 output format (Password instead of Public key)
|
||||
UPDATE protocols
|
||||
SET install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
|
||||
XRAY_PORT=${SERVER_PORT:-443}
|
||||
|
||||
docker pull teddysun/xray >/dev/null 2>&1 || true
|
||||
|
||||
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
|
||||
PRIVATE_KEY=$(printf "%s\n" "$GEN" | sed -n -E "s/^[Pp]rivate[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
if [ -z "$PRIVATE_KEY" ]; then
|
||||
PRIVATE_KEY=$(printf "%s\n" "$GEN" | grep -i "private" | head -1 | sed "s/.*:[[:space:]]*//" | tr -d " \\t\\r\\n")
|
||||
fi
|
||||
PUBLIC_KEY=$(printf "%s\n" "$GEN" | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
if [ -z "$PUBLIC_KEY" ]; then
|
||||
PUBLIC_KEY=$(printf "%s\n" "$GEN" | sed -n -E "s/^[Pp]assword:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
fi
|
||||
if [ -z "$PUBLIC_KEY" ] && [ -n "$PRIVATE_KEY" ]; then
|
||||
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]assword:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
|
||||
fi
|
||||
|
||||
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
|
||||
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
|
||||
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
|
||||
FINGERPRINT="${FINGERPRINT:-chrome}"
|
||||
SPIDER_X="${SPIDER_X:-/}"
|
||||
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
mkdir -p /opt/amnezia/xray
|
||||
|
||||
cat > /opt/amnezia/xray/server.json <<EOJSON
|
||||
{
|
||||
"log": { "loglevel": "warning" },
|
||||
"inbounds": [{
|
||||
"listen": "0.0.0.0",
|
||||
"port": ${XRAY_PORT},
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"clients": [{ "id": "${CLIENT_ID}", "flow": "xtls-rprx-vision" }],
|
||||
"decryption": "none"
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"show": false,
|
||||
"dest": "${SERVER_NAME}:443",
|
||||
"xver": 0,
|
||||
"serverNames": ["${SERVER_NAME}"],
|
||||
"privateKey": "${PRIVATE_KEY}",
|
||||
"shortIds": ["${SHORT_ID}"],
|
||||
"fingerprint": "${FINGERPRINT}",
|
||||
"spiderX": "${SPIDER_X}"
|
||||
}
|
||||
}
|
||||
}],
|
||||
"outbounds": [{ "protocol": "freedom", "tag": "direct" }]
|
||||
}
|
||||
EOJSON
|
||||
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always -p "${XRAY_PORT}:${XRAY_PORT}" -v /opt/amnezia/xray:/opt/amnezia/xray teddysun/xray xray run -c /opt/amnezia/xray/server.json
|
||||
|
||||
sleep 2
|
||||
|
||||
echo "XrayPort: ${XRAY_PORT}"
|
||||
echo "Port: ${XRAY_PORT}"
|
||||
echo "ClientID: ${CLIENT_ID}"
|
||||
echo "PublicKey: ${PUBLIC_KEY}"
|
||||
echo "PrivateKey: ${PRIVATE_KEY}"
|
||||
echo "ShortID: ${SHORT_ID}"
|
||||
echo "ServerName: ${SERVER_NAME}"
|
||||
echo "ContainerName: ${CONTAINER_NAME}"'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,64 @@
|
||||
-- Safely update protocols table schema and data
|
||||
|
||||
-- 1. Ensure columns exist
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = "protocols";
|
||||
SET @columnname = "definition";
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(table_name = @tablename)
|
||||
AND (table_schema = @dbname)
|
||||
AND (column_name = @columnname)
|
||||
) > 0,
|
||||
"SELECT 1",
|
||||
"ALTER TABLE protocols ADD COLUMN definition JSON NULL AFTER description"
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
SET @columnname = "show_text_content";
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(table_name = @tablename)
|
||||
AND (table_schema = @dbname)
|
||||
AND (column_name = @columnname)
|
||||
) > 0,
|
||||
"SELECT 1",
|
||||
"ALTER TABLE protocols ADD COLUMN show_text_content TINYINT(1) DEFAULT 0 AFTER definition"
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
|
||||
-- 2. Insert Data (amnezia-wg removed - use amnezia-wg-advanced instead)
|
||||
INSERT IGNORE INTO protocols (slug, name, description, definition, show_text_content, is_active) VALUES
|
||||
('wireguard', 'WireGuard', 'Standard WireGuard', '{}', 0, 1),
|
||||
('openvpn', 'OpenVPN', 'Standard OpenVPN', '{}', 0, 1),
|
||||
('shadowsocks', 'Shadowsocks', 'Shadowsocks proxy', '{}', 0, 1),
|
||||
('cloak', 'Cloak', 'Cloak obfuscation', '{}', 0, 1);
|
||||
|
||||
-- 3. Update vpn_clients structure (original logic from migration)
|
||||
SET @exist := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='vpn_clients' AND COLUMN_NAME='protocol_id');
|
||||
SET @sql := IF(@exist=0, 'ALTER TABLE vpn_clients ADD COLUMN protocol_id INT UNSIGNED NULL AFTER server_id, ADD INDEX idx_protocol_id (protocol_id), ADD CONSTRAINT fk_clients_protocol FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE SET NULL', 'SELECT "Column protocol_id exists"');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 4. Create server_protocols if not exists
|
||||
CREATE TABLE IF NOT EXISTS server_protocols (
|
||||
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||
server_id INT UNSIGNED NOT NULL,
|
||||
protocol_id INT UNSIGNED NOT NULL,
|
||||
config_data JSON,
|
||||
container_id VARCHAR(255) NULL,
|
||||
applied_at TIMESTAMP NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_server_proto (server_id, protocol_id),
|
||||
FOREIGN KEY (server_id) REFERENCES vpn_servers(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (protocol_id) REFERENCES protocols(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,123 @@
|
||||
-- Enable Stats and API for XRay VLESS protocol
|
||||
-- This allows collecting traffic usage per user
|
||||
-- Supports restoration of existing keys via environment variables
|
||||
|
||||
UPDATE protocols SET install_script = '#!/bin/bash
|
||||
set -eu
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
|
||||
XRAY_PORT=${SERVER_PORT:-443}
|
||||
|
||||
docker pull teddysun/xray >/dev/null 2>&1 || true
|
||||
|
||||
# Use existing keys if provided, otherwise generate new ones
|
||||
if [ -z "${PRIVATE_KEY:-}" ]; then
|
||||
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
|
||||
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
if [ -z "$PRIVATE_KEY" ]; then
|
||||
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | grep -i "private" | head -1 | sed "s/.*:[[:space:]]*//" | tr -d " \\t\\r\\n")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Derive public key from private key
|
||||
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
|
||||
if [ -z "$PUBLIC_KEY" ]; then
|
||||
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]assword:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
|
||||
fi
|
||||
|
||||
# Use existing short_id or generate new one
|
||||
if [ -z "${SHORT_ID:-}" ]; then
|
||||
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
|
||||
fi
|
||||
|
||||
# Use existing client_id or generate new one
|
||||
if [ -z "${CLIENT_ID:-}" ]; then
|
||||
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
|
||||
fi
|
||||
|
||||
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
|
||||
FINGERPRINT="${FINGERPRINT:-chrome}"
|
||||
SPIDER_X="${SPIDER_X:-/}"
|
||||
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
mkdir -p /opt/amnezia/xray
|
||||
|
||||
cat > /opt/amnezia/xray/server.json <<EOJSON
|
||||
{
|
||||
"log": { "loglevel": "warning" },
|
||||
"stats": {},
|
||||
"api": {
|
||||
"tag": "api",
|
||||
"services": [ "StatsService" ]
|
||||
},
|
||||
"policy": {
|
||||
"levels": {
|
||||
"0": {
|
||||
"statsUserUplink": true,
|
||||
"statsUserDownlink": true
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsInboundUplink": true,
|
||||
"statsInboundDownlink": true
|
||||
}
|
||||
},
|
||||
"inbounds": [{
|
||||
"listen": "0.0.0.0",
|
||||
"port": ${XRAY_PORT},
|
||||
"protocol": "vless",
|
||||
"settings": {
|
||||
"clients": [{ "id": "${CLIENT_ID}", "flow": "xtls-rprx-vision", "email": "${CLIENT_ID}" }],
|
||||
"decryption": "none"
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"show": false,
|
||||
"dest": "${SERVER_NAME}:443",
|
||||
"xver": 0,
|
||||
"serverNames": ["${SERVER_NAME}"],
|
||||
"privateKey": "${PRIVATE_KEY}",
|
||||
"shortIds": ["${SHORT_ID}"],
|
||||
"fingerprint": "${FINGERPRINT}",
|
||||
"spiderX": "${SPIDER_X}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"port": 10085,
|
||||
"protocol": "dokodemo-door",
|
||||
"tag": "api",
|
||||
"settings": {
|
||||
"address": "127.0.0.1"
|
||||
}
|
||||
}],
|
||||
"outbounds": [{ "protocol": "freedom", "tag": "direct" }],
|
||||
"routing": {
|
||||
"rules": [
|
||||
{
|
||||
"inboundTag": [ "api" ],
|
||||
"outboundTag": "api",
|
||||
"type": "field"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
EOJSON
|
||||
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always -p "${XRAY_PORT}:${XRAY_PORT}" -v /opt/amnezia/xray:/opt/amnezia/xray teddysun/xray xray run -c /opt/amnezia/xray/server.json
|
||||
|
||||
sleep 2
|
||||
|
||||
echo "XrayPort: ${XRAY_PORT}"
|
||||
echo "Port: ${XRAY_PORT}"
|
||||
echo "ClientID: ${CLIENT_ID}"
|
||||
echo "PublicKey: ${PUBLIC_KEY}"
|
||||
echo "PrivateKey: ${PRIVATE_KEY}"
|
||||
echo "ShortID: ${SHORT_ID}"
|
||||
echo "ServerName: ${SERVER_NAME}"
|
||||
echo "ContainerName: ${CONTAINER_NAME}"
|
||||
'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,20 @@
|
||||
-- Add dns_servers column to vpn_servers table if missing
|
||||
-- Needed for correct configuration regeneration
|
||||
|
||||
SET @dbname = DATABASE();
|
||||
SET @tablename = "vpn_servers";
|
||||
SET @columnname = "dns_servers";
|
||||
SET @preparedStatement = (SELECT IF(
|
||||
(
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
(table_name = @tablename)
|
||||
AND (table_schema = @dbname)
|
||||
AND (column_name = @columnname)
|
||||
) > 0,
|
||||
"SELECT 1",
|
||||
"ALTER TABLE vpn_servers ADD COLUMN dns_servers VARCHAR(255) DEFAULT '1.1.1.1, 1.0.0.1'"
|
||||
));
|
||||
PREPARE alterIfNotExists FROM @preparedStatement;
|
||||
EXECUTE alterIfNotExists;
|
||||
DEALLOCATE PREPARE alterIfNotExists;
|
||||
@@ -0,0 +1,150 @@
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Ensure host directory exists for persistence
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Function to check if container is healthy
|
||||
check_container() {
|
||||
local status
|
||||
status=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "missing")
|
||||
if [ "$status" = "running" ]; then
|
||||
return 0
|
||||
elif [ "$status" = "restarting" ]; then
|
||||
return 2 # Restarting loop
|
||||
else
|
||||
return 1 # Stopped or missing
|
||||
fi
|
||||
}
|
||||
|
||||
# Validate existing config
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
# Check for broken config
|
||||
if grep -Fq ''$PRIVATE_KEY'' /opt/amnezia/awg/wg0.conf; then
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf; then
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for invalid hex parameters H
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf; then
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
# Check for insecure defaults (1, 2, 3, 4)
|
||||
if grep -Eiq "^H1[[:space:]]*=[[:space:]]*1$" /opt/amnezia/awg/wg0.conf; then
|
||||
# Only remove if H2=2 etc also match? Or just safe to regenerate if H1=1 (insecure)
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for existing configuration on HOST first (preferred persistence)
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration on host."
|
||||
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
VPN_PORT=${PORT:-$VPN_PORT}
|
||||
|
||||
STATUS=0
|
||||
check_container || STATUS=$?
|
||||
|
||||
if [ $STATUS -ne 0 ]; then
|
||||
echo "Starting container..."
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 2
|
||||
fi
|
||||
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Port: $VPN_PORT"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey: $PSK"; fi
|
||||
|
||||
# Output variables for preview
|
||||
echo "Variable: server_port=$VPN_PORT"
|
||||
echo "Variable: server_public_key=$PUBKEY"
|
||||
echo "Variable: preshared_key=$PSK"
|
||||
|
||||
# Extract actual params
|
||||
JC=$(grep -E "^Jc" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
H1=$(grep -E "^H1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
echo "Variable: Jc=${JC:-5}"
|
||||
echo "Variable: H1=${H1:-$((RANDOM * 1000 + RANDOM))}"
|
||||
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate new config
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# Generate Random Obfuscation Params
|
||||
JC=$(( (RANDOM % 8) + 3 ))
|
||||
JMIN=50
|
||||
JMAX=$(( (RANDOM % 500) + 500 ))
|
||||
S1=$(( (RANDOM % 150) + 50 ))
|
||||
S2=$(( (RANDOM % 150) + 50 ))
|
||||
# Using od for larger range 32-bit ints
|
||||
H1=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d "[:space:]")
|
||||
H2=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d "[:space:]")
|
||||
H3=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d "[:space:]")
|
||||
H4=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d "[:space:]")
|
||||
|
||||
cat > /opt/amnezia/awg/wg0.conf << WG_CONF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = $JC
|
||||
Jmin = $JMIN
|
||||
Jmax = $JMAX
|
||||
S1 = $S1
|
||||
S2 = $S2
|
||||
H1 = $H1
|
||||
H2 = $H2
|
||||
H3 = $H3
|
||||
H4 = $H4
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
WG_CONF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey: $PRESHARED_KEY"
|
||||
|
||||
echo "Variable: server_port=$VPN_PORT"
|
||||
echo "Variable: server_public_key=$PUBLIC_KEY"
|
||||
echo "Variable: preshared_key=$PRESHARED_KEY"
|
||||
echo "Variable: Jc=$JC"
|
||||
echo "Variable: Jmin=$JMIN"
|
||||
echo "Variable: H1=$H1"
|
||||
echo "Variable: H2=$H2"
|
||||
echo "Variable: H3=$H3"
|
||||
echo "Variable: H4=$H4"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1,155 @@
|
||||
-- Fix AWG Advanced install script: create container BEFORE generating keys
|
||||
-- The issue was that the script tried to call docker exec wg genkey before the container existed
|
||||
UPDATE protocols SET
|
||||
install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT=${VPN_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
mkdir -p /opt/amnezia/awg
|
||||
|
||||
# Check if container exists and is running
|
||||
container_running() {
|
||||
docker inspect --format="{{.State.Running}}" "$CONTAINER_NAME" 2>/dev/null | grep -q true
|
||||
}
|
||||
|
||||
# Clean up broken configs
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
if grep -q "PRIVATE_KEY" /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
if grep -Eiq "^S3[[:space:]]*=" /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
if grep -Eiq "^H[1-4][[:space:]]*=[[:space:]]*0x" /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
if grep -Eiq "^H1[[:space:]]*=[[:space:]]*1$" /opt/amnezia/awg/wg0.conf 2>/dev/null; then
|
||||
rm -f /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
fi
|
||||
|
||||
# If valid config exists, just ensure container is running
|
||||
if [ -f /opt/amnezia/awg/wg0.conf ]; then
|
||||
echo "Found existing configuration"
|
||||
VPN_PORT=$(grep -E "^ListenPort" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " " || echo "$VPN_PORT")
|
||||
|
||||
if ! container_running; then
|
||||
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 3
|
||||
# Force reload interface to apply AWG params
|
||||
docker exec "$CONTAINER_NAME" ip link del wg0 2>/dev/null || true
|
||||
docker exec "$CONTAINER_NAME" wg-quick up /opt/amnezia/awg/wg0.conf
|
||||
fi
|
||||
|
||||
PUBKEY=$(cat /opt/amnezia/awg/wireguard_server_public_key.key 2>/dev/null || echo "")
|
||||
PSK=$(cat /opt/amnezia/awg/wireguard_psk.key 2>/dev/null || echo "")
|
||||
JC=$(grep -E "^Jc" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
|
||||
JMIN=$(grep -E "^Jmin" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
|
||||
JMAX=$(grep -E "^Jmax" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
|
||||
S1=$(grep -E "^S1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
|
||||
S2=$(grep -E "^S2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
|
||||
H1=$(grep -E "^H1" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
|
||||
H2=$(grep -E "^H2" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
|
||||
H3=$(grep -E "^H3" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
|
||||
H4=$(grep -E "^H4" /opt/amnezia/awg/wg0.conf | cut -d= -f2 | tr -d " ")
|
||||
|
||||
echo "Using existing AmneziaWG configuration"
|
||||
echo "Variable: server_port=$VPN_PORT"
|
||||
echo "Variable: server_public_key=$PUBKEY"
|
||||
echo "Variable: preshared_key=$PSK"
|
||||
echo "Variable: container_name=$CONTAINER_NAME"
|
||||
echo "Variable: Jc=$JC"
|
||||
echo "Variable: Jmin=$JMIN"
|
||||
echo "Variable: Jmax=$JMAX"
|
||||
echo "Variable: S1=$S1"
|
||||
echo "Variable: S2=$S2"
|
||||
echo "Variable: H1=$H1"
|
||||
echo "Variable: H2=$H2"
|
||||
echo "Variable: H3=$H3"
|
||||
echo "Variable: H4=$H4"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# FRESH INSTALL
|
||||
echo "Starting fresh AmneziaWG Advanced installation..."
|
||||
|
||||
# Remove old container if exists
|
||||
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
|
||||
|
||||
# Start container FIRST so we can use wg tools inside it
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --privileged --cap-add=NET_ADMIN --cap-add=SYS_MODULE -p "${VPN_PORT}:${VPN_PORT}/udp" -v /lib/modules:/lib/modules -v /opt/amnezia/awg:/opt/amnezia/awg amneziavpn/amnezia-wg:latest sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; wg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 3
|
||||
|
||||
# Generate keys using the container
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# Generate random obfuscation parameters
|
||||
JC=$((RANDOM % 8 + 3))
|
||||
JMIN=50
|
||||
JMAX=$((RANDOM % 500 + 500))
|
||||
S1=$((RANDOM % 150 + 50))
|
||||
S2=$((RANDOM % 150 + 50))
|
||||
H1=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d " ")
|
||||
H2=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d " ")
|
||||
H3=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d " ")
|
||||
H4=$(od -vAn -N4 -tu4 < /dev/urandom | tr -d " ")
|
||||
|
||||
# Create config file
|
||||
cat > /opt/amnezia/awg/wg0.conf << WGCONF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = $JC
|
||||
Jmin = $JMIN
|
||||
Jmax = $JMAX
|
||||
S1 = $S1
|
||||
S2 = $S2
|
||||
H1 = $H1
|
||||
H2 = $H2
|
||||
H3 = $H3
|
||||
H4 = $H4
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
WGCONF
|
||||
|
||||
# Save keys
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg/clientsTable
|
||||
|
||||
# Restart container and explicitly reload WG interface to apply AWG params
|
||||
docker restart "$CONTAINER_NAME"
|
||||
sleep 2
|
||||
|
||||
# CRITICAL: Force reload interface to apply AWG obfuscation parameters
|
||||
docker exec "$CONTAINER_NAME" ip link del wg0 2>/dev/null || true
|
||||
docker exec "$CONTAINER_NAME" wg-quick up /opt/amnezia/awg/wg0.conf
|
||||
sleep 1
|
||||
|
||||
echo "AmneziaWG Advanced installed successfully"
|
||||
echo "Variable: server_port=$VPN_PORT"
|
||||
echo "Variable: server_public_key=$PUBLIC_KEY"
|
||||
echo "Variable: preshared_key=$PRESHARED_KEY"
|
||||
echo "Variable: container_name=$CONTAINER_NAME"
|
||||
echo "Variable: Jc=$JC"
|
||||
echo "Variable: Jmin=$JMIN"
|
||||
echo "Variable: Jmax=$JMAX"
|
||||
echo "Variable: S1=$S1"
|
||||
echo "Variable: S2=$S2"
|
||||
echo "Variable: H1=$H1"
|
||||
echo "Variable: H2=$H2"
|
||||
echo "Variable: H3=$H3"
|
||||
echo "Variable: H4=$H4"
|
||||
'
|
||||
WHERE slug = 'amnezia-wg-advanced';
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE vpn_clients ADD COLUMN current_speed BIGINT DEFAULT 0 AFTER traffic_limit;
|
||||
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE vpn_clients ADD COLUMN speed_up BIGINT DEFAULT 0 AFTER current_speed;
|
||||
ALTER TABLE vpn_clients ADD COLUMN speed_down BIGINT DEFAULT 0 AFTER speed_up;
|
||||
-- We can drop current_speed later or keep it as total
|
||||
@@ -0,0 +1,131 @@
|
||||
-- Enable single IP enforcement for XRay VLESS protocol
|
||||
-- Adds:
|
||||
-- 1. statsUserOnline for tracking online connections
|
||||
-- 2. RoutingService for dynamic IP blocking
|
||||
-- 3. blocked outbound (blackhole) for dropping unwanted traffic
|
||||
-- 4. vless-in tag on main inbound for targeting rules
|
||||
|
||||
UPDATE protocols SET install_script = '#!/bin/bash
|
||||
set -eu
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-xray}"
|
||||
XRAY_PORT=${SERVER_PORT:-443}
|
||||
|
||||
docker pull teddysun/xray >/dev/null 2>&1 || true
|
||||
|
||||
# Use existing keys if provided, otherwise generate new ones
|
||||
if [ -z "${PRIVATE_KEY:-}" ]; then
|
||||
GEN=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 2>/dev/null || true)
|
||||
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | sed -n -E "s/^[Pp]rivate[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n")
|
||||
if [ -z "$PRIVATE_KEY" ]; then
|
||||
PRIVATE_KEY=$(printf "%s\\n" "$GEN" | grep -i "private" | head -1 | sed "s/.*:[[:space:]]*//" | tr -d " \\t\\r\\n")
|
||||
fi
|
||||
fi
|
||||
|
||||
# Derive public key from private key
|
||||
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]ublic[[:space:]]*[Kk]ey:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
|
||||
if [ -z "$PUBLIC_KEY" ]; then
|
||||
PUBLIC_KEY=$(docker run --rm --entrypoint /usr/bin/xray teddysun/xray x25519 -i "$PRIVATE_KEY" 2>/dev/null | sed -n -E "s/^[Pp]assword:[[:space:]]*(.*)$/\\1/p" | tr -d " \\t\\r\\n" || true)
|
||||
fi
|
||||
|
||||
# Use existing short_id or generate new one
|
||||
if [ -z "${SHORT_ID:-}" ]; then
|
||||
SHORT_ID=$(od -An -tx1 -N8 /dev/urandom | tr -d " \\n")
|
||||
fi
|
||||
|
||||
# Use existing client_id or generate new one
|
||||
if [ -z "${CLIENT_ID:-}" ]; then
|
||||
CLIENT_ID=$(cat /proc/sys/kernel/random/uuid)
|
||||
fi
|
||||
|
||||
SERVER_NAME="${SERVER_NAME:-www.googletagmanager.com}"
|
||||
FINGERPRINT="${FINGERPRINT:-chrome}"
|
||||
SPIDER_X="${SPIDER_X:-/}"
|
||||
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
mkdir -p /opt/amnezia/xray
|
||||
|
||||
cat > /opt/amnezia/xray/server.json <<EOJSON
|
||||
{
|
||||
"log": { "loglevel": "warning" },
|
||||
"stats": {},
|
||||
"api": {
|
||||
"tag": "api",
|
||||
"services": [ "StatsService", "RoutingService" ]
|
||||
},
|
||||
"policy": {
|
||||
"levels": {
|
||||
"0": {
|
||||
"statsUserUplink": true,
|
||||
"statsUserDownlink": true,
|
||||
"statsUserOnline": true
|
||||
}
|
||||
},
|
||||
"system": {
|
||||
"statsInboundUplink": true,
|
||||
"statsInboundDownlink": true
|
||||
}
|
||||
},
|
||||
"inbounds": [{
|
||||
"listen": "0.0.0.0",
|
||||
"port": ${XRAY_PORT},
|
||||
"protocol": "vless",
|
||||
"tag": "vless-in",
|
||||
"settings": {
|
||||
"clients": [{ "id": "${CLIENT_ID}", "flow": "xtls-rprx-vision", "email": "${CLIENT_ID}", "level": 0 }],
|
||||
"decryption": "none"
|
||||
},
|
||||
"streamSettings": {
|
||||
"network": "tcp",
|
||||
"security": "reality",
|
||||
"realitySettings": {
|
||||
"show": false,
|
||||
"dest": "${SERVER_NAME}:443",
|
||||
"xver": 0,
|
||||
"serverNames": ["${SERVER_NAME}"],
|
||||
"privateKey": "${PRIVATE_KEY}",
|
||||
"shortIds": ["${SHORT_ID}"],
|
||||
"fingerprint": "${FINGERPRINT}",
|
||||
"spiderX": "${SPIDER_X}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"listen": "127.0.0.1",
|
||||
"port": 10085,
|
||||
"protocol": "dokodemo-door",
|
||||
"tag": "api",
|
||||
"settings": {
|
||||
"address": "127.0.0.1"
|
||||
}
|
||||
}],
|
||||
"outbounds": [
|
||||
{ "protocol": "freedom", "tag": "direct" },
|
||||
{ "protocol": "blackhole", "tag": "blocked" }
|
||||
],
|
||||
"routing": {
|
||||
"rules": [
|
||||
{
|
||||
"inboundTag": [ "api" ],
|
||||
"outboundTag": "api",
|
||||
"type": "field"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
EOJSON
|
||||
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always -p "${XRAY_PORT}:${XRAY_PORT}" -v /opt/amnezia/xray:/opt/amnezia/xray teddysun/xray xray run -c /opt/amnezia/xray/server.json
|
||||
|
||||
sleep 2
|
||||
|
||||
echo "XrayPort: ${XRAY_PORT}"
|
||||
echo "Port: ${XRAY_PORT}"
|
||||
echo "ClientID: ${CLIENT_ID}"
|
||||
echo "PublicKey: ${PUBLIC_KEY}"
|
||||
echo "PrivateKey: ${PRIVATE_KEY}"
|
||||
echo "ShortID: ${SHORT_ID}"
|
||||
echo "ServerName: ${SERVER_NAME}"
|
||||
echo "ContainerName: ${CONTAINER_NAME}"
|
||||
'
|
||||
WHERE slug = 'xray-vless';
|
||||
@@ -0,0 +1,9 @@
|
||||
-- Add translation for dashboard.online_now
|
||||
INSERT INTO translations (`locale`, `category`, `key_name`, `translation`) VALUES
|
||||
('en', 'dashboard', 'online_now', 'Online Now'),
|
||||
('ru', 'dashboard', 'online_now', 'Сейчас онлайн'),
|
||||
('es', 'dashboard', 'online_now', 'En línea ahora'),
|
||||
('de', 'dashboard', 'online_now', 'Jetzt online'),
|
||||
('fr', 'dashboard', 'online_now', 'En ligne maintenant'),
|
||||
('zh', 'dashboard', 'online_now', '当前在线')
|
||||
ON DUPLICATE KEY UPDATE `translation` = VALUES(`translation`);
|
||||
@@ -0,0 +1,5 @@
|
||||
-- Enable text content display by default on client page for XRay VLESS
|
||||
UPDATE protocols
|
||||
SET show_text_content = 1
|
||||
WHERE slug = 'xray-vless'
|
||||
AND COALESCE(show_text_content, 0) <> 1;
|
||||
@@ -0,0 +1,31 @@
|
||||
-- Add missing translations for protocol management UI (EN/RU)
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en', 'protocols', 'management', 'Protocol Management'),
|
||||
('ru', 'protocols', 'management', 'Управление протоколами'),
|
||||
('en', 'protocols', 'management_description', 'Configure and manage VPN protocols'),
|
||||
('ru', 'protocols', 'management_description', 'Настройка и управление VPN-протоколами'),
|
||||
('en', 'common', 'active', 'Active'),
|
||||
('ru', 'common', 'active', 'Активный'),
|
||||
('en', 'common', 'inactive', 'Inactive'),
|
||||
('ru', 'common', 'inactive', 'Неактивный'),
|
||||
('en', 'protocols', 'add_protocol', 'Add Protocol'),
|
||||
('ru', 'protocols', 'add_protocol', 'Добавить протокол'),
|
||||
('en', 'common', 'settings', 'Settings'),
|
||||
('ru', 'common', 'settings', 'Настройки'),
|
||||
('en', 'protocols', 'available_protocols', 'Available Protocols'),
|
||||
('ru', 'protocols', 'available_protocols', 'Доступные протоколы'),
|
||||
('en', 'protocols', 'search_protocols', 'Search protocols'),
|
||||
('ru', 'protocols', 'search_protocols', 'Поиск протоколов'),
|
||||
('en', 'protocols', 'all_protocols', 'All Protocols'),
|
||||
('ru', 'protocols', 'all_protocols', 'Все протоколы'),
|
||||
('en', 'protocols', 'active_only', 'Active only'),
|
||||
('ru', 'protocols', 'active_only', 'Только активные'),
|
||||
('en', 'protocols', 'with_ai_generations', 'With AI generations'),
|
||||
('ru', 'protocols', 'with_ai_generations', 'С AI-генерациями')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
|
||||
-- Hide protocols that should not be published
|
||||
UPDATE protocols
|
||||
SET is_active = 0
|
||||
WHERE slug IN ('cloak', 'openvpn', 'shadowsocks', 'wireguard', 'wireguard-standard')
|
||||
OR name IN ('Cloak', 'OpenVPN', 'Shadowsocks', 'WireGuard', 'WireGuard Standard');
|
||||
@@ -0,0 +1,371 @@
|
||||
-- =====================================================================
|
||||
-- Migration 058: Add AmneziaWG 2.0 protocol (amneziawg-go userspace)
|
||||
-- Uses amneziawg-go (Go userspace) instead of kernel module
|
||||
-- https://github.com/amnezia-vpn/amneziawg-go
|
||||
-- =====================================================================
|
||||
|
||||
-- 1. Insert the protocol entry (clone output_template from amnezia-wg-advanced)
|
||||
INSERT INTO protocols (name, slug, description, install_script, uninstall_script, output_template, ubuntu_compatible, is_active, definition, created_at, updated_at)
|
||||
SELECT
|
||||
'AmneziaWG 2.0',
|
||||
'awg2',
|
||||
'AmneziaWG 2.0 — userspace Go implementation (amneziawg-go). No kernel module required.',
|
||||
'#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Use exported variables from panel (SERVER_PORT, SERVER_CONTAINER) or defaults
|
||||
CONTAINER_NAME="${SERVER_CONTAINER:-amnezia-awg2}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT="${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}"
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
# Install git if not available
|
||||
if ! command -v git &> /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq git >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
mkdir -p /opt/amnezia/awg2
|
||||
|
||||
# Clone amneziawg-go source for Docker build
|
||||
if [ ! -d /opt/amnezia/awg2/src ]; then
|
||||
git clone --depth=1 https://github.com/amnezia-vpn/amneziawg-go.git /opt/amnezia/awg2/src
|
||||
fi
|
||||
|
||||
# Build Docker image using the repo Dockerfile (multi-stage: Go compile + tools)
|
||||
docker build --no-cache -t amnezia-awg2 /opt/amnezia/awg2/src
|
||||
|
||||
# Run container (userspace: no SYS_MODULE, no /lib/modules)
|
||||
EXISTING=$(docker ps -aq -f "name=$CONTAINER_NAME" 2>/dev/null | head -1)
|
||||
if [ -z "$EXISTING" ]; then
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --cap-add=NET_ADMIN --device /dev/net/tun -p "${VPN_PORT}:${VPN_PORT}/udp" -v /opt/amnezia/awg2:/opt/amnezia/awg amnezia-awg2 sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; WG_QUICK_USERSPACE_IMPLEMENTATION=amneziawg-go awg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
|
||||
sleep 2
|
||||
else
|
||||
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "")
|
||||
if [ \"$STATUS\" != \"running\" ]; then
|
||||
docker start \"$CONTAINER_NAME\" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check for existing config
|
||||
if [ -f /opt/amnezia/awg2/wg0.conf ]; then
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg2/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg2/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg2/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG 2.0 configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
|
||||
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
|
||||
echo "Server Host: $EXTERNAL_IP"
|
||||
|
||||
# Output AWG params from existing config
|
||||
for P in Jc Jmin Jmax S1 S2 S3 S4 H1 H2 H3 H4; do
|
||||
VAL=$(grep -E "^$P " /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
if [ -n "$VAL" ]; then echo "Variable: $P=$VAL"; fi
|
||||
done
|
||||
echo "Variable: dns_servers=1.1.1.1, 1.0.0.1"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Generate keys
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
# AWG obfuscation parameters
|
||||
JC=5
|
||||
JMIN=50
|
||||
JMAX=1000
|
||||
S1_VAL=50
|
||||
S2_VAL=100
|
||||
S3_VAL=20
|
||||
S4_VAL=10
|
||||
# H1-H4: keep numeric values for broad awg-tools compatibility.
|
||||
H1_VAL=123456789
|
||||
H2_VAL=223456789
|
||||
H3_VAL=323456789
|
||||
H4_VAL=423456789
|
||||
|
||||
# Write config
|
||||
cat > /opt/amnezia/awg2/wg0.conf << EOF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = $JC
|
||||
Jmin = $JMIN
|
||||
Jmax = $JMAX
|
||||
S1 = $S1_VAL
|
||||
S2 = $S2_VAL
|
||||
S3 = $S3_VAL
|
||||
S4 = $S4_VAL
|
||||
H1 = $H1_VAL
|
||||
H2 = $H2_VAL
|
||||
H3 = $H3_VAL
|
||||
H4 = $H4_VAL
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
EOF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg2/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg2/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg2/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg2/clientsTable
|
||||
|
||||
# Get external IP
|
||||
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
|
||||
|
||||
echo "AmneziaWG 2.0 installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY"
|
||||
echo "Server Host: $EXTERNAL_IP"
|
||||
echo "Variable: Jc=$JC"
|
||||
echo "Variable: Jmin=$JMIN"
|
||||
echo "Variable: Jmax=$JMAX"
|
||||
echo "Variable: S1=$S1_VAL"
|
||||
echo "Variable: S2=$S2_VAL"
|
||||
echo "Variable: S3=$S3_VAL"
|
||||
echo "Variable: S4=$S4_VAL"
|
||||
echo "Variable: H1=$H1_VAL"
|
||||
echo "Variable: H2=$H2_VAL"
|
||||
echo "Variable: H3=$H3_VAL"
|
||||
echo "Variable: H4=$H4_VAL"
|
||||
echo "Variable: dns_servers=1.1.1.1, 1.0.0.1"',
|
||||
'#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-awg2}"
|
||||
|
||||
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||
docker rm -fv "$CONTAINER_NAME" 2>/dev/null || true
|
||||
docker image rm amnezia-awg2 2>/dev/null || true
|
||||
rm -rf /opt/amnezia/awg2 2>/dev/null || true
|
||||
|
||||
echo "{\"success\":true,\"message\":\"AmneziaWG 2.0 uninstalled\"}"',
|
||||
p.output_template,
|
||||
1,
|
||||
1,
|
||||
JSON_OBJECT(
|
||||
'engine', 'shell',
|
||||
'metadata', JSON_OBJECT(
|
||||
'container_name', 'amnezia-awg2',
|
||||
'vpn_subnet', '10.8.1.0/24',
|
||||
'port_range', JSON_ARRAY(30000, 65000),
|
||||
'config_dir', '/opt/amnezia/awg2'
|
||||
)
|
||||
),
|
||||
NOW(),
|
||||
NOW()
|
||||
FROM protocols p
|
||||
WHERE p.slug = 'amnezia-wg-advanced'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocols WHERE slug = 'awg2');
|
||||
|
||||
-- 2. Clone protocol variables from amnezia-wg-advanced to awg2
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT
|
||||
(SELECT id FROM protocols WHERE slug = 'awg2' LIMIT 1),
|
||||
src.variable_name,
|
||||
src.variable_type,
|
||||
src.default_value,
|
||||
src.description,
|
||||
src.required
|
||||
FROM protocol_variables src
|
||||
WHERE src.protocol_id = (SELECT id FROM protocols WHERE slug = 'amnezia-wg-advanced' LIMIT 1)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM protocol_variables ev
|
||||
WHERE ev.protocol_id = (SELECT id FROM protocols WHERE slug = 'awg2' LIMIT 1)
|
||||
AND ev.variable_name = src.variable_name
|
||||
);
|
||||
|
||||
-- 3. Clone protocol templates from amnezia-wg-advanced to awg2
|
||||
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
|
||||
SELECT
|
||||
(SELECT id FROM protocols WHERE slug = 'awg2' LIMIT 1),
|
||||
src.template_name,
|
||||
src.template_content,
|
||||
src.is_default
|
||||
FROM protocol_templates src
|
||||
WHERE src.protocol_id = (SELECT id FROM protocols WHERE slug = 'amnezia-wg-advanced' LIMIT 1)
|
||||
AND NOT EXISTS (
|
||||
SELECT 1 FROM protocol_templates et
|
||||
WHERE et.protocol_id = (SELECT id FROM protocols WHERE slug = 'awg2' LIMIT 1)
|
||||
AND et.template_name = src.template_name
|
||||
);
|
||||
|
||||
-- 4. Update install_script for existing awg2 protocol (in case migration was already run)
|
||||
UPDATE protocols SET install_script = '#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${SERVER_CONTAINER:-amnezia-awg2}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
VPN_PORT="${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}"
|
||||
MTU=${MTU:-1420}
|
||||
|
||||
if ! command -v git &> /dev/null; then
|
||||
apt-get update -qq && apt-get install -y -qq git >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
mkdir -p /opt/amnezia/awg2
|
||||
|
||||
if [ ! -d /opt/amnezia/awg2/src ]; then
|
||||
git clone --depth=1 https://github.com/amnezia-vpn/amneziawg-go.git /opt/amnezia/awg2/src
|
||||
fi
|
||||
|
||||
docker build --no-cache -t amnezia-awg2 /opt/amnezia/awg2/src
|
||||
|
||||
EXISTING=$(docker ps -aq -f "name=$CONTAINER_NAME" 2>/dev/null | head -1)
|
||||
if [ -z "$EXISTING" ]; then
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --cap-add=NET_ADMIN --device /dev/net/tun -p "${VPN_PORT}:${VPN_PORT}/udp" -v /opt/amnezia/awg2:/opt/amnezia/awg amnezia-awg2 sh -c "while [ ! -f /opt/amnezia/awg/wg0.conf ]; do sleep 1; done; WG_QUICK_USERSPACE_IMPLEMENTATION=amneziawg-go awg-quick up /opt/amnezia/awg/wg0.conf && sleep infinity"
|
||||
sleep 2
|
||||
else
|
||||
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "")
|
||||
if [ \"$STATUS\" != \"running\" ]; then
|
||||
docker start \"$CONTAINER_NAME\" >/dev/null 2>&1 || true
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f /opt/amnezia/awg2/wg0.conf ]; then
|
||||
PORT=$(grep -E "^ListenPort" /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
PSK=$(cat /opt/amnezia/awg2/wireguard_psk.key 2>/dev/null || true)
|
||||
if [ -z "$PSK" ]; then
|
||||
PSK=$(grep -E "^PresharedKey" /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
fi
|
||||
PUBKEY=$(cat /opt/amnezia/awg2/wireguard_server_public_key.key 2>/dev/null || true)
|
||||
if [ -z "$PUBKEY" ]; then
|
||||
PRIVKEY=$(cat /opt/amnezia/awg2/wireguard_server_private_key.key 2>/dev/null || true)
|
||||
if [ -n "$PRIVKEY" ]; then
|
||||
PUBKEY=$(echo "$PRIVKEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "Using existing AmneziaWG 2.0 configuration"
|
||||
echo "Port: ${PORT:-$VPN_PORT}"
|
||||
if [ -n "${PUBKEY:-}" ]; then echo "Server Public Key: $PUBKEY"; fi
|
||||
if [ -n "${PSK:-}" ]; then echo "PresharedKey = $PSK"; fi
|
||||
|
||||
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
|
||||
echo "Server Host: $EXTERNAL_IP"
|
||||
|
||||
for P in Jc Jmin Jmax S1 S2 S3 S4 H1 H2 H3 H4; do
|
||||
VAL=$(grep -E "^$P " /opt/amnezia/awg2/wg0.conf | cut -d= -f2 | tr -d "[:space:]")
|
||||
if [ -n "$VAL" ]; then echo "Variable: $P=$VAL"; fi
|
||||
done
|
||||
echo "Variable: dns_servers=1.1.1.1, 1.0.0.1"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PRIVATE_KEY=$(docker exec "$CONTAINER_NAME" wg genkey)
|
||||
PUBLIC_KEY=$(echo "$PRIVATE_KEY" | docker exec -i "$CONTAINER_NAME" wg pubkey)
|
||||
PRESHARED_KEY=$(docker exec "$CONTAINER_NAME" wg genpsk)
|
||||
|
||||
JC=5
|
||||
JMIN=50
|
||||
JMAX=1000
|
||||
S1_VAL=50
|
||||
S2_VAL=100
|
||||
S3_VAL=20
|
||||
S4_VAL=10
|
||||
H1_VAL=123456789
|
||||
H2_VAL=223456789
|
||||
H3_VAL=323456789
|
||||
H4_VAL=423456789
|
||||
|
||||
cat > /opt/amnezia/awg2/wg0.conf << EOF
|
||||
[Interface]
|
||||
PrivateKey = $PRIVATE_KEY
|
||||
Address = 10.8.1.1/24
|
||||
ListenPort = $VPN_PORT
|
||||
MTU = $MTU
|
||||
Jc = $JC
|
||||
Jmin = $JMIN
|
||||
Jmax = $JMAX
|
||||
S1 = $S1_VAL
|
||||
S2 = $S2_VAL
|
||||
S3 = $S3_VAL
|
||||
S4 = $S4_VAL
|
||||
H1 = $H1_VAL
|
||||
H2 = $H2_VAL
|
||||
H3 = $H3_VAL
|
||||
H4 = $H4_VAL
|
||||
PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE
|
||||
PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE
|
||||
EOF
|
||||
|
||||
echo "$PRIVATE_KEY" > /opt/amnezia/awg2/wireguard_server_private_key.key
|
||||
echo "$PUBLIC_KEY" > /opt/amnezia/awg2/wireguard_server_public_key.key
|
||||
echo "$PRESHARED_KEY" > /opt/amnezia/awg2/wireguard_psk.key
|
||||
echo "[]" > /opt/amnezia/awg2/clientsTable
|
||||
|
||||
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
|
||||
|
||||
echo "AmneziaWG 2.0 installed successfully"
|
||||
echo "Port: $VPN_PORT"
|
||||
echo "Server Public Key: $PUBLIC_KEY"
|
||||
echo "PresharedKey = $PRESHARED_KEY"
|
||||
echo "Server Host: $EXTERNAL_IP"
|
||||
echo "Variable: Jc=$JC"
|
||||
echo "Variable: Jmin=$JMIN"
|
||||
echo "Variable: Jmax=$JMAX"
|
||||
echo "Variable: S1=$S1_VAL"
|
||||
echo "Variable: S2=$S2_VAL"
|
||||
echo "Variable: S3=$S3_VAL"
|
||||
echo "Variable: S4=$S4_VAL"
|
||||
echo "Variable: H1=$H1_VAL"
|
||||
echo "Variable: H2=$H2_VAL"
|
||||
echo "Variable: H3=$H3_VAL"
|
||||
echo "Variable: H4=$H4_VAL"
|
||||
echo "Variable: dns_servers=1.1.1.1, 1.0.0.1"'
|
||||
WHERE slug = 'awg2';
|
||||
|
||||
-- 5. Update output_template for AWG2 (add S3/S4 padding params)
|
||||
UPDATE protocols SET output_template = '[Interface]
|
||||
PrivateKey = {{private_key}}
|
||||
Address = {{client_ip}}/32
|
||||
DNS = {{dns_servers}}
|
||||
MTU = 1280
|
||||
Jc = {{Jc}}
|
||||
Jmin = {{Jmin}}
|
||||
Jmax = {{Jmax}}
|
||||
S1 = {{S1}}
|
||||
S2 = {{S2}}
|
||||
S3 = {{S3}}
|
||||
S4 = {{S4}}
|
||||
H1 = {{H1}}
|
||||
H2 = {{H2}}
|
||||
H3 = {{H3}}
|
||||
H4 = {{H4}}
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{server_public_key}}
|
||||
PresharedKey = {{preshared_key}}
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
Endpoint = {{server_host}}:{{server_port}}
|
||||
PersistentKeepalive = 25'
|
||||
WHERE slug = 'awg2';
|
||||
|
||||
-- 6. Add S3/S4 protocol variables for awg2
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'S3', 'number', '20', 'Padding of handshake cookie message', false
|
||||
FROM protocols p WHERE p.slug = 'awg2'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'S3');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'S4', 'number', '10', 'Padding of transport messages', false
|
||||
FROM protocols p WHERE p.slug = 'awg2'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'S4');
|
||||
@@ -0,0 +1,113 @@
|
||||
-- =====================================================================
|
||||
-- Migration 059: Add MTProxy (Telegram) protocol
|
||||
-- https://hub.docker.com/r/telegrammessenger/proxy/
|
||||
-- Zero-configuration Telegram MTProto proxy server
|
||||
-- =====================================================================
|
||||
|
||||
-- 1. Insert the MTProxy protocol
|
||||
INSERT INTO protocols (name, slug, description, install_script, uninstall_script, output_template, show_text_content, ubuntu_compatible, is_active, definition, created_at, updated_at)
|
||||
SELECT
|
||||
'MTProxy (Telegram)',
|
||||
'mtproxy',
|
||||
'Telegram MTProto proxy — zero-configuration proxy server for Telegram messenger.',
|
||||
'#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Use exported variables from panel (SERVER_PORT, SERVER_CONTAINER) or defaults
|
||||
CONTAINER_NAME="${SERVER_CONTAINER:-amnezia-mtproxy}"
|
||||
PORT_RANGE_START=${PORT_RANGE_START:-30000}
|
||||
PORT_RANGE_END=${PORT_RANGE_END:-65000}
|
||||
MTPROXY_PORT="${SERVER_PORT:-$((RANDOM % (PORT_RANGE_END - PORT_RANGE_START + 1) + PORT_RANGE_START))}"
|
||||
|
||||
mkdir -p /opt/amnezia/mtproxy
|
||||
|
||||
# Generate secret if not exists
|
||||
if [ -f /opt/amnezia/mtproxy/secret ]; then
|
||||
SECRET=$(cat /opt/amnezia/mtproxy/secret)
|
||||
echo "Using existing MTProxy secret"
|
||||
else
|
||||
SECRET=$(cat /dev/urandom | tr -dc a-f0-9 | head -c 32 || true)
|
||||
echo "$SECRET" > /opt/amnezia/mtproxy/secret
|
||||
fi
|
||||
|
||||
# Store port
|
||||
echo "$MTPROXY_PORT" > /opt/amnezia/mtproxy/port
|
||||
|
||||
# Remove existing container
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
# Run MTProxy container (single line for heredoc compatibility)
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always -p "${MTPROXY_PORT}:443" -v /opt/amnezia/mtproxy:/data -e SECRET="$SECRET" telegrammessenger/proxy:latest
|
||||
|
||||
sleep 3
|
||||
|
||||
# Get external IP
|
||||
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
|
||||
|
||||
echo "MTProxy installed successfully"
|
||||
echo "Port: $MTPROXY_PORT"
|
||||
echo "Secret: $SECRET"
|
||||
echo "Server Host: $EXTERNAL_IP"',
|
||||
'#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-amnezia-mtproxy}"
|
||||
|
||||
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||
docker rm -fv "$CONTAINER_NAME" 2>/dev/null || true
|
||||
docker image rm telegrammessenger/proxy:latest 2>/dev/null || true
|
||||
rm -rf /opt/amnezia/mtproxy 2>/dev/null || true
|
||||
|
||||
echo "{\"success\":true,\"message\":\"MTProxy uninstalled\"}"',
|
||||
'tg://proxy?server={{server_host}}&port={{server_port}}&secret={{secret}}',
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
JSON_OBJECT(
|
||||
'engine', 'shell',
|
||||
'metadata', JSON_OBJECT(
|
||||
'container_name', 'amnezia-mtproxy',
|
||||
'port_range', JSON_ARRAY(30000, 65000),
|
||||
'config_dir', '/opt/amnezia/mtproxy'
|
||||
)
|
||||
),
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug = 'mtproxy');
|
||||
|
||||
-- 2. Add protocol variables for MTProxy
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'secret', 'string', '', 'MTProxy secret (32 hex chars)', true
|
||||
FROM protocols p WHERE p.slug = 'mtproxy'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'secret');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true
|
||||
FROM protocols p WHERE p.slug = 'mtproxy'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'server_host');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_port', 'number', '443', 'MTProxy external port', true
|
||||
FROM protocols p WHERE p.slug = 'mtproxy'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'server_port');
|
||||
|
||||
-- 3. Add default template for MTProxy
|
||||
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
|
||||
SELECT p.id, 'Default MTProxy', 'tg://proxy?server={{server_host}}&port={{server_port}}&secret={{secret}}', true
|
||||
FROM protocols p WHERE p.slug = 'mtproxy'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id = p.id AND template_name = 'Default MTProxy');
|
||||
|
||||
-- 4. Add QR code template (same as output)
|
||||
UPDATE protocols SET
|
||||
qr_code_template = 'tg://proxy?server={{server_host}}&port={{server_port}}&secret={{secret}}',
|
||||
qr_code_format = 'raw'
|
||||
WHERE slug = 'mtproxy';
|
||||
|
||||
-- 5. Add translations for MTProxy
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en', 'protocols', 'protocol_mtproxy', 'MTProxy (Telegram)')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('ru', 'protocols', 'protocol_mtproxy', 'MTProxy (Telegram)')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
@@ -0,0 +1,156 @@
|
||||
-- =====================================================================
|
||||
-- Migration 060: Add AIVPN protocol (AI-powered VPN with traffic disguise)
|
||||
-- https://github.com/infosave2007/aivpn
|
||||
-- Neural Resonance AI for DPI bypass, Zero-RTT, PFS
|
||||
-- =====================================================================
|
||||
|
||||
-- 1. Insert the AIVPN protocol
|
||||
INSERT INTO protocols (name, slug, description, install_script, uninstall_script, output_template, show_text_content, ubuntu_compatible, is_active, definition, created_at, updated_at)
|
||||
SELECT
|
||||
'AIVPN',
|
||||
'aivpn',
|
||||
'AIVPN — AI-powered VPN с маскировкой трафика под реальные приложения (Zoom, TikTok, DNS). Neural Resonance для обхода DPI.',
|
||||
'#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# Use exported variables from panel (SERVER_PORT, SERVER_CONTAINER) or defaults
|
||||
CONTAINER_NAME="${SERVER_CONTAINER:-aivpn-server}"
|
||||
VPN_PORT="${SERVER_PORT:-443}"
|
||||
CONFIG_DIR="/etc/aivpn"
|
||||
|
||||
# Install git and iptables if not available
|
||||
if ! command -v git &> /dev/null || ! command -v iptables &> /dev/null; then
|
||||
apt-get update -qq
|
||||
if ! command -v git &> /dev/null; then
|
||||
apt-get install -y -qq git >/dev/null 2>&1
|
||||
fi
|
||||
if ! command -v iptables &> /dev/null; then
|
||||
apt-get install -y -qq iptables >/dev/null 2>&1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Install Docker if not available
|
||||
if ! command -v docker &> /dev/null; then
|
||||
apt-get update -qq
|
||||
apt-get install -y -qq apt-transport-https ca-certificates curl gnupg lsb-release >/dev/null 2>&1
|
||||
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
|
||||
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
|
||||
apt-get update -qq && apt-get install -y -qq docker-ce docker-ce-cli containerd.io >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
mkdir -p "$CONFIG_DIR"
|
||||
|
||||
# Enable IP forwarding
|
||||
sysctl -w net.ipv4.ip_forward=1 2>/dev/null || true
|
||||
|
||||
# Generate server key if not exists
|
||||
if [ ! -f "$CONFIG_DIR/server.key" ]; then
|
||||
openssl rand 32 > "$CONFIG_DIR/server.key"
|
||||
chmod 600 "$CONFIG_DIR/server.key"
|
||||
echo "Generated new AIVPN server key"
|
||||
else
|
||||
echo "Using existing AIVPN server key"
|
||||
fi
|
||||
|
||||
# Setup NAT
|
||||
iptables -t nat -C POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE 2>/dev/null || \
|
||||
iptables -t nat -A POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE
|
||||
|
||||
# Get external IP
|
||||
EXTERNAL_IP=$(curl -s -4 ifconfig.me 2>/dev/null || curl -s -4 icanhazip.com 2>/dev/null || echo "YOUR_SERVER_IP")
|
||||
|
||||
# Clone AIVPN source for Docker build
|
||||
if [ ! -d /opt/amnezia/aivpn ]; then
|
||||
git clone --depth=1 https://github.com/infosave2007/aivpn.git /opt/amnezia/aivpn
|
||||
fi
|
||||
|
||||
# Build Docker image
|
||||
cd /opt/amnezia/aivpn
|
||||
docker build --no-cache -t aivpn-server -f Dockerfile .
|
||||
|
||||
# Remove existing container
|
||||
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
|
||||
# Run AIVPN container
|
||||
docker run -d --name "$CONTAINER_NAME" --restart always --cap-add=NET_ADMIN --device /dev/net/tun --network host -v "$CONFIG_DIR:/etc/aivpn" aivpn-server --listen "0.0.0.0:${VPN_PORT}" --key-file /etc/aivpn/server.key
|
||||
|
||||
sleep 3
|
||||
|
||||
# Check container status
|
||||
STATUS=$(docker inspect --format="{{.State.Status}}" "$CONTAINER_NAME" 2>/dev/null || echo "unknown")
|
||||
if [ "$STATUS" != "running" ]; then
|
||||
echo "ERROR: AIVPN container is not running"
|
||||
docker logs "$CONTAINER_NAME" 2>&1
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "AIVPN installed successfully"
|
||||
# Output variables for the web panel parser
|
||||
KEY_B64=$(base64 -w 0 "$CONFIG_DIR/server.key" 2>/dev/null || base64 "$CONFIG_DIR/server.key")
|
||||
echo "Variable: connection_key=$KEY_B64"
|
||||
echo "Variable: server_host=$EXTERNAL_IP"
|
||||
echo "Variable: server_port=$VPN_PORT"
|
||||
echo "Variable: config_dir=$CONFIG_DIR"',
|
||||
'#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
CONTAINER_NAME="${CONTAINER_NAME:-aivpn-server}"
|
||||
|
||||
docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||
docker rm -fv "$CONTAINER_NAME" 2>/dev/null || true
|
||||
docker image rm aivpn-server 2>/dev/null || true
|
||||
rm -rf /opt/amnezia/aivpn 2>/dev/null || true
|
||||
|
||||
# Remove NAT rules
|
||||
iptables -t nat -D POSTROUTING -s 10.0.0.0/24 -o eth0 -j MASQUERADE 2>/dev/null || true
|
||||
|
||||
echo "{\"success\":true,\"message\":\"AIVPN uninstalled\"}"',
|
||||
'aivpn://{{connection_key}}',
|
||||
1,
|
||||
1,
|
||||
1,
|
||||
JSON_OBJECT(
|
||||
'engine', 'shell',
|
||||
'metadata', JSON_OBJECT(
|
||||
'container_name', 'aivpn-server',
|
||||
'port_range', JSON_ARRAY(443, 443),
|
||||
'config_dir', '/etc/aivpn',
|
||||
'vpn_subnet', '10.0.0.0/24',
|
||||
'requires_docker_build', true,
|
||||
'git_repo', 'https://github.com/infosave2007/aivpn.git'
|
||||
)
|
||||
),
|
||||
NOW(),
|
||||
NOW()
|
||||
WHERE NOT EXISTS (SELECT 1 FROM protocols WHERE slug = 'aivpn');
|
||||
|
||||
-- 2. Add protocol variables for AIVPN
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'connection_key', 'string', '', 'AIVPN connection key (generated by server)', true
|
||||
FROM protocols p WHERE p.slug = 'aivpn'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'connection_key');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_host', 'string', '', 'Server hostname or IP', true
|
||||
FROM protocols p WHERE p.slug = 'aivpn'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'server_host');
|
||||
|
||||
INSERT INTO protocol_variables (protocol_id, variable_name, variable_type, default_value, description, required)
|
||||
SELECT p.id, 'server_port', 'number', '443', 'AIVPN server port', true
|
||||
FROM protocols p WHERE p.slug = 'aivpn'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocol_variables WHERE protocol_id = p.id AND variable_name = 'server_port');
|
||||
|
||||
-- 3. Add default template for AIVPN
|
||||
INSERT INTO protocol_templates (protocol_id, template_name, template_content, is_default)
|
||||
SELECT p.id, 'Default AIVPN', 'aivpn://{{connection_key}}', true
|
||||
FROM protocols p WHERE p.slug = 'aivpn'
|
||||
AND NOT EXISTS (SELECT 1 FROM protocol_templates WHERE protocol_id = p.id AND template_name = 'Default AIVPN');
|
||||
|
||||
-- 4. Add translations for AIVPN
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en', 'protocols', 'protocol_aivpn', 'AIVPN (AI-Powered)')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('ru', 'protocols', 'protocol_aivpn', 'AIVPN (ИИ-протокол)')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
@@ -0,0 +1,11 @@
|
||||
-- Ensure clients.connection_instructions exists in all locales used by UI.
|
||||
-- Without this key, client view heading may be missing or fallback text can appear inconsistent.
|
||||
|
||||
INSERT INTO translations (locale, category, key_name, translation) VALUES
|
||||
('en', 'clients', 'connection_instructions', 'Connection Instructions'),
|
||||
('ru', 'clients', 'connection_instructions', 'Инструкции по подключению'),
|
||||
('es', 'clients', 'connection_instructions', 'Instrucciones de conexión'),
|
||||
('de', 'clients', 'connection_instructions', 'Verbindungsanweisungen'),
|
||||
('fr', 'clients', 'connection_instructions', 'Instructions de connexion'),
|
||||
('zh', 'clients', 'connection_instructions', '连接说明')
|
||||
ON DUPLICATE KEY UPDATE translation = VALUES(translation);
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Add persistent AIVPN raw/offset counters for monotonic traffic totals across server restarts.
|
||||
|
||||
ALTER TABLE vpn_clients
|
||||
ADD COLUMN aivpn_raw_bytes_in BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER bytes_received,
|
||||
ADD COLUMN aivpn_raw_bytes_out BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER aivpn_raw_bytes_in,
|
||||
ADD COLUMN aivpn_offset_bytes_in BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER aivpn_raw_bytes_out,
|
||||
ADD COLUMN aivpn_offset_bytes_out BIGINT UNSIGNED NOT NULL DEFAULT 0 AFTER aivpn_offset_bytes_in;
|
||||
@@ -0,0 +1,14 @@
|
||||
-- Remove invalid empty peer block from AWG2 install script.
|
||||
-- The old script generated wg0.conf with:
|
||||
-- [Peer]
|
||||
-- PublicKey =
|
||||
-- which causes awg setconf parse errors and restart loops.
|
||||
|
||||
UPDATE protocols
|
||||
SET install_script = REPLACE(
|
||||
install_script,
|
||||
'\n[Peer]\nPublicKey = \nPresharedKey = $PRESHARED_KEY\nAllowedIPs = 10.8.1.2/32\n',
|
||||
'\n'
|
||||
)
|
||||
WHERE slug = 'awg2'
|
||||
AND install_script LIKE '%[Peer]%PublicKey = %PresharedKey = $PRESHARED_KEY%AllowedIPs = 10.8.1.2/32%';
|
||||
@@ -10,3 +10,5 @@ collation-server = utf8mb4_unicode_ci
|
||||
init_connect = 'SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci'
|
||||
init_connect = 'SET collation_connection = utf8mb4_unicode_ci'
|
||||
skip-character-set-client-handshake
|
||||
binlog_expire_logs_seconds = 259200
|
||||
max_binlog_size = 100M
|
||||
|
||||
+2570
-436
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/inc/Config.php';
|
||||
Config::load(__DIR__ . '/.env');
|
||||
require_once __DIR__ . '/inc/DB.php';
|
||||
|
||||
try {
|
||||
$pdo = DB::conn();
|
||||
$sql = file_get_contents(__DIR__ . '/migrations/053_split_speed.sql');
|
||||
$pdo->exec($sql);
|
||||
echo "Migration 053 applied successfully.\n";
|
||||
} catch (Exception $e) {
|
||||
echo "Migration failed: " . $e->getMessage() . "\n";
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
|
||||
EMAIL="${EMAIL:-admin@amnez.ia}"
|
||||
PASSWORD="${PASSWORD:-admin123}"
|
||||
SERVER_HOST="${SERVER_HOST:-217.26.25.6}"
|
||||
PROTOCOL_SLUG="${PROTOCOL_SLUG:-amnezia-wg-advanced}"
|
||||
CLIENT_NAME="${CLIENT_NAME:-api-selfcheck}"
|
||||
CLIENT_LOGIN="${CLIENT_LOGIN:-api-selfcheck}"
|
||||
OUT_DIR="${OUT_DIR:-scripts/_cycle_out}"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
AUTH_RESP=$(curl -sS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD" || true)
|
||||
TOKEN=$(printf '%s' "$AUTH_RESP" | python3 -c 'import sys,json; raw=sys.stdin.read().strip();
|
||||
import sys
|
||||
if not raw: sys.exit(2)
|
||||
j=json.loads(raw)
|
||||
print(j.get("token",""))
|
||||
') || {
|
||||
echo "ERROR: failed to parse /api/auth/token response" >&2
|
||||
echo "PANEL_URL=$PANEL_URL" >&2
|
||||
echo "Response (first 500 chars):" >&2
|
||||
printf '%s' "$AUTH_RESP" | head -c 500 >&2
|
||||
echo >&2
|
||||
exit 1
|
||||
}
|
||||
if [[ -z "${TOKEN:-}" ]]; then
|
||||
echo "ERROR: token is empty" >&2
|
||||
printf '%s' "$AUTH_RESP" | head -c 500 >&2
|
||||
echo >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "TOKEN_OK"
|
||||
|
||||
SERVER_JSON=$(curl -fsS "$PANEL_URL/api/servers" -H "Authorization: Bearer $TOKEN")
|
||||
SERVER_ID=$(printf '%s' "$SERVER_JSON" | python3 -c 'import sys,json; j=json.load(sys.stdin); host=sys.argv[1];
|
||||
out="";
|
||||
for s in j.get("servers",[]):
|
||||
if str(s.get("host","" )).strip()==host:
|
||||
out=str(s.get("id",""))
|
||||
break
|
||||
print(out)
|
||||
' "$SERVER_HOST")
|
||||
|
||||
if [[ -z "${SERVER_ID:-}" ]]; then
|
||||
echo "ERROR: server with host $SERVER_HOST not found" >&2
|
||||
printf '%s' "$SERVER_JSON" | python3 -m json.tool | head -200
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "SERVER_ID=$SERVER_ID"
|
||||
|
||||
PROTO_JSON=$(curl -fsS "$PANEL_URL/api/protocols/active" -H "Authorization: Bearer $TOKEN")
|
||||
PROTOCOL_ID=$(printf '%s' "$PROTO_JSON" | python3 -c 'import sys,json; j=json.load(sys.stdin); slug=sys.argv[1];
|
||||
out="";
|
||||
for p in j.get("protocols",[]):
|
||||
if p.get("slug")==slug:
|
||||
out=str(p.get("id",""))
|
||||
break
|
||||
print(out)
|
||||
' "$PROTOCOL_SLUG")
|
||||
|
||||
if [[ -z "${PROTOCOL_ID:-}" ]]; then
|
||||
echo "ERROR: protocol $PROTOCOL_SLUG not found" >&2
|
||||
printf '%s' "$PROTO_JSON" | python3 -m json.tool | head -200
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "PROTOCOL_ID=$PROTOCOL_ID"
|
||||
|
||||
pretty_print() {
|
||||
# Reads JSON from stdin and pretty-prints it. If it's not JSON, prints raw.
|
||||
local data
|
||||
data=$(cat)
|
||||
if python3 -m json.tool >/dev/null 2>&1 <<<"$data"; then
|
||||
python3 -m json.tool <<<"$data"
|
||||
else
|
||||
printf '%s' "$data"
|
||||
fi
|
||||
}
|
||||
|
||||
echo "--- UNINSTALL $PROTOCOL_SLUG (ignore errors)"
|
||||
set +e
|
||||
UNINSTALL_RESP=$(curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/$PROTOCOL_SLUG/uninstall" \
|
||||
-H "Authorization: Bearer $TOKEN" || true)
|
||||
printf '%s' "$UNINSTALL_RESP" >"$OUT_DIR/uninstall_${PROTOCOL_SLUG}.txt"
|
||||
printf '%s' "$UNINSTALL_RESP" | pretty_print | head -200
|
||||
set -e
|
||||
|
||||
echo "--- INSTALL protocol_id=$PROTOCOL_ID"
|
||||
INSTALL_RESP=$(curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/install" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"protocol_id\":$PROTOCOL_ID}" || true)
|
||||
printf '%s' "$INSTALL_RESP" >"$OUT_DIR/install_${PROTOCOL_ID}.txt"
|
||||
printf '%s' "$INSTALL_RESP" | pretty_print | head -200
|
||||
|
||||
echo "--- CREATE client"
|
||||
CLIENT_RESP=$(curl -fsS -X POST "$PANEL_URL/api/clients/create" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"server_id\":$SERVER_ID,\"protocol_id\":$PROTOCOL_ID,\"name\":\"$CLIENT_NAME\",\"login\":\"$CLIENT_LOGIN\"}")
|
||||
|
||||
printf '%s' "$CLIENT_RESP" >"$OUT_DIR/client_create_${PROTOCOL_ID}.txt"
|
||||
|
||||
printf '%s' "$CLIENT_RESP" | pretty_print | head -200
|
||||
CLIENT_ID=$(printf '%s' "$CLIENT_RESP" | python3 -c 'import sys,json; j=json.load(sys.stdin); print(j.get("client",{}).get("id",""))')
|
||||
|
||||
if [[ -z "${CLIENT_ID:-}" ]]; then
|
||||
echo "ERROR: client_id missing" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "CLIENT_ID=$CLIENT_ID"
|
||||
|
||||
echo "--- SELFTEST"
|
||||
SELFTEST_RESP=$(curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/selftest" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"client_id\":$CLIENT_ID,\"create_client\":false,\"install\":false,\"protocol_id\":$PROTOCOL_ID}")
|
||||
|
||||
printf '%s' "$SELFTEST_RESP" >"$OUT_DIR/selftest_${CLIENT_ID}.txt"
|
||||
|
||||
printf '%s' "$SELFTEST_RESP" | pretty_print | head -260
|
||||
|
||||
NEED_DIAG=$(printf '%s' "$SELFTEST_RESP" | python3 -c 'import sys,json; j=json.load(sys.stdin); peer=(j.get("wg") or {}).get("peer") or {}; hs=int(peer.get("latest_handshake") or 0); ep=str(peer.get("endpoint") or ""); print("1" if (ep=="(none)" or hs==0) else "0")')
|
||||
|
||||
if [[ "$NEED_DIAG" == "1" ]]; then
|
||||
echo "--- DIAGNOSE HANDSHAKE"
|
||||
DIAG_RESP=$(curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/diagnose-handshake" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"client_id\":$CLIENT_ID,\"duration_seconds\":5}" || true)
|
||||
printf '%s' "$DIAG_RESP" >"$OUT_DIR/diagnose_${CLIENT_ID}.txt"
|
||||
printf '%s' "$DIAG_RESP" | pretty_print | head -260
|
||||
fi
|
||||
|
||||
echo "DONE (responses saved in $OUT_DIR)"
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
|
||||
EMAIL="${EMAIL:-admin@amnez.ia}"
|
||||
PASSWORD="${PASSWORD:-admin123}"
|
||||
SERVER_ID="${SERVER_ID:-5}"
|
||||
CLIENT_ID="${CLIENT_ID:-}"
|
||||
DURATION="${DURATION:-10}"
|
||||
OUT_FILE="${OUT_FILE:-}"
|
||||
|
||||
if [[ -z "${CLIENT_ID}" ]]; then
|
||||
echo "ERROR: CLIENT_ID is required" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
if [[ -z "${OUT_FILE}" ]]; then
|
||||
OUT_FILE="scripts/_cycle_out/diagnose_client_${CLIENT_ID}.json"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$OUT_FILE")"
|
||||
|
||||
TOKEN_JSON=$(curl -sS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD")
|
||||
TOKEN=$(printf '%s' "$TOKEN_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("token",""))')
|
||||
|
||||
if [[ -z "${TOKEN}" ]]; then
|
||||
echo "ERROR: token empty" >&2
|
||||
printf '%s' "$TOKEN_JSON" | head -c 300 >&2
|
||||
echo >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
RESP_WITH_STATUS=$(curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/diagnose-handshake" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"client_id\":$CLIENT_ID,\"duration_seconds\":$DURATION}" \
|
||||
-w "\n__HTTP_STATUS__%{http_code}")
|
||||
|
||||
HTTP_STATUS=$(printf '%s' "$RESP_WITH_STATUS" | awk -F'__HTTP_STATUS__' 'END{print $2}')
|
||||
RESP=$(printf '%s' "$RESP_WITH_STATUS" | awk -F'__HTTP_STATUS__' '{print $1}')
|
||||
|
||||
if [[ -z "${RESP:-}" ]]; then
|
||||
echo "ERROR: empty response (http_status=${HTTP_STATUS:-unknown})" >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
TMP_FILE="${OUT_FILE}.tmp"
|
||||
printf '%s' "$RESP" >"$TMP_FILE"
|
||||
mv "$TMP_FILE" "$OUT_FILE"
|
||||
|
||||
echo "saved:$OUT_FILE (http_status=${HTTP_STATUS:-unknown})"
|
||||
|
||||
if [[ "${HTTP_STATUS:-}" =~ ^[0-9]+$ ]] && [[ "${HTTP_STATUS}" -ge 400 ]]; then
|
||||
exit 5
|
||||
fi
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
|
||||
EMAIL="${EMAIL:-admin@amnez.ia}"
|
||||
PASSWORD="${PASSWORD:-admin123}"
|
||||
SERVER_ID="${SERVER_ID:-5}"
|
||||
OUT_FILE="${OUT_FILE:-scripts/_cycle_out/diagnose_no_client.json}"
|
||||
DURATION="${DURATION:-2}"
|
||||
|
||||
mkdir -p "$(dirname "$OUT_FILE")"
|
||||
|
||||
TOKEN_JSON=$(curl -sS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD")
|
||||
TOKEN=$(printf '%s' "$TOKEN_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])')
|
||||
|
||||
TMP_FILE="${OUT_FILE}.tmp"
|
||||
RESP=$(curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/diagnose-handshake" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"duration_seconds\":$DURATION}" || true)
|
||||
|
||||
if [[ -z "${RESP:-}" ]]; then
|
||||
echo "ERROR: empty response from diagnose-handshake" >&2
|
||||
echo "PANEL_URL=$PANEL_URL SERVER_ID=$SERVER_ID" >&2
|
||||
echo "Token JSON (first 200):" >&2
|
||||
printf '%s' "$TOKEN_JSON" | head -c 200 >&2
|
||||
echo >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
printf '%s' "$RESP" > "$TMP_FILE"
|
||||
mv "$TMP_FILE" "$OUT_FILE"
|
||||
|
||||
echo "saved:$OUT_FILE"
|
||||
@@ -0,0 +1,22 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
|
||||
EMAIL="${EMAIL:-admin@amnez.ia}"
|
||||
PASSWORD="${PASSWORD:-admin123}"
|
||||
|
||||
TOKEN_JSON=$(curl -sS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD")
|
||||
TOKEN=$(printf '%s' "$TOKEN_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("token",""))')
|
||||
|
||||
if [[ -z "${TOKEN:-}" ]]; then
|
||||
echo "ERROR: token empty" >&2
|
||||
printf '%s' "$TOKEN_JSON" | head -c 200 >&2
|
||||
echo >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
curl -fsS "$PANEL_URL/api/clients" -H "Authorization: Bearer $TOKEN" | \
|
||||
python3 -c 'import sys,json; j=json.load(sys.stdin); cs=j.get("clients",[]);
|
||||
print("id\tname\tprotocol\tserver_id")
|
||||
for c in cs:
|
||||
print(f"{c.get("id")}\t{c.get("name")}\t{c.get("protocol")}\t{c.get("server_id")}")'
|
||||
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
|
||||
EMAIL="${EMAIL:-}"
|
||||
PASSWORD="${PASSWORD:-}"
|
||||
TOKEN="${TOKEN:-}"
|
||||
SERVER_ID="${SERVER_ID:-1}"
|
||||
PROTOCOL_ID="${PROTOCOL_ID:-}"
|
||||
UNINSTALL_SLUG="${UNINSTALL_SLUG:-}"
|
||||
CLIENT_NAME="${CLIENT_NAME:-smoke-client}"
|
||||
CLIENT_LOGIN="${CLIENT_LOGIN:-smoke-client}"
|
||||
SELFTEST="${SELFTEST:-1}"
|
||||
DIAGNOSE="${DIAGNOSE:-1}"
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
if [[ -z "$EMAIL" || -z "$PASSWORD" ]]; then
|
||||
echo "ERROR: set TOKEN or (EMAIL and PASSWORD)" >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[1/6] Getting JWT token..." >&2
|
||||
TOKEN="$(curl -fsS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD" | php -r '$j=json_decode(stream_get_contents(STDIN),true); echo $j["token"] ?? "";')"
|
||||
fi
|
||||
|
||||
if [[ -z "$TOKEN" ]]; then
|
||||
echo "ERROR: failed to obtain token" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
auth=(-H "Authorization: Bearer $TOKEN")
|
||||
|
||||
echo "[2/6] Listing active protocols..." >&2
|
||||
curl -fsS "$PANEL_URL/api/protocols/active" "${auth[@]}" | cat
|
||||
|
||||
if [[ -n "$UNINSTALL_SLUG" ]]; then
|
||||
echo "[3/6] Uninstalling protocol slug=$UNINSTALL_SLUG on server=$SERVER_ID ..." >&2
|
||||
curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/$UNINSTALL_SLUG/uninstall" "${auth[@]}" | cat
|
||||
else
|
||||
echo "[3/6] Skipping uninstall (set UNINSTALL_SLUG to run)." >&2
|
||||
fi
|
||||
|
||||
if [[ -n "$PROTOCOL_ID" ]]; then
|
||||
echo "[4/6] Installing protocol_id=$PROTOCOL_ID on server=$SERVER_ID ..." >&2
|
||||
curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/install" \
|
||||
"${auth[@]}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"protocol_id\": $PROTOCOL_ID}" | cat
|
||||
else
|
||||
echo "[4/6] Skipping install (set PROTOCOL_ID to run)." >&2
|
||||
fi
|
||||
|
||||
echo "[5/6] Creating client on server=$SERVER_ID (protocol_id=${PROTOCOL_ID:-auto})..." >&2
|
||||
CREATE_PAYLOAD=$(php -r '$d=["server_id"=>(int)getenv("SERVER_ID"),"name"=>getenv("CLIENT_NAME"),"login"=>getenv("CLIENT_LOGIN")]; $pid=getenv("PROTOCOL_ID"); if($pid!==false && $pid!==""){$d["protocol_id"]= (int)$pid;} echo json_encode($d, JSON_UNESCAPED_SLASHES);')
|
||||
RESP="$(curl -fsS -X POST "$PANEL_URL/api/clients/create" "${auth[@]}" -H "Content-Type: application/json" -d "$CREATE_PAYLOAD")"
|
||||
echo "$RESP" | cat
|
||||
|
||||
CLIENT_ID=$(echo "$RESP" | php -r '$j=json_decode(stream_get_contents(STDIN),true); echo $j["client"]["id"] ?? "";')
|
||||
|
||||
if [[ -n "$CLIENT_ID" ]]; then
|
||||
echo "[6/6] Fetching client details (includes stats sync)..." >&2
|
||||
curl -fsS "$PANEL_URL/api/clients/$CLIENT_ID/details" "${auth[@]}" | cat
|
||||
|
||||
if [[ "$SELFTEST" == "1" ]]; then
|
||||
echo >&2
|
||||
echo "[selftest] Verifying generated config vs server wg0..." >&2
|
||||
SELFTEST_PAYLOAD=$(php -r '$d=["protocol_id"=>getenv("PROTOCOL_ID")!==false && getenv("PROTOCOL_ID")!=="" ? (int)getenv("PROTOCOL_ID") : 0, "install"=>false, "create_client"=>false, "client_id"=>(int)getenv("CLIENT_ID")]; echo json_encode($d, JSON_UNESCAPED_SLASHES);')
|
||||
SELFTEST_RESP=$(curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/selftest" \
|
||||
"${auth[@]}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$SELFTEST_PAYLOAD")
|
||||
echo "$SELFTEST_RESP" | cat
|
||||
|
||||
if [[ "$DIAGNOSE" == "1" ]]; then
|
||||
# If peer endpoint is none OR latest_handshake=0, run server-side diagnostics
|
||||
NEED_DIAG=$(echo "$SELFTEST_RESP" | php -r '$j=json_decode(stream_get_contents(STDIN),true); $hs=$j["wg"]["peer"]["latest_handshake"] ?? null; $ep=$j["wg"]["peer"]["endpoint"] ?? null; echo ((string)$ep==="(none)" || (int)$hs===0) ? "1" : "0";')
|
||||
if [[ "$NEED_DIAG" == "1" ]]; then
|
||||
echo >&2
|
||||
echo "[diagnose] Collecting server-side evidence (wg/ports/firewall/tcpdump)..." >&2
|
||||
DIAG_PAYLOAD=$(php -r '$d=["client_id"=>(int)getenv("CLIENT_ID"),"duration_seconds"=>5]; echo json_encode($d, JSON_UNESCAPED_SLASHES);')
|
||||
curl -fsS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/diagnose-handshake" \
|
||||
"${auth[@]}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$DIAG_PAYLOAD" | cat
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "[6/6] No client id returned; skipping details." >&2
|
||||
fi
|
||||
|
||||
echo >&2
|
||||
echo "Done." >&2
|
||||
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PANEL_URL="${PANEL_URL:-http://localhost:8082}"
|
||||
EMAIL="${EMAIL:-admin@amnez.ia}"
|
||||
PASSWORD="${PASSWORD:-admin123}"
|
||||
CLIENT_NAME="${CLIENT_NAME:-}"
|
||||
CLIENT_ID="${CLIENT_ID:-}"
|
||||
OUT_DIR="${OUT_DIR:-scripts/_cycle_out}"
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
|
||||
if [[ -z "$CLIENT_ID" && -z "$CLIENT_NAME" ]]; then
|
||||
echo "ERROR: set CLIENT_ID or CLIENT_NAME" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
TOKEN_JSON=$(curl -sS -X POST "$PANEL_URL/api/auth/token" -d "email=$EMAIL&password=$PASSWORD")
|
||||
TOKEN=$(printf '%s' "$TOKEN_JSON" | python3 -c 'import sys,json; print(json.load(sys.stdin).get("token",""))')
|
||||
|
||||
if [[ -z "${TOKEN:-}" ]]; then
|
||||
echo "ERROR: token empty" >&2
|
||||
printf '%s' "$TOKEN_JSON" | head -c 200 >&2
|
||||
echo >&2
|
||||
exit 3
|
||||
fi
|
||||
|
||||
if [[ -z "$CLIENT_ID" ]]; then
|
||||
CLIENTS_JSON=$(curl -fsS "$PANEL_URL/api/clients" -H "Authorization: Bearer $TOKEN")
|
||||
CLIENT_ID=$(printf '%s' "$CLIENTS_JSON" | python3 -c 'import sys,json; j=json.load(sys.stdin); needle=sys.argv[1];
|
||||
for c in j.get("clients",[]):
|
||||
if str(c.get("name",""))==needle:
|
||||
print(c.get("id",""));
|
||||
raise SystemExit
|
||||
print("")' "$CLIENT_NAME")
|
||||
fi
|
||||
|
||||
if [[ -z "${CLIENT_ID:-}" ]]; then
|
||||
echo "ERROR: client not found" >&2
|
||||
exit 4
|
||||
fi
|
||||
|
||||
RESP=$(curl -fsS -X POST "$PANEL_URL/api/clients/$CLIENT_ID/regenerate-config" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{}' )
|
||||
|
||||
JSON_OUT="$OUT_DIR/regenerate_${CLIENT_ID}.json"
|
||||
CONF_OUT="$OUT_DIR/${CLIENT_NAME:-client_${CLIENT_ID}}_regenerated.conf"
|
||||
|
||||
printf '%s' "$RESP" >"$JSON_OUT"
|
||||
|
||||
# Extract config field
|
||||
printf '%s' "$RESP" | python3 -c 'import sys,json; j=json.load(sys.stdin); c=(j.get("client") or {}).get("config") or ""; sys.stdout.write(c)' >"$CONF_OUT"
|
||||
|
||||
echo "saved_json:$JSON_OUT"
|
||||
echo "saved_conf:$CONF_OUT"
|
||||
Executable
+69
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
# Universal cleanup script for all Amnezia containers
|
||||
# Based on remove_all_containers.sh from amnezia-client
|
||||
# Usage: ./cleanup_amnezia.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
echo "========================================="
|
||||
echo "Amnezia VPN - Complete Cleanup Script"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "WARNING: This will remove ALL Amnezia containers, images, and data!"
|
||||
echo "Press Ctrl+C to cancel, or Enter to continue..."
|
||||
read -r
|
||||
|
||||
echo ""
|
||||
echo "Step 1: Stopping all Amnezia containers..."
|
||||
CONTAINERS=$(docker ps -a | grep amnezia | awk '{print $1}' || true)
|
||||
if [ -n "$CONTAINERS" ]; then
|
||||
echo "$CONTAINERS" | xargs docker stop || true
|
||||
echo "✓ Containers stopped"
|
||||
else
|
||||
echo "✓ No running containers found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Step 2: Removing all Amnezia containers..."
|
||||
CONTAINERS=$(docker ps -a | grep amnezia | awk '{print $1}' || true)
|
||||
if [ -n "$CONTAINERS" ]; then
|
||||
echo "$CONTAINERS" | xargs docker rm -fv || true
|
||||
echo "✓ Containers removed"
|
||||
else
|
||||
echo "✓ No containers to remove"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Step 3: Removing all Amnezia images..."
|
||||
IMAGES=$(docker images -a | grep amnezia | awk '{print $3}' || true)
|
||||
if [ -n "$IMAGES" ]; then
|
||||
echo "$IMAGES" | xargs docker rmi -f || true
|
||||
echo "✓ Images removed"
|
||||
else
|
||||
echo "✓ No images to remove"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Step 4: Removing Amnezia DNS network..."
|
||||
docker network rm amnezia-dns-net 2>/dev/null && echo "✓ Network removed" || echo "✓ Network not found"
|
||||
|
||||
echo ""
|
||||
echo "Step 5: Removing Amnezia data directory..."
|
||||
if [ -d "/opt/amnezia" ]; then
|
||||
rm -rf /opt/amnezia
|
||||
echo "✓ Data directory removed"
|
||||
else
|
||||
echo "✓ Data directory not found"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "========================================="
|
||||
echo "Cleanup completed successfully!"
|
||||
echo "========================================="
|
||||
echo ""
|
||||
echo "Summary:"
|
||||
echo "- All Amnezia containers stopped and removed"
|
||||
echo "- All Amnezia Docker images removed"
|
||||
echo "- Amnezia DNS network removed"
|
||||
echo "- All configuration data removed from /opt/amnezia"
|
||||
echo ""
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
require_once __DIR__ . '/../inc/Config.php';
|
||||
require_once __DIR__ . '/../inc/DB.php';
|
||||
require_once __DIR__ . '/../inc/VpnClient.php';
|
||||
require_once __DIR__ . '/../inc/VpnServer.php';
|
||||
|
||||
$pdo = DB::conn();
|
||||
$clientId = 4;
|
||||
|
||||
echo "Loading client $clientId...\n";
|
||||
$client = new VpnClient($clientId);
|
||||
$data = $client->getData();
|
||||
|
||||
if (!$data) {
|
||||
die("Client not found\n");
|
||||
}
|
||||
|
||||
echo "Client Name: " . $data['name'] . "\n";
|
||||
echo "Config: " . substr($data['config'], 0, 50) . "...\n";
|
||||
|
||||
echo "Running syncStats()...\n";
|
||||
try {
|
||||
$res = $client->syncStats();
|
||||
echo "Sync Result: " . ($res ? 'TRUE' : 'FALSE') . "\n";
|
||||
|
||||
// Check DB
|
||||
$fresh = new VpnClient($clientId);
|
||||
$d = $fresh->getData();
|
||||
echo "Bytes Sent: " . $d['bytes_sent'] . "\n";
|
||||
echo "Bytes Recv: " . $d['bytes_received'] . "\n";
|
||||
echo "Last Handshake: " . $d['last_handshake'] . "\n";
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
$priv = getenv('WG_PRIV_B64') ?: '';
|
||||
$priv = trim($priv);
|
||||
$raw = base64_decode($priv, true);
|
||||
if ($raw === false) {
|
||||
fwrite(STDERR, "invalid_base64\n");
|
||||
exit(2);
|
||||
}
|
||||
echo "raw_len=" . strlen($raw) . "\n";
|
||||
if (strlen($raw) !== 32) {
|
||||
fwrite(STDERR, "invalid_length\n");
|
||||
exit(3);
|
||||
}
|
||||
if (!function_exists('sodium_crypto_scalarmult_base')) {
|
||||
fwrite(STDERR, "libsodium_missing\n");
|
||||
exit(4);
|
||||
}
|
||||
$pub = sodium_crypto_scalarmult_base($raw);
|
||||
echo "pub=" . base64_encode($pub) . "\n";
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
PANEL_URL="http://localhost:8082"
|
||||
EMAIL="admin@amnez.ia"
|
||||
PASSWORD="admin123"
|
||||
SERVER_ID="1"
|
||||
REMOTE_HOST="217.26.25.6"
|
||||
REMOTE_USER="root"
|
||||
REMOTE_PASS='1Fr045jZbtF!'
|
||||
|
||||
# protocol IDs in this workspace
|
||||
AWG2_ID="11"
|
||||
AIVPN_ID="13"
|
||||
MTPROXY_ID="12"
|
||||
|
||||
echo "== auth =="
|
||||
TOKEN=$(curl -sS -X POST "$PANEL_URL/api/auth/token" \
|
||||
-d "email=$EMAIL&password=$PASSWORD" | python3 -c 'import sys,json; print(json.load(sys.stdin)["token"])')
|
||||
|
||||
echo "== remote full docker cleanup =="
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" 'bash -s' <<'EOSSH'
|
||||
set -euo pipefail
|
||||
|
||||
# Stop and remove all containers if any
|
||||
if [ -n "$(docker ps -aq 2>/dev/null || true)" ]; then
|
||||
docker rm -f $(docker ps -aq) >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Full cleanup of images/volumes/networks/build cache
|
||||
if command -v docker >/dev/null 2>&1; then
|
||||
docker system prune -af --volumes >/dev/null 2>&1 || true
|
||||
docker builder prune -af >/dev/null 2>&1 || true
|
||||
fi
|
||||
|
||||
# Remove protocol dirs to force fresh bootstrap
|
||||
rm -rf /opt/amnezia /etc/aivpn /etc/amnezia /etc/mtproxy 2>/dev/null || true
|
||||
mkdir -p /opt/amnezia /etc/aivpn /etc/amnezia /etc/mtproxy
|
||||
|
||||
echo "remote cleanup done"
|
||||
EOSSH
|
||||
|
||||
echo "== install awg2 =="
|
||||
curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/install" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"protocol_id\":$AWG2_ID}" | tee /tmp/install_awg2_after_remote_reset.json
|
||||
|
||||
echo
|
||||
echo "== install aivpn =="
|
||||
curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/install" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"protocol_id\":$AIVPN_ID}" | tee /tmp/install_aivpn_after_remote_reset.json
|
||||
|
||||
echo
|
||||
echo "== install mtproxy =="
|
||||
curl -sS -X POST "$PANEL_URL/api/servers/$SERVER_ID/protocols/install" \
|
||||
-H "Authorization: Bearer $TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
--data "{\"protocol_id\":$MTPROXY_ID}" | tee /tmp/install_mtproxy_after_remote_reset.json
|
||||
|
||||
echo
|
||||
echo "== verify containers on remote =="
|
||||
sshpass -p "$REMOTE_PASS" ssh -o StrictHostKeyChecking=no "$REMOTE_USER@$REMOTE_HOST" \
|
||||
"docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'"
|
||||
|
||||
echo
|
||||
echo "done"
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/bin/bash
|
||||
# Remove single Amnezia container
|
||||
# Based on remove_container.sh from amnezia-client
|
||||
# Usage: ./remove_container.sh <container_name>
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
if [ $# -eq 0 ]; then
|
||||
echo "Usage: $0 <container_name>"
|
||||
echo "Example: $0 amnezia-awg"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CONTAINER_NAME="$1"
|
||||
|
||||
echo "Removing Amnezia container: $CONTAINER_NAME"
|
||||
echo ""
|
||||
|
||||
# Stop the container
|
||||
echo "Stopping container..."
|
||||
docker stop "$CONTAINER_NAME" 2>/dev/null && echo "✓ Container stopped" || echo "✓ Container not running"
|
||||
|
||||
# Remove the container with volumes
|
||||
echo "Removing container..."
|
||||
docker rm -fv "$CONTAINER_NAME" 2>/dev/null && echo "✓ Container removed" || echo "✓ Container not found"
|
||||
|
||||
# Remove the image
|
||||
echo "Removing image..."
|
||||
docker rmi "$CONTAINER_NAME" 2>/dev/null && echo "✓ Image removed" || echo "✓ Image not found"
|
||||
|
||||
echo ""
|
||||
echo "Container $CONTAINER_NAME has been removed successfully!"
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
|
||||
if (php_sapi_name() !== 'cli') {
|
||||
die("CLI only");
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/../inc/Config.php';
|
||||
require_once __DIR__ . '/../inc/DB.php';
|
||||
require_once __DIR__ . '/../inc/VpnServer.php';
|
||||
|
||||
echo "Starting AmneziaWG Sync (DB -> Server)...\n";
|
||||
|
||||
try {
|
||||
// Assuming Server ID 1 for now (or pass as arg)
|
||||
$serverId = 1;
|
||||
$server = new VpnServer($serverId);
|
||||
$data = $server->getData();
|
||||
|
||||
if (!$data) {
|
||||
die("Server not found\n");
|
||||
}
|
||||
|
||||
$containerName = $data['container_name'] ?? 'amnezia-awg';
|
||||
|
||||
// 1. Get Server Params
|
||||
$awgParams = json_decode($data['awg_params'] ?? '[]', true);
|
||||
if (empty($awgParams)) {
|
||||
// Safe Fallback if DB empty? Or error?
|
||||
// Better error out to avoid breakage, but user wants FIX.
|
||||
// If empty, generate new randoms?
|
||||
// Let's assume params exist or fetch from current wg0 check.
|
||||
// For now, fail if missing.
|
||||
echo "Warning: AWG Params missing in DB. Fetching defaults/randoms...\n";
|
||||
$awgParams = [
|
||||
'Jc' => 5,
|
||||
'Jmin' => 50,
|
||||
'Jmax' => 1000,
|
||||
'S1' => 100,
|
||||
'S2' => 200,
|
||||
'H1' => 18274619,
|
||||
'H2' => 2938471,
|
||||
'H3' => 918273,
|
||||
'H4' => 1928374
|
||||
];
|
||||
}
|
||||
|
||||
// 2. Get Keys (Interface)
|
||||
// Server Private Key should be in DB?
|
||||
// vpn_servers table has server_public_key... but usually NOT private key?
|
||||
// Start script puts keys in /opt/amnezia/awg/....key
|
||||
// We should READ them from file to be safe.
|
||||
// Read directly from HOST file to avoid container dependency (deadlock if stuck in restart loop)
|
||||
$privKey = trim($server->executeCommand("cat /opt/amnezia/awg/wireguard_server_private_key.key 2>/dev/null", true));
|
||||
|
||||
if (empty($privKey)) {
|
||||
// Fallback: try container exec (only if host file missing)
|
||||
$privKey = trim($server->executeCommand("docker exec -i $containerName cat /opt/amnezia/awg/server_private.key", true));
|
||||
}
|
||||
|
||||
if (!$privKey || strpos($privKey, 'Error response') !== false) {
|
||||
// If still missing or error message
|
||||
die("Fatal: Could not retrieve Server Private Key. Check /opt/amnezia/awg/ directory.\n");
|
||||
}
|
||||
|
||||
$vpnPort = $data['vpn_port'] ?? 51820;
|
||||
|
||||
// 3. Build Interface Block
|
||||
$conf = "[Interface]\n";
|
||||
$conf .= "PrivateKey = $privKey\n";
|
||||
$conf .= "Address = 10.8.1.1/24\n"; // Hardcoded or from DB? vpn_subnet usually.
|
||||
$conf .= "ListenPort = $vpnPort\n";
|
||||
|
||||
// Normalize params
|
||||
$cleanParams = [];
|
||||
foreach ($awgParams as $k => $v)
|
||||
$cleanParams[strtoupper($k)] = $v;
|
||||
|
||||
$conf .= "Jc = " . ($cleanParams['JC'] ?? 5) . "\n";
|
||||
$conf .= "Jmin = " . ($cleanParams['JMIN'] ?? 50) . "\n";
|
||||
$conf .= "Jmax = " . ($cleanParams['JMAX'] ?? 1000) . "\n";
|
||||
$conf .= "S1 = " . ($cleanParams['S1'] ?? 50) . "\n";
|
||||
$conf .= "S2 = " . ($cleanParams['S2'] ?? 100) . "\n";
|
||||
$conf .= "H1 = " . ($cleanParams['H1'] ?? 1) . "\n";
|
||||
$conf .= "H2 = " . ($cleanParams['H2'] ?? 2) . "\n";
|
||||
$conf .= "H3 = " . ($cleanParams['H3'] ?? 3) . "\n";
|
||||
$conf .= "H4 = " . ($cleanParams['H4'] ?? 4) . "\n";
|
||||
|
||||
$conf .= "PostUp = iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o eth0 -j MASQUERADE\n";
|
||||
$conf .= "PostDown = iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o eth0 -j MASQUERADE\n\n";
|
||||
|
||||
// 4. Load Clients
|
||||
$pdo = DB::conn();
|
||||
$stmt = $pdo->prepare("SELECT * FROM vpn_clients WHERE server_id = ? AND status = 'active'");
|
||||
$stmt->execute([$serverId]);
|
||||
$clients = $stmt->fetchAll();
|
||||
|
||||
echo "Found " . count($clients) . " clients in DB.\n";
|
||||
|
||||
foreach ($clients as $client) {
|
||||
$pub = $client['public_key'];
|
||||
if (empty($pub)) {
|
||||
echo "Skipping client {$client['id']} (Empty Public Key)\n";
|
||||
continue;
|
||||
}
|
||||
$psk = $client['preshared_key'];
|
||||
$ip = $client['client_ip'];
|
||||
$allowed = $client['allowed_ips'] ?? "$ip/32"; // Fallback to IP/32
|
||||
|
||||
$conf .= "[Peer]\n";
|
||||
$conf .= "PublicKey = $pub\n";
|
||||
if ($psk)
|
||||
$conf .= "PresharedKey = $psk\n";
|
||||
$conf .= "AllowedIPs = $allowed\n\n";
|
||||
}
|
||||
|
||||
// 5. Write Config
|
||||
// Use host path that matches container volume (-v /opt/amnezia/awg:/opt/amnezia/awg)
|
||||
$hostConfPath = '/opt/amnezia/awg/wg0.conf';
|
||||
|
||||
$escaped = addslashes($conf);
|
||||
$server->executeCommand("echo \"$escaped\" > $hostConfPath", true);
|
||||
// Also copy to container path if mounted (usually same file via bind mount)
|
||||
|
||||
// 6. Restart Interface
|
||||
echo "Restarting WireGuard interface...\n";
|
||||
$server->executeCommand("docker exec -i $containerName wg-quick down wg0 || true", true);
|
||||
$server->executeCommand("docker exec -i $containerName wg-quick up wg0", true);
|
||||
|
||||
echo "Sync Complete.\n";
|
||||
|
||||
} catch (Throwable $e) {
|
||||
echo "Error: " . $e->getMessage() . "\n";
|
||||
exit(1);
|
||||
}
|
||||
@@ -0,0 +1,244 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}{{ t('ai.generation_preview') }} - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">{{ t('ai.generation_preview') }}</h1>
|
||||
<p class="mt-2 text-gray-600">{{ t('ai.generation_preview_description') }}</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<a href="/settings/protocols" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
{{ t('protocols.back_to_protocols') }}
|
||||
</a>
|
||||
<button id="apply-script-btn" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
|
||||
</svg>
|
||||
{{ t('ai.apply_to_protocol') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generation Info -->
|
||||
<div class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('ai.generation_details') }}</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('ai.model_used') }}</label>
|
||||
<p class="mt-1 text-sm text-gray-900">{{ generation.model_used }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('ai.generated_at') }}</label>
|
||||
<p class="mt-1 text-sm text-gray-900">{{ generation.created_at|date('Y-m-d H:i:s') }}</p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('protocols.ubuntu_compatible') }}</label>
|
||||
<div class="mt-1">
|
||||
{% if generation.ubuntu_compatible %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{{ t('common.compatible') }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-red-100 text-red-800">
|
||||
<svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
{{ t('common.not_compatible') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% if generation.protocol_name %}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('ai.associated_protocol') }}</label>
|
||||
<p class="mt-1 text-sm text-gray-900">{{ generation.protocol_name }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prompt -->
|
||||
<div class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('ai.user_prompt') }}</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<p class="text-sm text-gray-900 whitespace-pre-wrap">{{ generation.prompt }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Generated Script -->
|
||||
<div class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="flex justify-between items-center">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('ai.generated_installation_script') }}</h2>
|
||||
<button id="copy-script-btn" class="inline-flex items-center px-3 py-1 border border-gray-300 rounded-md text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
|
||||
</svg>
|
||||
{{ t('ai.copy_script') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="bg-gray-900 text-green-400 p-4 rounded-md overflow-x-auto">
|
||||
<pre id="script-content" class="text-sm whitespace-pre-wrap">{{ script }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Suggestions -->
|
||||
{% if suggestions %}
|
||||
<div class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('ai.suggestions') }}</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<ul class="space-y-2">
|
||||
{% for suggestion in suggestions %}
|
||||
<li class="flex items-start">
|
||||
<svg class="w-5 h-5 text-blue-500 mr-2 mt-0.5 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<span class="text-sm text-gray-900">{{ suggestion }}</span>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Actions -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('common.actions') }}</h2>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="flex flex-wrap gap-3">
|
||||
<button id="download-script-btn" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
{{ t('ai.download_script') }}
|
||||
</button>
|
||||
|
||||
{% if generation.protocol_id %}
|
||||
<button id="view-protocol-btn" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"/>
|
||||
</svg>
|
||||
{{ t('protocols.view_protocol') }}
|
||||
</button>
|
||||
{% endif %}
|
||||
|
||||
<button id="regenerate-btn" class="inline-flex items-center px-4 py-2 border border-purple-300 rounded-md shadow-sm text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
|
||||
</svg>
|
||||
{{ t('ai.regenerate') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const scriptContent = document.getElementById('script-content').textContent;
|
||||
const generationId = {{ generation.id }};
|
||||
const protocolId = {{ generation.protocol_id ?: 'null' }};
|
||||
|
||||
// Copy script
|
||||
document.getElementById('copy-script-btn').addEventListener('click', function() {
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = scriptContent;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
|
||||
// Show feedback
|
||||
const originalText = this.innerHTML;
|
||||
this.innerHTML = '<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/></svg>{{ t('common.copied') }}';
|
||||
setTimeout(() => {
|
||||
this.innerHTML = originalText;
|
||||
}, 2000);
|
||||
});
|
||||
|
||||
// Download script
|
||||
document.getElementById('download-script-btn').addEventListener('click', function() {
|
||||
const blob = new Blob([scriptContent], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `install-${generationId}.sh`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
// Apply to protocol
|
||||
document.getElementById('apply-script-btn').addEventListener('click', function() {
|
||||
if (!protocolId) {
|
||||
alert('{{ t('ai.no_associated_protocol') }}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (confirm('{{ t('ai.confirm_apply_script_to_protocol') }}')) {
|
||||
fetch(`/api/ai/generations/${generationId}/apply`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert('{{ t('ai.script_applied_successfully') }}');
|
||||
window.location.href = `/settings/protocols/${protocolId}/edit`;
|
||||
} else {
|
||||
alert('{{ t('ai.error_applying_script') }}: ' + result.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('{{ t('ai.error_applying_script') }}: ' + error.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// View protocol
|
||||
document.getElementById('view-protocol-btn').addEventListener('click', function() {
|
||||
if (protocolId) {
|
||||
window.location.href = `/settings/protocols/${protocolId}/edit`;
|
||||
}
|
||||
});
|
||||
|
||||
// Regenerate
|
||||
document.getElementById('regenerate-btn').addEventListener('click', function() {
|
||||
if (confirm('{{ t('ai.confirm_regenerate_script') }}')) {
|
||||
// Go back to protocols page with AI assistant open
|
||||
window.location.href = '/settings/protocols';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -18,6 +18,7 @@
|
||||
{% endif %}
|
||||
</dd>
|
||||
</div>
|
||||
<div><dt class="text-sm text-gray-600">Логин</dt><dd>{{ client.name|default('') }}</dd></div>
|
||||
<div><dt class="text-sm text-gray-600">{{ t('common.created') }}</dt><dd>{{ client.created_at }}</dd></div>
|
||||
</dl>
|
||||
<div class="flex gap-2">
|
||||
@@ -150,6 +151,13 @@
|
||||
<p class="text-sm text-gray-600 mt-2">Scan with Amnezia VPN app</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if protocol_output and client.show_text_content %}
|
||||
<div class="bg-white rounded shadow p-6 mt-6">
|
||||
<h3 class="font-bold mb-4">{{ t('clients.connection_instructions') }}</h3>
|
||||
<pre class="mb-0" style="white-space: pre-wrap; overflow-wrap: anywhere; word-break: break-word;">{{ protocol_output }}</pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -40,13 +40,13 @@
|
||||
|
||||
<div class="bg-white rounded-lg shadow p-6">
|
||||
<div class="flex items-center">
|
||||
<div class="p-3 rounded-full bg-purple-100 text-purple-600">
|
||||
<i class="fas fa-check-circle text-2xl"></i>
|
||||
<div class="p-3 rounded-full bg-green-100 text-green-600">
|
||||
<i class="fas fa-wifi text-2xl"></i>
|
||||
</div>
|
||||
<div class="ml-4">
|
||||
<p class="text-sm font-medium text-gray-600">{{ t('dashboard.active_clients') }}</p>
|
||||
<p class="text-sm font-medium text-gray-600">{{ t('dashboard.online_now') }}</p>
|
||||
<p class="text-2xl font-bold text-gray-900">
|
||||
{{ servers|filter(s => s.status == 'active')|length }}
|
||||
{{ online_count|default(0) }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -135,6 +135,28 @@
|
||||
<main class="{% if user %}py-10{% endif %}">
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
<!-- Custom Confirmation Modal -->
|
||||
<div id="confirmModal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-[9999]" style="display:none;">
|
||||
<div class="relative top-20 mx-auto p-5 border w-96 shadow-lg rounded-md bg-white">
|
||||
<div class="mt-3 text-center">
|
||||
<div class="mx-auto flex items-center justify-center h-12 w-12 rounded-full bg-red-100">
|
||||
<i class="fas fa-exclamation-triangle text-red-600 text-xl"></i>
|
||||
</div>
|
||||
<h3 class="text-lg leading-6 font-medium text-gray-900 mt-4" id="confirmModalTitle">Подтверждение</h3>
|
||||
<div class="mt-2 px-7 py-3">
|
||||
<p class="text-sm text-gray-500" id="confirmModalMessage">Вы уверены?</p>
|
||||
</div>
|
||||
<div class="items-center px-4 py-3 flex justify-center gap-4">
|
||||
<button id="confirmModalCancel" class="px-4 py-2 bg-gray-200 text-gray-800 text-base font-medium rounded-md shadow-sm hover:bg-gray-300 focus:outline-none focus:ring-2 focus:ring-gray-300">
|
||||
Отмена
|
||||
</button>
|
||||
<button id="confirmModalOk" class="px-4 py-2 bg-red-600 text-white text-base font-medium rounded-md shadow-sm hover:bg-red-700 focus:outline-none focus:ring-2 focus:ring-red-500">
|
||||
Удалить
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<footer class="bg-white border-t border-gray-200 mt-auto">
|
||||
@@ -174,6 +196,58 @@
|
||||
// Form will submit normally, dropdown will close on page reload
|
||||
}
|
||||
});
|
||||
|
||||
// Custom confirmation modal function (replaces native confirm)
|
||||
window.showConfirmModal = function(message, title) {
|
||||
return new Promise((resolve) => {
|
||||
const modal = document.getElementById('confirmModal');
|
||||
const titleEl = document.getElementById('confirmModalTitle');
|
||||
const msgEl = document.getElementById('confirmModalMessage');
|
||||
const okBtn = document.getElementById('confirmModalOk');
|
||||
const cancelBtn = document.getElementById('confirmModalCancel');
|
||||
|
||||
if (!modal) {
|
||||
// Fallback to native confirm if modal not found
|
||||
resolve(confirm(message));
|
||||
return;
|
||||
}
|
||||
|
||||
titleEl.textContent = title || 'Подтверждение';
|
||||
msgEl.textContent = message || 'Вы уверены?';
|
||||
|
||||
modal.style.display = 'flex';
|
||||
modal.classList.remove('hidden');
|
||||
|
||||
function cleanup() {
|
||||
modal.style.display = 'none';
|
||||
modal.classList.add('hidden');
|
||||
okBtn.removeEventListener('click', onOk);
|
||||
cancelBtn.removeEventListener('click', onCancel);
|
||||
modal.removeEventListener('click', onBackdrop);
|
||||
}
|
||||
|
||||
function onOk() {
|
||||
cleanup();
|
||||
resolve(true);
|
||||
}
|
||||
|
||||
function onCancel() {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
|
||||
function onBackdrop(e) {
|
||||
if (e.target === modal) {
|
||||
cleanup();
|
||||
resolve(false);
|
||||
}
|
||||
}
|
||||
|
||||
okBtn.addEventListener('click', onOk);
|
||||
cancelBtn.addEventListener('click', onCancel);
|
||||
modal.addEventListener('click', onBackdrop);
|
||||
});
|
||||
};
|
||||
</script>
|
||||
|
||||
{% block scripts %}{% endblock %}
|
||||
|
||||
+182
-33
@@ -3,43 +3,113 @@
|
||||
{% block content %}
|
||||
<div class="max-w-3xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<h1 class="text-3xl font-bold mb-8"><i class="fas fa-plus-circle text-purple-600"></i> Add New Server</h1>
|
||||
{% if error %}<div class="mb-4 bg-red-50 border border-red-400 text-red-700 px-4 py-3 rounded">{{ error }}</div>{% endif %}
|
||||
{% if error %}
|
||||
<div class="mb-4 bg-red-50 border border-red-400 text-red-700 px-4 py-3 rounded">{{ error }}</div>
|
||||
{% endif %}
|
||||
<form method="POST" enctype="multipart/form-data" class="bg-white shadow rounded-lg p-6 space-y-6">
|
||||
<div><label class="block text-sm font-medium text-gray-700">Server Name</label><input name="name" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="US Server 1"></div>
|
||||
<div><label class="block text-sm font-medium text-gray-700">Host IP/Domain</label><input name="host" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="123.456.789.0"></div>
|
||||
<div><label class="block text-sm font-medium text-gray-700">SSH Port</label><input name="port" type="number" value="22" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
|
||||
<div><label class="block text-sm font-medium text-gray-700">SSH Username</label><input name="username" value="root" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
|
||||
<div><label class="block text-sm font-medium text-gray-700">SSH Password</label><input name="password" type="password" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md"></div>
|
||||
|
||||
<!-- Import from existing panel -->
|
||||
<div class="border-t pt-6">
|
||||
<div class="flex items-center mb-4">
|
||||
<input type="checkbox" id="enableImport" name="enable_import" class="h-4 w-4 text-purple-600 rounded" onchange="toggleImportFields()">
|
||||
<label for="enableImport" class="ml-2 text-sm font-medium text-gray-700">
|
||||
{{ t('servers.import_from_panel') }}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('servers.creation_mode') }}</label>
|
||||
<div class="mt-2 flex items-center gap-6">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" name="creation_mode" value="manual" class="text-purple-600" {% if selected_mode|default('manual') == 'manual' %}checked{% endif %}>
|
||||
<span class="ml-2 text-sm text-gray-700">{{ t('servers.creation_mode_manual') }}</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" name="creation_mode" value="backup" class="text-purple-600" {% if selected_mode == 'backup' %}checked{% endif %}>
|
||||
<span class="ml-2 text-sm text-gray-700">{{ t('servers.creation_mode_backup') }}</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div id="importFields" style="display: none;" class="space-y-4 pl-6 border-l-2 border-purple-200">
|
||||
</div>
|
||||
|
||||
<div id="manualSection" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Server Name</label>
|
||||
<input name="name" data-field-manual required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="US Server 1" value="{{ form_data.name ?? '' }}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Installation Protocol</label>
|
||||
{% set selectedProtocol = form_data.install_protocol ?? default_protocol %}
|
||||
<select name="install_protocol" data-field-manual class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
{% for protocol in protocols %}
|
||||
<option value="{{ protocol.slug }}" {% if protocol.slug == selectedProtocol %}selected{% endif %}>{{ protocol.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<p id="protocolDescription" class="mt-1 text-xs text-gray-500"></p>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">Host IP/Domain</label>
|
||||
<input name="host" data-field-manual required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" placeholder="123.456.789.0" value="{{ form_data.host ?? '' }}">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">SSH Port</label>
|
||||
<input name="port" data-field-manual type="number" value="{{ form_data.port ?? 22 }}" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">SSH Username</label>
|
||||
<input name="username" data-field-manual value="{{ form_data.username ?? 'root' }}" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('servers.select_panel_type') }}</label>
|
||||
<select name="panel_type" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option value="">-- {{ t('servers.select_panel_type') }} --</option>
|
||||
<option value="wg-easy">{{ t('servers.panel_type_wgeasy') }}</option>
|
||||
<option value="3x-ui">{{ t('servers.panel_type_3xui') }}</option>
|
||||
</select>
|
||||
<label class="block text-sm font-medium text-gray-700">Authentication Method</label>
|
||||
<div class="mt-2 flex items-center gap-6">
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" name="auth_method" value="password" class="text-purple-600" checked onchange="toggleAuthMethod()">
|
||||
<span class="ml-2 text-sm text-gray-700">Password</span>
|
||||
</label>
|
||||
<label class="inline-flex items-center">
|
||||
<input type="radio" name="auth_method" value="ssh_key" class="text-purple-600" onchange="toggleAuthMethod()">
|
||||
<span class="ml-2 text-sm text-gray-700">SSH Key</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('servers.upload_backup_file') }}</label>
|
||||
<input type="file" name="backup_file" accept=".json" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<p class="mt-1 text-xs text-gray-500">
|
||||
wg-easy: db.json | 3x-ui: export.json
|
||||
</p>
|
||||
|
||||
<div id="authPassword">
|
||||
<label class="block text-sm font-medium text-gray-700">SSH Password</label>
|
||||
<input name="password" data-field-manual type="password" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
</div>
|
||||
|
||||
<div id="authSshKey" style="display: none;">
|
||||
<label class="block text-sm font-medium text-gray-700">SSH Private Key</label>
|
||||
<textarea name="ssh_key" data-field-manual rows="6" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-xs" placeholder="-----BEGIN OPENSSH PRIVATE KEY-----..."></textarea>
|
||||
<p class="mt-1 text-xs text-gray-500">Paste your private key (PEM or OpenSSH format)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div id="backupSection" class="space-y-6" style="display: none;">
|
||||
<input type="hidden" name="backup_token" value="{{ form_data.backup_token ?? '' }}">
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('servers.backup_upload_type') }}</label>
|
||||
<select name="backup_upload_type" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" data-field-backup>
|
||||
<option value="auto" {% if form_data.backup_upload_type|default('auto') == 'auto' %}selected{% endif %}>{{ t('servers.backup_type_auto') }}</option>
|
||||
<option value="amnezia_app" {% if form_data.backup_upload_type|default('auto') == 'amnezia_app' %}selected{% endif %}>{{ t('servers.backup_type_amnezia') }}</option>
|
||||
<option value="panel_backup" {% if form_data.backup_upload_type|default('auto') == 'panel_backup' %}selected{% endif %}>{{ t('servers.backup_type_panel') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('servers.upload_backup_file') }}</label>
|
||||
<input type="file" name="backup_upload" accept=".backup,.json" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" data-field-backup>
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('servers.backup_upload_hint') }}</p>
|
||||
</div>
|
||||
|
||||
{% if form_data.uploaded_servers is defined and form_data.uploaded_servers %}
|
||||
<div class="border-t pt-6 space-y-2">
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('servers.backup_server_entry') }}</label>
|
||||
<select name="backup_server_index" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md" data-field-backup>
|
||||
{% for server in form_data.uploaded_servers %}
|
||||
{% set option_host = server.host is defined and server.host is not empty ? server.host : t('common.na') %}
|
||||
{% set option_clients = server.client_count is defined ? server.client_count : t('common.na') %}
|
||||
<option value="{{ server.index }}" {% if form_data.backup_server_index|default('') == (server.index ~ '') %}selected{% endif %}>
|
||||
{{ server.label }} — {{ t('servers.backup_summary_host') }}: {{ option_host }}, {{ t('servers.backup_summary_clients') }}: {{ option_clients }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<button type="submit" class="w-full gradient-bg text-white py-2 px-4 rounded-md hover:opacity-90">
|
||||
<i class="fas fa-save mr-2"></i>Create Server
|
||||
</button>
|
||||
@@ -47,11 +117,90 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function toggleImportFields() {
|
||||
const checkbox = document.getElementById('enableImport');
|
||||
const fields = document.getElementById('importFields');
|
||||
fields.style.display = checkbox.checked ? 'block' : 'none';
|
||||
const modeRadios = Array.prototype.slice.call(document.querySelectorAll('input[name="creation_mode"]'));
|
||||
const manualSection = document.getElementById('manualSection');
|
||||
const backupSection = document.getElementById('backupSection');
|
||||
const manualFields = Array.prototype.slice.call(document.querySelectorAll('[data-field-manual]'));
|
||||
const backupFields = Array.prototype.slice.call(document.querySelectorAll('[data-field-backup]'));
|
||||
const initialMode = {{ selected_mode|default('manual')|json_encode|raw }};
|
||||
|
||||
function setFieldsState(mode) {
|
||||
manualFields.forEach(el => {
|
||||
if (!el) return;
|
||||
el.disabled = mode !== 'manual';
|
||||
});
|
||||
backupFields.forEach(el => {
|
||||
if (!el) return;
|
||||
el.disabled = mode !== 'backup';
|
||||
});
|
||||
}
|
||||
|
||||
function switchMode() {
|
||||
const selectedRadio = document.querySelector('input[name="creation_mode"]:checked');
|
||||
const mode = selectedRadio ? selectedRadio.value : 'manual';
|
||||
if (manualSection) {
|
||||
manualSection.style.display = mode === 'manual' ? 'block' : 'none';
|
||||
}
|
||||
if (backupSection) {
|
||||
backupSection.style.display = mode === 'backup' ? 'block' : 'none';
|
||||
}
|
||||
setFieldsState(mode);
|
||||
}
|
||||
|
||||
modeRadios.forEach(function (radio) {
|
||||
radio.addEventListener('change', switchMode);
|
||||
});
|
||||
|
||||
const initialModeRadio = document.querySelector(`input[name="creation_mode"][value="${initialMode}"]`);
|
||||
if (initialModeRadio) {
|
||||
initialModeRadio.checked = true;
|
||||
}
|
||||
switchMode();
|
||||
|
||||
{% set protocol_map = {} %}
|
||||
{% for protocol in protocols %}
|
||||
{% set protocol_map = protocol_map | merge({ (protocol.slug): (protocol.description ?? '') }) %}
|
||||
{% endfor %}
|
||||
const protocolDescriptions = {{ protocol_map | json_encode | raw }};
|
||||
const protocolDescriptionEl = document.getElementById('protocolDescription');
|
||||
const protocolSelect = document.querySelector('select[name="install_protocol"]');
|
||||
|
||||
function updateProtocolDescription() {
|
||||
if (!protocolSelect || !protocolDescriptionEl) return;
|
||||
const description = protocolDescriptions[protocolSelect.value] || '';
|
||||
protocolDescriptionEl.textContent = description;
|
||||
protocolDescriptionEl.style.display = description ? 'block' : 'none';
|
||||
}
|
||||
|
||||
function toggleAuthMethod() {
|
||||
const method = document.querySelector('input[name="auth_method"]:checked').value;
|
||||
const passwordSection = document.getElementById('authPassword');
|
||||
const keySection = document.getElementById('authSshKey');
|
||||
const passwordInput = passwordSection.querySelector('input');
|
||||
const keyInput = keySection.querySelector('textarea');
|
||||
|
||||
if (method === 'password') {
|
||||
passwordSection.style.display = 'block';
|
||||
keySection.style.display = 'none';
|
||||
passwordInput.required = true;
|
||||
keyInput.required = false;
|
||||
keyInput.value = ''; // Clear key if switching back to password to avoid ambiguity
|
||||
} else {
|
||||
passwordSection.style.display = 'none';
|
||||
keySection.style.display = 'block';
|
||||
passwordInput.required = false;
|
||||
passwordInput.value = ''; // Clear password
|
||||
keyInput.required = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (protocolSelect) {
|
||||
protocolSelect.addEventListener('change', updateProtocolDescription);
|
||||
updateProtocolDescription();
|
||||
}
|
||||
|
||||
// Initialize auth method state
|
||||
toggleAuthMethod();
|
||||
</script>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
+113
-32
@@ -2,55 +2,136 @@
|
||||
{% block title %}Deploy {{ server.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||
<h1 class="text-2xl font-bold mb-6">Deploying: {{ server.name }}</h1>
|
||||
<h1 class="text-2xl font-bold mb-1">Deploying: {{ server.name }}</h1>
|
||||
<p class="text-sm text-gray-400 mb-6">Protocol: {{ server.install_protocol ?? 'amnezia-wg' }}</p>
|
||||
<div id="deployLog" class="bg-gray-900 text-green-400 p-4 rounded font-mono text-sm h-96 overflow-y-auto mb-4">
|
||||
<div>Ready to deploy...</div>
|
||||
</div>
|
||||
<div id="deployActions" class="flex gap-3 mb-4 hidden"></div>
|
||||
<button id="deployBtn" onclick="deploy()" class="gradient-bg text-white px-6 py-2 rounded hover:opacity-90 transition-all disabled:opacity-50 disabled:cursor-not-allowed">
|
||||
<span id="btnText">Start Deployment</span>
|
||||
<i id="btnSpinner" class="fas fa-spinner fa-spin ml-2 hidden"></i>
|
||||
</button>
|
||||
</div>
|
||||
<script>
|
||||
function deploy() {
|
||||
let pendingDecisionToken = null;
|
||||
|
||||
function setButtonState(isProcessing, label) {
|
||||
const btn = document.getElementById('deployBtn');
|
||||
const btnText = document.getElementById('btnText');
|
||||
const btnSpinner = document.getElementById('btnSpinner');
|
||||
btn.disabled = isProcessing;
|
||||
btnText.textContent = label;
|
||||
if (isProcessing) {
|
||||
btnSpinner.classList.remove('hidden');
|
||||
} else {
|
||||
btnSpinner.classList.add('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function appendLog(message, cssClass) {
|
||||
const log = document.getElementById('deployLog');
|
||||
|
||||
// Disable button and show spinner
|
||||
btn.disabled = true;
|
||||
btnText.textContent = 'Deploying...';
|
||||
btnSpinner.classList.remove('hidden');
|
||||
|
||||
log.innerHTML = '<div>📡 Connecting to server...</div>';
|
||||
log.innerHTML += '<div>🔧 Installing Docker...</div>';
|
||||
log.innerHTML += '<div>📦 Building container...</div>';
|
||||
log.innerHTML += '<div>🔐 Generating keys...</div>';
|
||||
log.innerHTML += '<div>⚙️ Configuring WireGuard...</div>';
|
||||
|
||||
fetch('/servers/{{ server.id }}/deploy', {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) {
|
||||
log.innerHTML += '<div class="text-green-500 font-bold">✅ Deployment successful!</div>';
|
||||
log.innerHTML += '<div class="text-yellow-300">🔌 VPN Port: ' + d.vpn_port + '</div>';
|
||||
log.innerHTML += '<div class="text-yellow-300">🔑 Public Key: ' + d.public_key.substring(0, 40) + '...</div>';
|
||||
btnText.textContent = 'Redirecting...';
|
||||
btnSpinner.classList.add('hidden');
|
||||
const line = document.createElement('div');
|
||||
if (cssClass) {
|
||||
line.className = cssClass;
|
||||
}
|
||||
line.innerHTML = message;
|
||||
log.appendChild(line);
|
||||
log.scrollTop = log.scrollHeight;
|
||||
}
|
||||
|
||||
function hideActions() {
|
||||
const actions = document.getElementById('deployActions');
|
||||
actions.classList.add('hidden');
|
||||
actions.innerHTML = '';
|
||||
}
|
||||
|
||||
function showActions(options) {
|
||||
const actions = document.getElementById('deployActions');
|
||||
actions.innerHTML = '';
|
||||
Object.keys(options || {}).forEach(key => {
|
||||
const option = options[key];
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.textContent = option.label || key;
|
||||
btn.className = 'px-4 py-2 rounded bg-white text-gray-800 border border-gray-200 shadow-sm hover:bg-gray-50 transition';
|
||||
btn.onclick = function () {
|
||||
hideActions();
|
||||
deploy(option.mode || key);
|
||||
};
|
||||
actions.appendChild(btn);
|
||||
});
|
||||
if (actions.childElementCount > 0) {
|
||||
actions.classList.remove('hidden');
|
||||
}
|
||||
}
|
||||
|
||||
function deploy(mode) {
|
||||
const payload = {};
|
||||
if (mode) {
|
||||
payload.install_mode = mode;
|
||||
}
|
||||
if (pendingDecisionToken) {
|
||||
payload.decision_token = pendingDecisionToken;
|
||||
}
|
||||
|
||||
if (!mode) {
|
||||
pendingDecisionToken = null;
|
||||
document.getElementById('deployLog').innerHTML = '';
|
||||
appendLog('📡 Connecting to server...');
|
||||
appendLog('🔧 Preparing environment...');
|
||||
}
|
||||
|
||||
hideActions();
|
||||
setButtonState(true, mode ? 'Processing...' : 'Deploying...');
|
||||
|
||||
fetch('/servers/{{ server.id }}/deploy', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload)
|
||||
})
|
||||
.then(async response => {
|
||||
const data = await response.json().catch(() => ({ success: false, error: 'Invalid server response' }));
|
||||
if (!response.ok && !data.requires_action) {
|
||||
throw new Error(data.error || ('HTTP ' + response.status));
|
||||
}
|
||||
return data;
|
||||
})
|
||||
.then(data => {
|
||||
if (data.requires_action) {
|
||||
pendingDecisionToken = data.decision_token || null;
|
||||
const details = data.details || {};
|
||||
appendLog('⚠️ ' + (details.message || 'Existing configuration detected'), 'text-yellow-300');
|
||||
if (details.details && details.details.summary) {
|
||||
appendLog(details.details.summary, 'text-yellow-200');
|
||||
} else if (details.details) {
|
||||
appendLog(JSON.stringify(details.details), 'text-yellow-200 text-xs');
|
||||
}
|
||||
showActions(data.options);
|
||||
setButtonState(false, 'Select action');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.success) {
|
||||
pendingDecisionToken = null;
|
||||
hideActions();
|
||||
appendLog('✅ Deployment successful!', 'text-green-500 font-bold');
|
||||
if (data.vpn_port) {
|
||||
appendLog('🔌 VPN Port: ' + data.vpn_port, 'text-yellow-300');
|
||||
}
|
||||
if (data.public_key) {
|
||||
appendLog('🔑 Public Key: ' + data.public_key.substring(0, 40) + '...', 'text-yellow-300');
|
||||
}
|
||||
setButtonState(true, 'Redirecting...');
|
||||
setTimeout(() => window.location.href = '/servers/{{ server.id }}', 2000);
|
||||
} else {
|
||||
log.innerHTML += '<div class="text-red-500 font-bold">❌ Error: ' + (d.error || 'Unknown error') + '</div>';
|
||||
btn.disabled = false;
|
||||
btnText.textContent = 'Retry Deployment';
|
||||
btnSpinner.classList.add('hidden');
|
||||
appendLog('❌ ' + (data.error || 'Unknown error'), 'text-red-500 font-bold');
|
||||
setButtonState(false, 'Retry Deployment');
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
log.innerHTML += '<div class="text-red-500 font-bold">❌ Network error: ' + e.message + '</div>';
|
||||
btn.disabled = false;
|
||||
btnText.textContent = 'Retry Deployment';
|
||||
btnSpinner.classList.add('hidden');
|
||||
.catch(error => {
|
||||
appendLog('❌ Network error: ' + error.message, 'text-red-500 font-bold');
|
||||
setButtonState(false, 'Retry Deployment');
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -98,9 +98,8 @@
|
||||
<a href="/servers/{{ server.id }}" class="text-purple-600 hover:text-purple-900">
|
||||
<i class="fas fa-eye mr-1"></i>{{ t('servers.view') }}
|
||||
</a>
|
||||
<form method="POST" action="/servers/{{ server.id }}/delete" class="inline"
|
||||
onsubmit="return confirm('{{ t('message.confirm') }} Delete server {{ server.name }}?');">
|
||||
<button type="submit" class="text-red-600 hover:text-red-900">
|
||||
<form method="POST" action="/servers/{{ server.id }}/delete" class="inline" id="delete-form-{{ server.id }}">
|
||||
<button type="button" class="text-red-600 hover:text-red-900" onclick="(async()=>{ event.stopPropagation(); if(await showConfirmModal('Удалить сервер {{ server.name }}?', 'Удаление сервера')) { document.getElementById('delete-form-{{ server.id }}').submit(); } })()">
|
||||
<i class="fas fa-trash mr-1"></i>{{ t('servers.delete') }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
+389
-46
@@ -60,8 +60,50 @@
|
||||
<div><dt class="text-sm text-gray-600">{{ t('common.status') }}</dt><dd><span class="px-2 py-1 bg-green-100 text-green-800 rounded text-sm">{{ server.status }}</span></dd></div>
|
||||
<div><dt class="text-sm text-gray-600">VPN Port</dt><dd>{{ server.vpn_port }}</dd></div>
|
||||
<div><dt class="text-sm text-gray-600">Subnet</dt><dd>{{ server.vpn_subnet }}</dd></div>
|
||||
|
||||
</dl>
|
||||
|
||||
|
||||
<div class="mt-4 flex items-center gap-2">
|
||||
<button type="button" id="uninstallAllBtn" class="px-3 py-1 bg-gray-600 text-white rounded text-sm">Удалить все протоколы</button>
|
||||
<span id="uninstallMsg" class="ml-3 text-sm text-gray-600"></span>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm text-gray-600 mb-1">Добавить протокол</label>
|
||||
<div class="flex items-center gap-2">
|
||||
<select id="availableProtocolSelect" class="px-3 py-2 border rounded">
|
||||
{% for p in available_protocols %}
|
||||
<option value="{{ p.id }}">{{ p.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button id="activateProtocolBtn" class="px-3 py-1 bg-green-600 text-white rounded text-sm">Установить</button>
|
||||
<span id="activateMsg" class="ml-3 text-sm text-gray-600"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Установка протоколов выполняется только через Настройки -->
|
||||
|
||||
<div class="mt-4">
|
||||
<label class="block text-sm text-gray-600 mb-1">Установленные протоколы</label>
|
||||
<div class="space-y-2">
|
||||
{% for sp in server_protocols %}
|
||||
<div class="border rounded px-3 py-2">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="text-sm font-medium">{{ sp.name }} <span class="text-gray-500">({{ sp.slug }})</span></div>
|
||||
<button type="button" class="px-3 py-1 bg-red-600 text-white rounded text-sm btn-uninstall-sp" data-slug="{{ sp.slug }}">Удалить</button>
|
||||
</div>
|
||||
<div class="mt-1 text-xs text-gray-600">
|
||||
{% if sp.server_host %}<span>Host: {{ sp.server_host }}</span>{% endif %}
|
||||
{% if sp.server_port %}<span class="ml-2">Port: {{ sp.server_port }}</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-sm text-gray-500">Нет установленных протоколов</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="uninstallSpMsg" class="mt-2 text-sm text-gray-600"></div>
|
||||
</div>
|
||||
|
||||
{% if server.status == 'active' %}
|
||||
<div class="metric-mini" id="serverMetrics">
|
||||
<div class="metric-row">
|
||||
@@ -96,7 +138,17 @@
|
||||
<form method="POST" action="/servers/{{ server.id }}/clients/create" class="space-y-3" id="createClientForm">
|
||||
<div>
|
||||
<input name="name" placeholder="{{ t('clients.name') }}" required class="w-full px-3 py-2 border rounded" id="clientName">
|
||||
<p class="text-xs text-gray-500 mt-1">Spaces will be replaced with underscore. All characters allowed including Cyrillic.</p>
|
||||
</div>
|
||||
<div>
|
||||
<input name="login" placeholder="Логин (уникально на сервере, пусто — из имени)" class="w-full px-3 py-2 border rounded" id="clientLogin">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">{{ t('ai.protocol_type') }}</label>
|
||||
<select name="protocol_id" class="w-full px-3 py-2 border rounded">
|
||||
{% for sp in server_protocols %}
|
||||
<option value="{{ sp.protocol_id }}">{{ sp.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm text-gray-600 mb-1">{{ t('clients.expiration') }}</label>
|
||||
@@ -136,6 +188,31 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded shadow p-6 mb-8">
|
||||
<h3 class="font-bold mb-4 flex items-center gap-2">
|
||||
<i class="fas fa-file-import text-purple-500"></i>
|
||||
{{ t('servers.config_import_title') }}
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 mb-4">{{ t('servers.config_import_hint') }}</p>
|
||||
<form method="POST" action="/servers/{{ server.id }}/config/import" enctype="multipart/form-data" class="space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('servers.config_import_type_label') }}</label>
|
||||
<select name="import_type" class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<option value="panel_backup">{{ t('servers.config_import_type_panel') }}</option>
|
||||
<option value="amnezia_app">{{ t('servers.config_import_type_amnezia') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{ t('servers.config_import_file_label') }}</label>
|
||||
<input type="file" name="config_file" accept=".json,.backup" required class="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
<p class="text-xs text-gray-500 mt-1">{{ t('servers.config_import_file_hint') }}</p>
|
||||
</div>
|
||||
<button type="submit" class="gradient-bg text-white px-4 py-2 rounded">
|
||||
<i class="fas fa-upload mr-2"></i>{{ t('servers.config_import_submit') }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Backup Section -->
|
||||
<div class="bg-white rounded shadow mb-8">
|
||||
@@ -154,15 +231,29 @@
|
||||
<div class="bg-white rounded shadow">
|
||||
<div class="px-6 py-4 border-b flex justify-between items-center">
|
||||
<h3 class="font-bold">{{ t('clients.title') }} ({{ clients|length }})</h3>
|
||||
<button onclick="syncAllStats({{ server.id }})" class="text-purple-600 hover:text-purple-800 text-sm">
|
||||
<i class="fas fa-sync-alt"></i> {{ t('clients.sync_stats') }}
|
||||
</button>
|
||||
<div class="flex items-center gap-3">
|
||||
<form method="GET" action="/servers/{{ server.id }}" class="flex items-center gap-2">
|
||||
<label class="text-sm text-gray-600">{{ t('ai.protocol_type') }}</label>
|
||||
<select name="protocol_id" class="px-3 py-2 border rounded" onchange="this.form.submit()">
|
||||
<option value="">Все</option>
|
||||
{% for sp in server_protocols %}
|
||||
<option value="{{ sp.protocol_id }}" {% if selected_protocol_id == sp.protocol_id %}selected{% endif %}>{{ sp.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</form>
|
||||
<button onclick="syncAllStats({{ server.id }})" class="text-purple-600 hover:text-purple-800 text-sm">
|
||||
<i class="fas fa-sync-alt"></i> {{ t('clients.sync_stats') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% if clients|length > 0 %}
|
||||
<table class="w-full">
|
||||
<div class="w-full overflow-x-auto">
|
||||
<table class="w-full" style="min-width: 1120px;">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.name') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Логин</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ai.protocol_type') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.ip') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.status') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('clients.expiration') }}</th>
|
||||
@@ -177,12 +268,23 @@
|
||||
{% for client in clients %}
|
||||
<tr class="border-t">
|
||||
<td class="px-6 py-4">{{ client.name }}</td>
|
||||
<td class="px-6 py-4">{{ client.client_ip }}</td>
|
||||
<td class="px-6 py-4">{{ client.name }}</td>
|
||||
<td class="px-6 py-4">
|
||||
{% if client.status == 'active' %}
|
||||
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">{{ t('status.active') }}</span>
|
||||
{% if client.protocol_name %}
|
||||
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{{ client.protocol_name }}</span>
|
||||
{% else %}
|
||||
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t('status.disabled') }}</span>
|
||||
<span class="text-gray-400">-</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4">{{ client.client_ip }}</td>
|
||||
<td class="px-6 py-4" data-client-name="{{ client.name }}" data-client-status="{{ client.status }}" data-last-handshake="{{ client.last_handshake }}">
|
||||
{% set is_online_by_handshake = client.last_handshake and (("now"|date('U') - client.last_handshake|date('U')) < 300) %}
|
||||
{% if client.name in online_logins or is_online_by_handshake %}
|
||||
<span class="online-badge px-2 py-1 bg-green-100 text-green-800 rounded text-xs"><i class="fas fa-wifi mr-1"></i>Online</span>
|
||||
{% elseif client.status == 'active' %}
|
||||
<span class="status-badge px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">{{ t('status.active') }}</span>
|
||||
{% else %}
|
||||
<span class="status-badge px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t('status.disabled') }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
@@ -203,18 +305,18 @@
|
||||
<span class="text-gray-600">{{ client.expires_at|date('Y-m-d') }}</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-gray-400">{{ t('clients.never_expires') }}</span>
|
||||
<span class="text-green-500 text-xl" title="{{ t('clients.never_expires') }}">∞</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<div class="text-gray-600">
|
||||
↑ {{ (client.bytes_sent|default(0) / 1024 / 1024)|number_format(2) }} MB
|
||||
<td class="px-2 py-2 text-xs">
|
||||
<div class="text-gray-600 font-mono">
|
||||
↑{{ (client.bytes_sent|default(0) / 1024 / 1024)|number_format(2) }} MB
|
||||
</div>
|
||||
<div class="text-gray-600">
|
||||
↓ {{ (client.bytes_received|default(0) / 1024 / 1024)|number_format(2) }} MB
|
||||
<div class="text-gray-600 font-mono">
|
||||
↓{{ (client.bytes_received|default(0) / 1024 / 1024)|number_format(2) }} MB
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<td class="px-2 py-2 text-xs text-center">
|
||||
{% if client.traffic_limit %}
|
||||
{% set total_traffic = (client.bytes_sent|default(0) + client.bytes_received|default(0)) %}
|
||||
{% set limit_gb = (client.traffic_limit / 1073741824)|number_format(2) %}
|
||||
@@ -222,26 +324,35 @@
|
||||
{% set percentage = ((total_traffic / client.traffic_limit) * 100)|round %}
|
||||
|
||||
{% if percentage >= 100 %}
|
||||
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">
|
||||
<span class="px-2 py-1 bg-red-100 text-red-800 rounded">
|
||||
<i class="fas fa-exclamation-circle"></i> {{ t('clients.overlimit') }}
|
||||
</span>
|
||||
{% elseif percentage >= 80 %}
|
||||
<span class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs">
|
||||
<span class="px-2 py-1 bg-yellow-100 text-yellow-800 rounded">
|
||||
{{ used_gb }} / {{ limit_gb }} GB ({{ percentage }}%)
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="text-gray-600">{{ used_gb }} / {{ limit_gb }} GB</span>
|
||||
{% endif %}
|
||||
{% else %}
|
||||
<span class="text-gray-400">{{ t('clients.unlimited') }}</span>
|
||||
<span class="text-green-500 text-lg" title="{{ t('clients.unlimited') }}">∞</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<div id="client-speed-{{ client.id }}" class="text-gray-400">-</div>
|
||||
<td class="px-2 py-2 text-xs">
|
||||
<div class="flex flex-col items-center" style="width: 120px; max-width: 120px;">
|
||||
<div style="height: 30px; width: 100%;">
|
||||
<canvas id="clientSparkline-{{ client.id }}"></canvas>
|
||||
</div>
|
||||
<div id="client-speed-{{ client.id }}" class="text-gray-600 text-[10px] mt-1 font-mono text-center leading-tight">
|
||||
<div class="text-green-600 whitespace-nowrap">↑{{ ((client.speed_up|default(0) * 8) / 1000000)|number_format(2) }} Mbit</div>
|
||||
<div class="text-blue-600 whitespace-nowrap">↓{{ ((client.speed_down|default(0) * 8) / 1000000)|number_format(2) }} Mbit</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm">
|
||||
<td class="px-2 py-2 text-xs whitespace-nowrap text-right">
|
||||
{% if client.last_handshake %}
|
||||
<span class="text-gray-600">{{ client.last_handshake }}</span>
|
||||
<span class="text-gray-600 block">{{ client.last_handshake|split(' ')|first }}</span>
|
||||
<span class="text-gray-400 block">{{ client.last_handshake|split(' ')|last }}</span>
|
||||
{% else %}
|
||||
<span class="text-gray-400">{{ t('clients.never') }}</span>
|
||||
{% endif %}
|
||||
@@ -250,7 +361,7 @@
|
||||
<a href="/clients/{{ client.id }}" class="text-purple-600 hover:text-purple-800 mr-2">{{ t('servers.view') }}</a>
|
||||
{% if client.status == 'active' %}
|
||||
<form method="POST" action="/clients/{{ client.id }}/revoke" style="display:inline;">
|
||||
<button type="submit" class="text-orange-600 hover:text-orange-800 mr-2" onclick="return confirm('{{ t('clients.revoke_confirm') }}')">{{ t('clients.revoke') }}</button>
|
||||
<button type="button" class="text-orange-600 hover:text-orange-800 mr-2" onclick="confirmAction(this, '{{ t('clients.revoke_confirm') }}')">{{ t('clients.revoke') }}</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<form method="POST" action="/clients/{{ client.id }}/restore" style="display:inline;">
|
||||
@@ -258,13 +369,14 @@
|
||||
</form>
|
||||
{% endif %}
|
||||
<form method="POST" action="/clients/{{ client.id }}/delete" style="display:inline;">
|
||||
<button type="submit" class="text-red-600 hover:text-red-800" onclick="return confirm('{{ t('clients.delete_confirm') }}')">{{ t('clients.delete') }}</button>
|
||||
<button type="button" class="text-red-600 hover:text-red-800" onclick="confirmAction(this, '{{ t('clients.delete_confirm') }}')">{{ t('clients.delete') }}</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="p-12 text-center text-gray-500">{{ t('clients.no_clients') }}</div>
|
||||
{% endif %}
|
||||
@@ -272,6 +384,12 @@
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function confirmAction(btn, message) {
|
||||
if (await showConfirmModal(message)) {
|
||||
btn.closest('form').submit();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleExpirationInput() {
|
||||
const select = document.getElementById('expirationSelect');
|
||||
const input = document.getElementById('expirationSeconds');
|
||||
@@ -286,6 +404,105 @@ function toggleExpirationInput() {
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const uninstallAllBtn = document.getElementById('uninstallAllBtn');
|
||||
const msg = document.getElementById('uninstallMsg');
|
||||
|
||||
if (uninstallAllBtn) {
|
||||
uninstallAllBtn.addEventListener('click', async function(e) {
|
||||
console.log('uninstallAllBtn clicked');
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (!confirm('Удалить все Amnezia-контейнеры на сервере?')) {
|
||||
console.log('User canceled');
|
||||
return;
|
||||
}
|
||||
console.log('Starting uninstall all...');
|
||||
uninstallAllBtn.disabled = true;
|
||||
msg.innerHTML = '<i class="fas fa-circle-notch fa-spin text-red-600 mr-2"></i><span class="text-gray-700">Удаление всех контейнеров...</span>';
|
||||
try {
|
||||
const res = await fetch(`/servers/{{ server.id }}/protocols/uninstall-all`, { method: 'POST', credentials: 'same-origin' });
|
||||
const data = await res.json();
|
||||
console.log('Response:', data);
|
||||
if (data.success) {
|
||||
msg.textContent = data.message || 'Успешно';
|
||||
setTimeout(() => location.reload(), 1200);
|
||||
} else {
|
||||
msg.textContent = data.error || 'Ошибка';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error:', e);
|
||||
msg.textContent = e.message;
|
||||
}
|
||||
uninstallAllBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
const activateBtn = document.getElementById('activateProtocolBtn');
|
||||
if (activateBtn) {
|
||||
activateBtn.addEventListener('click', async function() {
|
||||
const select = document.getElementById('availableProtocolSelect');
|
||||
const msg2 = document.getElementById('activateMsg');
|
||||
msg2.textContent = '';
|
||||
const pid = select ? select.value : '';
|
||||
if (!pid) { msg2.textContent = 'Нет доступных протоколов'; return; }
|
||||
activateBtn.disabled = true;
|
||||
msg2.innerHTML = '<i class="fas fa-circle-notch fa-spin text-blue-600 mr-2"></i><span class="text-gray-700">Установка протокола...</span>';
|
||||
try {
|
||||
const res = await fetch('/servers/{{ server.id }}/protocols/activate', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
body: 'protocol_id=' + encodeURIComponent(pid)
|
||||
});
|
||||
let data;
|
||||
const ct = res.headers.get('content-type') || '';
|
||||
if (ct.includes('application/json')) { data = await res.json(); } else { data = { error: await res.text() }; }
|
||||
if (res.ok && data && data.success !== false && !data.error) {
|
||||
msg2.textContent = 'Готово';
|
||||
setTimeout(() => location.reload(), 1000);
|
||||
} else {
|
||||
msg2.textContent = (data && data.error) ? data.error : ('Ошибка установки (' + res.status + ')');
|
||||
}
|
||||
} catch (e) {
|
||||
msg2.textContent = e.message || 'Ошибка связи';
|
||||
}
|
||||
activateBtn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
document.querySelectorAll('.btn-uninstall-sp').forEach(btn => {
|
||||
btn.addEventListener('click', async function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
e.stopImmediatePropagation();
|
||||
const confirmed = await showConfirmModal('Удалить протокол и всех его клиентов?', 'Удаление протокола');
|
||||
if (!confirmed) return;
|
||||
const slug = btn.getAttribute('data-slug');
|
||||
const m = document.getElementById('uninstallSpMsg');
|
||||
m.textContent = '';
|
||||
btn.disabled = true;
|
||||
m.innerHTML = '<i class="fas fa-circle-notch fa-spin text-red-600 mr-2"></i><span class="text-gray-700">Удаление протокола...</span>';
|
||||
try {
|
||||
const resp = await fetch('/servers/{{ server.id }}/protocols/' + encodeURIComponent(slug) + '/uninstall', { method: 'POST', credentials: 'same-origin' });
|
||||
let data;
|
||||
const ct = resp.headers.get('content-type') || '';
|
||||
if (ct.includes('application/json')) { data = await resp.json(); } else { data = { error: await resp.text() }; }
|
||||
if (resp.ok && data && !data.error) {
|
||||
m.textContent = 'Удалено. Клиенты: ' + (data.clients_removed || 0);
|
||||
setTimeout(() => location.reload(), 800);
|
||||
} else {
|
||||
m.textContent = (data && data.error) ? data.error : ('Ошибка удаления (' + resp.status + ')');
|
||||
}
|
||||
} catch (e) {
|
||||
m.textContent = e.message || 'Ошибка связи';
|
||||
}
|
||||
btn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
});
|
||||
|
||||
function toggleTrafficInput() {
|
||||
const select = document.getElementById('trafficSelect');
|
||||
const input = document.getElementById('trafficMegabytes');
|
||||
@@ -304,12 +521,11 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('createClientForm');
|
||||
const clientNameInput = document.getElementById('clientName');
|
||||
|
||||
// Auto-replace spaces with underscores
|
||||
if (clientNameInput) {
|
||||
clientNameInput.addEventListener('input', function(e) {
|
||||
// Replace only spaces with underscore, allow all other characters including Cyrillic
|
||||
const clientLoginInput = document.getElementById('clientLogin');
|
||||
if (clientLoginInput) {
|
||||
clientLoginInput.addEventListener('input', function(e) {
|
||||
let value = e.target.value;
|
||||
let sanitized = value.replace(/ /g, '_');
|
||||
let sanitized = value.replace(/\s+/g, '_');
|
||||
if (value !== sanitized) {
|
||||
e.target.value = sanitized;
|
||||
}
|
||||
@@ -662,39 +878,134 @@ if (document.getElementById('cpuSparkline')) {
|
||||
}
|
||||
|
||||
// Update client speeds
|
||||
let clientCharts = {};
|
||||
|
||||
function prepareSparklineSeries(values) {
|
||||
const cleaned = values.map(v => {
|
||||
const n = Number(v);
|
||||
return Number.isFinite(n) && n > 0 ? n : 0;
|
||||
});
|
||||
|
||||
const nonZero = cleaned.filter(v => v > 0).sort((a, b) => a - b);
|
||||
let capped = cleaned;
|
||||
|
||||
// Suppress single extreme spikes that make the mini-chart unreadable.
|
||||
if (nonZero.length >= 5) {
|
||||
const p95Index = Math.floor((nonZero.length - 1) * 0.95);
|
||||
const p95 = nonZero[p95Index] || 0;
|
||||
const cap = p95 > 0 ? p95 * 2 : 0;
|
||||
if (cap > 0) {
|
||||
capped = cleaned.map(v => Math.min(v, cap));
|
||||
}
|
||||
}
|
||||
|
||||
// Small moving average to reduce jitter on tiny sparklines.
|
||||
return capped.map((_, i, arr) => {
|
||||
const from = Math.max(0, i - 1);
|
||||
const to = Math.min(arr.length - 1, i + 1);
|
||||
let sum = 0;
|
||||
let count = 0;
|
||||
for (let j = from; j <= to; j++) {
|
||||
sum += arr[j];
|
||||
count++;
|
||||
}
|
||||
return count > 0 ? sum / count : 0;
|
||||
});
|
||||
}
|
||||
|
||||
async function updateClientSpeeds() {
|
||||
const clientRows = document.querySelectorAll('[id^="client-speed-"]');
|
||||
|
||||
console.log('Found client speed rows:', clientRows.length);
|
||||
|
||||
for (const row of clientRows) {
|
||||
const clientId = row.id.replace('client-speed-', '');
|
||||
|
||||
console.log(`Fetching metrics for client ${clientId}`);
|
||||
const canvasId = `clientSparkline-${clientId}`;
|
||||
const canvas = document.getElementById(canvasId);
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/clients/${clientId}/metrics?hours=1`, {
|
||||
const response = await fetch(`/api/clients/${clientId}/metrics?hours=24`, { // Fetch 24h for sparkline
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
console.log(`Client ${clientId} metrics:`, data);
|
||||
|
||||
if (data.success && data.metrics && data.metrics.length > 0) {
|
||||
const latest = data.metrics[data.metrics.length - 1];
|
||||
const speedUp = parseFloat(latest.speed_up_kbps).toFixed(1);
|
||||
const speedDown = parseFloat(latest.speed_down_kbps).toFixed(1);
|
||||
if (data.success && data.metrics) {
|
||||
const metrics = data.metrics; // Use all points for chart
|
||||
|
||||
// Format as compact badge
|
||||
row.innerHTML = `<span class="text-xs text-gray-700">↑${speedUp} ↓${speedDown} KB/s</span>`;
|
||||
// 1. Render/Update Chart
|
||||
if (canvas) {
|
||||
const labels = metrics.map((_, i) => i);
|
||||
const rawUp = metrics.map(m => (parseFloat(m.speed_up_kbps) / 1000)); // Mbps
|
||||
const rawDown = metrics.map(m => (parseFloat(m.speed_down_kbps) / 1000)); // Mbps
|
||||
const dataUp = prepareSparklineSeries(rawUp);
|
||||
const dataDown = prepareSparklineSeries(rawDown);
|
||||
|
||||
if (clientCharts[clientId]) {
|
||||
// Update existing chart
|
||||
clientCharts[clientId].data.labels = labels;
|
||||
clientCharts[clientId].data.datasets[0].data = dataUp;
|
||||
clientCharts[clientId].data.datasets[1].data = dataDown;
|
||||
clientCharts[clientId].update('none');
|
||||
} else {
|
||||
// Create new chart
|
||||
clientCharts[clientId] = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: labels,
|
||||
datasets: [
|
||||
{
|
||||
label: 'Up',
|
||||
data: dataUp,
|
||||
borderColor: '#16a34a', // green-600
|
||||
borderWidth: 1.5,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
tension: 0.4
|
||||
},
|
||||
{
|
||||
label: 'Down',
|
||||
data: dataDown,
|
||||
borderColor: '#2563eb', // blue-600
|
||||
borderWidth: 1.5,
|
||||
pointRadius: 0,
|
||||
fill: false,
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: { legend: { display: false }, tooltip: { enabled: false } },
|
||||
scales: {
|
||||
x: { display: false },
|
||||
y: { display: false, beginAtZero: true }
|
||||
},
|
||||
animation: false
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Update Text Badge (Last Known Speed)
|
||||
if (metrics.length > 0) {
|
||||
const latest = metrics[metrics.length - 1];
|
||||
const speedUp = (parseFloat(latest.speed_up_kbps) / 1000).toFixed(2);
|
||||
const speedDown = (parseFloat(latest.speed_down_kbps) / 1000).toFixed(2);
|
||||
|
||||
row.innerHTML = `
|
||||
<div class="text-green-600 whitespace-nowrap">↑${speedUp} Mbit</div>
|
||||
<div class="text-blue-600 whitespace-nowrap">↓${speedDown} Mbit</div>
|
||||
`;
|
||||
} else {
|
||||
row.innerHTML = '<span class="text-gray-400">-</span>';
|
||||
}
|
||||
|
||||
} else {
|
||||
console.log(`No metrics for client ${clientId}`);
|
||||
row.innerHTML = '<span class="text-xs text-gray-400">-</span>';
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to fetch metrics for client ${clientId}:`, error);
|
||||
row.innerHTML = '<span class="text-xs text-gray-400">-</span>';
|
||||
row.innerHTML = '<span class="text-xs text-gray-400">Error</span>';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -704,6 +1015,38 @@ if (document.querySelector('[id^="client-speed-"]')) {
|
||||
updateClientSpeeds();
|
||||
setInterval(updateClientSpeeds, 30000);
|
||||
}
|
||||
|
||||
// Real-time online status updates
|
||||
async function updateOnlineStatus() {
|
||||
const serverId = {{ server.id }};
|
||||
try {
|
||||
const response = await fetch(`/api/servers/${serverId}/online`, {
|
||||
credentials: 'same-origin'
|
||||
});
|
||||
if (!response.ok) return;
|
||||
const data = await response.json();
|
||||
if (!data.success) return;
|
||||
|
||||
const onlineSet = new Set(data.online);
|
||||
document.querySelectorAll('td[data-client-name]').forEach(cell => {
|
||||
const clientName = cell.dataset.clientName;
|
||||
const clientStatus = cell.dataset.clientStatus;
|
||||
|
||||
if (onlineSet.has(clientName)) {
|
||||
cell.innerHTML = '<span class="online-badge px-2 py-1 bg-green-100 text-green-800 rounded text-xs"><i class="fas fa-wifi mr-1"></i>Online</span>';
|
||||
} else if (clientStatus === 'active') {
|
||||
cell.innerHTML = '<span class="status-badge px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">{{ t("status.active") }}</span>';
|
||||
} else {
|
||||
cell.innerHTML = '<span class="status-badge px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t("status.disabled") }}</span>';
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Failed to update online status:', e);
|
||||
}
|
||||
}
|
||||
|
||||
// Poll every 5 seconds
|
||||
setInterval(updateOnlineStatus, 5000);
|
||||
{% endif %}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
+222
-2
@@ -42,21 +42,54 @@
|
||||
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-language mr-2"></i>{{ t('settings.translations') }}
|
||||
</a>
|
||||
<a href="/tools/qr-decode"
|
||||
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-qrcode mr-2"></i>QR Декодер
|
||||
</a>
|
||||
{% if user.role == 'admin' %}
|
||||
<a href="#" onclick="showTab('users'); return false;" id="tab-users"
|
||||
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-users mr-2"></i>{{ t('settings.users') }}
|
||||
</a>
|
||||
<a href="/settings/ldap"
|
||||
<a href="#" onclick="showTab('ldap'); return false;" id="tab-ldap"
|
||||
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-network-wired mr-2"></i>LDAP
|
||||
</a>
|
||||
<a href="#" onclick="showTab('protocols'); return false;" id="tab-protocols"
|
||||
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-cubes mr-2"></i>{{ t('settings.protocol_management') }}
|
||||
</a>
|
||||
<a href="/admin/logs"
|
||||
class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-file-alt mr-2"></i>{{ 'Логи' | trans }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Profile Tab -->
|
||||
<div id="content-profile" class="tab-content">
|
||||
<div class="bg-white shadow rounded-lg mb-6">
|
||||
<div class="px-6 py-5 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">
|
||||
<i class="fas fa-user mr-2 text-purple-600"></i>Профиль
|
||||
</h2>
|
||||
</div>
|
||||
<div class="px-6 py-5">
|
||||
<form method="POST" action="/settings/profile">
|
||||
<div class="space-y-4 max-w-md">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Отображаемое имя</label>
|
||||
<input type="text" name="display_name" value="{{ user.display_name|default(user.name) }}"
|
||||
class="block w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
||||
</div>
|
||||
<button type="submit" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
|
||||
<i class="fas fa-save mr-2"></i>{{ t('form.save') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-5 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">
|
||||
@@ -278,6 +311,11 @@
|
||||
<i class="fas fa-trash"></i> {{ t('clients.delete') }}
|
||||
</button>
|
||||
</form>
|
||||
{% else %}
|
||||
<!-- For the current user show a Change Password action in Actions column -->
|
||||
<a href="#" id="self-change-password" onclick="showTab('profile'); return false;" class="text-purple-600 hover:text-purple-900">
|
||||
<i class="fas fa-key mr-1"></i> {{ t('settings.change_password') }}
|
||||
</a>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
@@ -286,6 +324,163 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- LDAP Tab -->
|
||||
<div id="content-ldap" class="tab-content hidden">
|
||||
<div class="bg-white rounded-lg shadow-md p-6 mb-6 max-w-5xl">
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<h2 class="text-xl font-semibold text-gray-900">{{ t('ldap.settings') }}</h2>
|
||||
<button id="testConnection" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700">
|
||||
{{ t('ldap.test_connection') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<form id="ldapForm" method="POST" action="/settings/ldap/save">
|
||||
<div class="mb-6 p-4 bg-gray-50 rounded-lg">
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox" name="enabled" value="1"
|
||||
{% if config.enabled %}checked{% endif %}
|
||||
class="w-5 h-5 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-3 text-lg font-medium text-gray-900">{{ t('ldap.enable_ldap_auth') }}</span>
|
||||
</label>
|
||||
<p class="mt-2 ml-8 text-sm text-gray-600">{{ t('ldap.enable_description') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ t('ldap.host') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="host" value="{{ config.host }}" required
|
||||
placeholder="ldap.example.com"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ t('ldap.port') }}
|
||||
</label>
|
||||
<input type="number" name="port" value="{{ config.port }}"
|
||||
placeholder="389"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="flex items-center cursor-pointer">
|
||||
<input type="checkbox" name="use_tls" value="1"
|
||||
{% if config.use_tls %}checked{% endif %}
|
||||
class="w-4 h-4 text-blue-600 rounded focus:ring-2 focus:ring-blue-500">
|
||||
<span class="ml-2 text-sm text-gray-700">{{ t('ldap.use_tls') }} (LDAPS)</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ t('ldap.base_dn') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="base_dn" value="{{ config.base_dn }}" required
|
||||
placeholder="dc=example,dc=com"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.base_dn_description') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ t('ldap.bind_dn') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="text" name="bind_dn" value="{{ config.bind_dn }}" required
|
||||
placeholder="cn=admin,dc=example,dc=com"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.bind_dn_description') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ t('ldap.bind_password') }} <span class="text-red-500">*</span>
|
||||
</label>
|
||||
<input type="password" name="bind_password" value="{{ config.bind_password }}" required
|
||||
placeholder="••••••••"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ t('ldap.user_search_filter') }}
|
||||
</label>
|
||||
<input type="text" name="user_search_filter" value="{{ config.user_search_filter }}"
|
||||
placeholder="(uid=%s)"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.user_search_filter_description') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ t('ldap.group_search_filter') }}
|
||||
</label>
|
||||
<input type="text" name="group_search_filter" value="{{ config.group_search_filter }}"
|
||||
placeholder="(memberUid=%s)"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
{{ t('ldap.sync_interval') }}
|
||||
</label>
|
||||
<input type="number" name="sync_interval" value="{{ config.sync_interval }}"
|
||||
placeholder="30"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500">
|
||||
<p class="mt-1 text-xs text-gray-500">{{ t('ldap.sync_interval_description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6 flex gap-4">
|
||||
<button type="submit" class="px-6 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700">
|
||||
{{ t('common.save') }}
|
||||
</button>
|
||||
<a href="/settings" class="px-6 py-2 bg-gray-300 text-gray-700 rounded-lg hover:bg-gray-400">
|
||||
{{ t('common.cancel') }}
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="bg-white rounded-lg shadow-md p-6 max-w-5xl">
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-4">{{ t('ldap.group_mappings') }}</h3>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ldap.group') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ldap.role') }}</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">{{ t('ldap.description') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="bg-white divide-y divide-gray-200">
|
||||
{% for mapping in mappings %}
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">{{ mapping.ldap_group }}</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap">
|
||||
<span class="px-2 py-1 text-xs font-semibold rounded-full
|
||||
{% if mapping.role_name == 'admin' %}bg-red-100 text-red-800
|
||||
{% elseif mapping.role_name == 'manager' %}bg-blue-100 text-blue-800
|
||||
{% else %}bg-gray-100 text-gray-800{% endif %}">
|
||||
{{ mapping.role_name }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 text-sm text-gray-500">{{ mapping.description }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Protocols Tab -->
|
||||
<div id="content-protocols" class="tab-content hidden">
|
||||
{% include 'settings/protocols_management.twig' %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -341,8 +536,33 @@ function translateLanguage(lang) {
|
||||
.catch(err => {
|
||||
alert('{{ t('message.error') }}: ' + err.message);
|
||||
button.disabled = false;
|
||||
button.innerHTML = originalText;
|
||||
button.innerHTML = originalText;
|
||||
});
|
||||
}
|
||||
|
||||
// LDAP test button inside settings page
|
||||
document.addEventListener('click', function(e) {
|
||||
if (e.target && e.target.id === 'testConnection') {
|
||||
const btn = e.target;
|
||||
btn.disabled = true;
|
||||
btn.textContent = '{{ t('ldap.testing') }}...';
|
||||
fetch('/settings/ldap/test', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert('✓ ' + result.message);
|
||||
} else {
|
||||
alert('✗ ' + result.message);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
alert('{{ t('ldap.connection_test_failed') }}: ' + err.message);
|
||||
})
|
||||
.finally(() => {
|
||||
btn.disabled = false;
|
||||
btn.textContent = '{{ t('ldap.test_connection') }}';
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="mb-8 flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">{{ 'Логи приложения' | trans }}</h1>
|
||||
<p class="mt-2 text-sm text-gray-600">{{ 'Просмотр, поиск и управление файлами логов' | trans }}</p>
|
||||
</div>
|
||||
{% if log_files | length > 0 %}
|
||||
<div class="flex space-x-2">
|
||||
<button class="px-4 py-2 bg-yellow-500 text-white rounded-md hover:bg-yellow-600" id="btnClearAll" title="{{ 'Удалить все логи' | trans }}">
|
||||
<i class="fas fa-trash mr-2"></i>{{ 'Очистить все' | trans }}
|
||||
</button>
|
||||
<a href="/admin/logs/download?file={{ selected_file }}"
|
||||
class="px-4 py-2 rounded-md border {{ selected_file ? 'bg-white text-gray-700 hover:bg-gray-50' : 'bg-gray-100 text-gray-400 cursor-not-allowed' }}">
|
||||
<i class="fas fa-download mr-2"></i>{{ 'Скачать' | trans }}
|
||||
</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if user and user.role == 'admin' %}
|
||||
<div class="mb-6 border-b border-gray-200">
|
||||
<nav class="-mb-px flex space-x-8">
|
||||
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-user mr-2"></i>{{ t('settings.profile') }}
|
||||
</a>
|
||||
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-key mr-2"></i>{{ t('settings.api_keys') }}
|
||||
</a>
|
||||
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-language mr-2"></i>{{ t('settings.translations') }}
|
||||
</a>
|
||||
<a href="/settings" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-users mr-2"></i>{{ t('settings.users') }}
|
||||
</a>
|
||||
<a href="/settings/ldap" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-network-wired mr-2"></i>LDAP
|
||||
</a>
|
||||
<a href="/settings/protocols" class="tab-link border-transparent text-gray-500 hover:text-gray-700 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-cubes mr-2"></i>Protocols
|
||||
</a>
|
||||
<a href="/admin/logs" class="tab-link border-purple-500 text-purple-600 py-4 px-1 border-b-2 font-medium text-sm">
|
||||
<i class="fas fa-file-alt mr-2"></i>{{ 'Логи' | trans }}
|
||||
</a>
|
||||
</nav>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="grid grid-cols-12 gap-6">
|
||||
<!-- Sidebar with file list -->
|
||||
<div class="col-span-12 md:col-span-3">
|
||||
<div class="bg-white shadow rounded-lg overflow-hidden">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ 'Файлы логов' | trans }}</h2>
|
||||
</div>
|
||||
<div class="max-h-[600px] overflow-y-auto">
|
||||
{% if log_files | length > 0 %}
|
||||
{% for file in log_files %}
|
||||
<a href="/admin/logs?file={{ file.path }}" class="block px-6 py-4 border-b border-gray-100 hover:bg-gray-50 {{ selected_file == file.path ? 'bg-purple-50' : '' }}">
|
||||
<div class="flex items-start justify-between">
|
||||
<div class="flex-1">
|
||||
<div class="text-sm font-medium" style="word-break: break-all;">{{ file.name }}</div>
|
||||
<div class="text-xs text-gray-500">{{ file.size_formatted }}</div>
|
||||
</div>
|
||||
<button type="button" class="text-red-600 hover:text-red-800 text-sm delete-log" data-file="{{ file.path }}" title="{{ 'Удалить' | trans }}">
|
||||
<i class="fas fa-trash-alt"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-2">{{ file.modified_formatted }}</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<div class="px-6 py-10 text-center text-gray-500">
|
||||
{{ 'Логи не найдены' | trans }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content area -->
|
||||
<div class="col-span-12 md:col-span-9">
|
||||
{% if selected_file %}
|
||||
<!-- File info -->
|
||||
<div class="bg-white shadow rounded-lg mb-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<div class="md:flex md:items-center md:justify-between">
|
||||
<div class="mb-2 md:mb-0">
|
||||
<h3 class="text-sm text-gray-700">{{ 'Файл:' | trans }} <code class="text-gray-900">{{ selected_file }}</code></h3>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500">
|
||||
<strong>{{ 'Размер:' | trans }}</strong> {{ file_size | default(0) | bytes_format }}
|
||||
<span class="mx-2">•</span>
|
||||
<strong>{{ 'Строк:' | trans }}</strong> {{ line_count | number_format(0, '.', ' ') }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search form -->
|
||||
<div class="bg-white shadow rounded-lg mb-4">
|
||||
<div class="px-6 py-4">
|
||||
<form id="searchForm" class="md:flex md:items-center md:space-x-3">
|
||||
<input type="hidden" name="file" value="{{ selected_file }}">
|
||||
<div class="flex-1 mb-3 md:mb-0">
|
||||
<input type="text" id="searchQuery" name="query" required
|
||||
placeholder="{{ 'Поиск в логе...' | trans }}"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:ring-purple-500 focus:border-purple-500">
|
||||
</div>
|
||||
<label class="inline-flex items-center mb-3 md:mb-0">
|
||||
<input type="checkbox" id="caseSensitive" name="case_sensitive" class="rounded border-gray-300 text-purple-600 focus:ring-purple-500">
|
||||
<span class="ml-2 text-sm text-gray-600">{{ 'Учитывать регистр' | trans }}</span>
|
||||
</label>
|
||||
<button type="submit" class="px-4 py-2 bg-purple-600 text-white rounded-md hover:bg-purple-700">
|
||||
<i class="fas fa-search mr-2"></i>{{ 'Найти' | trans }}
|
||||
</button>
|
||||
<button type="button" class="px-4 py-2 bg-gray-100 text-gray-700 rounded-md hover:bg-gray-200" id="statsBtn">
|
||||
<i class="fas fa-chart-bar mr-2"></i>{{ 'Статистика' | trans }}
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Search results -->
|
||||
<div id="searchResults" class="hidden bg-blue-50 border border-blue-200 text-blue-800 px-4 py-3 rounded mb-4">
|
||||
<strong>{{ 'Результаты поиска:' | trans }}</strong>
|
||||
<div id="resultsContent" class="mt-2 text-sm"></div>
|
||||
</div>
|
||||
|
||||
<!-- Statistics -->
|
||||
<div id="statsPanel" class="hidden bg-white shadow rounded-lg mb-4">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ 'Статистика логов' | trans }}</h3>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="text-center p-3 border-r">
|
||||
<div class="text-2xl text-purple-600" id="totalLines">0</div>
|
||||
<div class="text-xs text-gray-500">{{ 'Всего строк' | trans }}</div>
|
||||
</div>
|
||||
<div class="text-center p-3 border-r">
|
||||
<div class="text-2xl text-red-600" id="errorCount">0</div>
|
||||
<div class="text-xs text-gray-500">{{ 'Ошибок' | trans }}</div>
|
||||
</div>
|
||||
<div class="text-center p-3 border-r">
|
||||
<div class="text-2xl text-yellow-600" id="warningCount">0</div>
|
||||
<div class="text-xs text-gray-500">{{ 'Предупреждений' | trans }}</div>
|
||||
</div>
|
||||
<div class="text-center p-3">
|
||||
<div class="text-2xl text-green-600" id="successCount">0</div>
|
||||
<div class="text-xs text-gray-500">{{ 'Успехов' | trans }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-2" id="lastModified"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Log content -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-3 border-b border-gray-200 flex items-center justify-between">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ 'Содержание логов' | trans }}</h3>
|
||||
<button type="button" class="px-2 py-1 text-sm border rounded hover:bg-gray-50" id="toggleLineNumbers">
|
||||
<i class="fas fa-list-ol"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="px-6 py-4">
|
||||
<pre class="mb-0 max-h-[600px] overflow-auto"><code id="logContent" class="show-line-numbers">{{ log_content }}</code></pre>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="bg-blue-50 border border-blue-200 text-blue-800 px-6 py-10 rounded text-center">
|
||||
<i class="fas fa-info-circle text-2xl mb-3"></i>
|
||||
<div class="text-lg">{{ 'Выберите файл логов для просмотра' | trans }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const selectedFile = '{{ selected_file }}';
|
||||
|
||||
// Delete log file
|
||||
document.querySelectorAll('.delete-log').forEach(btn => {
|
||||
btn.addEventListener('click', function(e) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const file = this.dataset.file;
|
||||
if (confirm('{{ "Удалить этот файл логов?" | trans }}')) {
|
||||
fetch('/admin/logs/delete', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'file=' + encodeURIComponent(file)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
window.location.href = data.redirect;
|
||||
} else {
|
||||
alert('{{ "Ошибка:" | trans }} ' + data.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Clear all logs
|
||||
const clearAllBtn = document.getElementById('btnClearAll');
|
||||
if (clearAllBtn) {
|
||||
clearAllBtn.addEventListener('click', function() {
|
||||
if (confirm('{{ "Удалить ВСЕ файлы логов? Это действие необратимо." | trans }}')) {
|
||||
fetch('/admin/logs/clear-all', {method: 'POST'})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
alert(data.message);
|
||||
window.location.href = data.redirect;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Search logs
|
||||
document.getElementById('searchForm').addEventListener('submit', function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
fetch('/admin/logs/search', {
|
||||
method: 'POST',
|
||||
body: new URLSearchParams(formData)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showSearchResults(data);
|
||||
} else {
|
||||
alert('{{ "Ошибка:" | trans }} ' + data.message);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Statistics
|
||||
document.getElementById('statsBtn').addEventListener('click', function() {
|
||||
if (!selectedFile) return;
|
||||
|
||||
fetch('/admin/logs/stats', {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: 'file=' + encodeURIComponent(selectedFile)
|
||||
})
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
showStatistics(data);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Toggle line numbers
|
||||
document.getElementById('toggleLineNumbers').addEventListener('click', function() {
|
||||
const content = document.getElementById('logContent');
|
||||
content.classList.toggle('show-line-numbers');
|
||||
});
|
||||
|
||||
function showSearchResults(data) {
|
||||
const resultsDiv = document.getElementById('searchResults');
|
||||
const resultsContent = document.getElementById('resultsContent');
|
||||
|
||||
if (data.results_count === 0) {
|
||||
resultsContent.innerHTML = '<p class="mb-0">{{ "Результатов не найдено" | trans }}</p>';
|
||||
} else {
|
||||
let html = '<p class="mb-2">{{ "Найдено совпадений:" | trans }} <strong>' + data.results_count + '</strong></p>';
|
||||
html += '<div class="results-list" style="max-height: 300px; overflow-y: auto;">';
|
||||
|
||||
data.results.forEach((result, idx) => {
|
||||
html += '<div class="border-bottom pb-2 mb-2">';
|
||||
html += '<small class="text-muted">{{ "Строка" | trans }} ' + result.line + ':</small><br>';
|
||||
html += '<code>' + escapeHtml(result.content.substring(0, 200)) + (result.content.length > 200 ? '...' : '') + '</code>';
|
||||
html += '</div>';
|
||||
});
|
||||
|
||||
html += '</div>';
|
||||
resultsContent.innerHTML = html;
|
||||
}
|
||||
|
||||
resultsDiv.style.display = 'block';
|
||||
}
|
||||
|
||||
function showStatistics(data) {
|
||||
document.getElementById('totalLines').textContent = data.total_lines;
|
||||
document.getElementById('errorCount').textContent = data.errors;
|
||||
document.getElementById('warningCount').textContent = data.warnings;
|
||||
document.getElementById('successCount').textContent = data.success;
|
||||
document.getElementById('lastModified').textContent = '{{ "Последнее обновление:" | trans }} ' + data.last_modified;
|
||||
|
||||
document.getElementById('statsPanel').style.display = 'block';
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const map = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": '''
|
||||
};
|
||||
return text.replace(/[&<>"']/g, m => map[m]);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#logContent {
|
||||
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
#logContent.show-line-numbers {
|
||||
counter-reset: line;
|
||||
}
|
||||
|
||||
#logContent.show-line-numbers::before {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.results-list {
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 3px;
|
||||
padding: 10px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.list-group-item.active {
|
||||
background-color: #007bff;
|
||||
border-color: #007bff;
|
||||
}
|
||||
</style>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,707 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}{{ editing ? t('protocols.edit_protocol') : t('protocols.create_protocol') }} - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">{{ editing ? t('protocols.edit_protocol') : t('protocols.create_protocol') }}</h1>
|
||||
<p class="mt-2 text-gray-600">{{ editing ? t('protocols.edit_protocol_description') : t('protocols.create_protocol_description') }}</p>
|
||||
</div>
|
||||
<a href="/settings/protocols" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
{{ t('protocols.back_to_protocols') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success/Error Messages -->
|
||||
{% if success %}
|
||||
<div class="mb-4 bg-green-50 border border-green-200 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<svg class="w-5 h-5 text-green-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<p class="text-green-800">{{ success }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="mb-4 bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<svg class="w-5 h-5 text-red-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<p class="text-red-800">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Protocol Form -->
|
||||
<form id="protocol-form" method="POST" action="/settings/protocols/save" class="space-y-6">
|
||||
{% if editing %}
|
||||
<input type="hidden" name="id" value="{{ editing.id }}">
|
||||
{% endif %}
|
||||
|
||||
<!-- Basic Information -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">{{ t('protocols.basic_information') }}</h2>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<label for="name" class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.name_label') }} *</label>
|
||||
<input type="text" id="name" name="name" value="{{ editing.name ?? '' }}" required class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.name_help') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="slug" class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.slug_label') }} *</label>
|
||||
<input type="text" id="slug" name="slug" value="{{ editing.slug ?? '' }}" required pattern="[a-z0-9_-]+" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.slug_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-6">
|
||||
<label for="description" class="block text-sm font-medium text-gray-700 mb-2">{{ t('common.description') }}</label>
|
||||
<textarea id="description" name="description" rows="3" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">{{ editing.description ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.description_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Installation Script -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.installation_script') }}</h2>
|
||||
<button type="button" id="ai-help-btn" class="inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
{{ t('ai.get_ai_help') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea id="install_script" name="install_script" rows="15" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="#!/bin/bash # Installation script here">{{ editing.install_script ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.install_script_help') }}</p>
|
||||
<div class="mt-3 flex items-center space-x-2">
|
||||
<button id="test-install-btn" type="button" class="inline-flex items-center px-3 py-2 bg-blue-600 text-white rounded hover:bg-blue-700">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4"/></svg>
|
||||
{{ t('protocols.test_install') }}
|
||||
</button>
|
||||
<span class="text-xs text-gray-500">{{ t('protocols.testing_on_ubuntu22') }}</span>
|
||||
</div>
|
||||
<div id="test-install-result" class="mt-3 hidden">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ t('protocols.test_result') }}</h3>
|
||||
<pre id="test-install-output" class="mt-2 p-3 bg-gray-50 border border-gray-200 rounded text-xs whitespace-pre-wrap"></pre>
|
||||
<h3 class="mt-3 text-sm font-medium text-gray-900">{{ t('protocols.client_output_preview') }}</h3>
|
||||
<pre id="test-client-preview" class="mt-2 p-3 bg-gray-50 border border-gray-200 rounded text-xs whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Uninstallation Script -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.uninstallation_script') }}</h2>
|
||||
<button type="button" class="ai-help-btn inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" data-target="uninstall">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
{{ t('ai.get_ai_help') }}
|
||||
</button>
|
||||
</div>
|
||||
<div>
|
||||
<textarea id="uninstall_script" name="uninstall_script" rows="12" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="#!/bin/bash # Uninstallation script here">{{ editing.uninstall_script ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.uninstall_script_help') }}</p>
|
||||
<div class="mt-3 flex items-center space-x-2">
|
||||
<button id="test-uninstall-btn" type="button" class="inline-flex items-center px-3 py-2 bg-red-600 text-white rounded hover:bg-red-700">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/></svg>
|
||||
{{ t('protocols.test_uninstall') }}
|
||||
</button>
|
||||
<span class="text-xs text-gray-500">{{ t('protocols.testing_on_ubuntu22') }}</span>
|
||||
</div>
|
||||
<div id="test-uninstall-result" class="mt-3 hidden">
|
||||
<h3 class="text-sm font-medium text-gray-900">{{ t('protocols.test_result') }}</h3>
|
||||
<pre id="test-uninstall-output" class="mt-2 p-3 bg-gray-50 border border-gray-200 rounded text-xs whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Output Template -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.output_template') }}</h2>
|
||||
<button type="button" class="ai-help-btn inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" data-target="template">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
{{ t('ai.get_ai_help') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea id="output_template" name="output_template" rows="10" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="[Interface] PrivateKey = {{private_key}} Address = {{client_ip}}/32">{{ editing.output_template ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.output_template_help') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-blue-50 rounded-md">
|
||||
<h3 class="text-sm font-medium text-blue-900 mb-2">{{ t('protocols.available_variables') }}</h3>
|
||||
<div class="text-sm text-blue-800 space-y-1">
|
||||
<p><code>{{private_key}}</code> - {{ t('protocols.variable_private_key_help') }}</p>
|
||||
<p><code>{{public_key}}</code> - {{ t('protocols.variable_public_key_help') }}</p>
|
||||
<p><code>{{client_ip}}</code> - {{ t('protocols.variable_client_ip_help') }}</p>
|
||||
<p><code>{{server_host}}</code> - {{ t('protocols.variable_server_host_help') }}</p>
|
||||
<p><code>{{server_port}}</code> - {{ t('protocols.variable_server_port_help') }}</p>
|
||||
<p><code>{{preshared_key}}</code> - {{ t('protocols.variable_preshared_key_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- QR Code Template -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="qr_section_toggle" class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded mr-3" checked>
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.qr_code_template') }}</h2>
|
||||
</div>
|
||||
<button type="button" class="ai-help-btn inline-flex items-center px-3 py-1 border border-purple-300 rounded-md text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500" data-target="qr_template">
|
||||
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
{{ t('ai.get_ai_help') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="qr_section_content">
|
||||
<div class="mb-4">
|
||||
<label for="qr_code_format" class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.qr_code_format') }}</label>
|
||||
<select id="qr_code_format" name="qr_code_format" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="amnezia_compressed" {% if editing.qr_code_format == 'amnezia_compressed' %}selected{% endif %}>Amnezia Compressed (Default)</option>
|
||||
<option value="raw" {% if editing.qr_code_format == 'raw' %}selected{% endif %}>Raw Content</option>
|
||||
<option value="text" {% if editing.qr_code_format == 'text' %}selected{% endif %}>{{ t('protocols.qr_code_format_text') }}</option>
|
||||
</select>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.qr_code_format_help') }}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<textarea id="qr_code_template" name="qr_code_template" rows="10" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="{"last_config":{{last_config_json}}}">{{ editing.qr_code_template ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.qr_code_template_help') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 p-4 bg-blue-50 rounded-md">
|
||||
<h3 class="text-sm font-medium text-blue-900 mb-2">{{ t('protocols.available_variables') }}</h3>
|
||||
<div class="text-sm text-blue-800 space-y-1">
|
||||
<p><code>{{last_config_json}}</code> - {{ t('protocols.variable_last_config_json_help') }}</p>
|
||||
<p>{{ t('protocols.plus_all_output_variables') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Password Generation -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">{{ t('protocols.password_generation') }}</h2>
|
||||
<div>
|
||||
<textarea id="password_command" name="password_command" rows="6" class="w-full px-3 py-2 border rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="echo \$(openssl rand -base64 12)">{{ editing.password_command ?? '' }}</textarea>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.password_command_help') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Settings -->
|
||||
<div class="bg-white shadow rounded-lg p-6">
|
||||
<h2 class="text-lg font-medium text-gray-900 mb-4">{{ t('common.settings') }}</h2>
|
||||
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="ubuntu_compatible" name="ubuntu_compatible" value="1" {% if editing.ubuntu_compatible %}checked{% endif %} class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<label for="ubuntu_compatible" class="ml-2 block text-sm text-gray-900">{{ t('protocols.ubuntu_compatible') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="show_text_content" name="show_text_content" value="1" {% if editing.show_text_content %}checked{% endif %} class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<label for="show_text_content" class="ml-2 block text-sm text-gray-900">{{ t('protocols.show_text_content') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="is_active" name="is_active" value="1" {% if editing.is_active is not defined or editing.is_active %}checked{% endif %} class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded">
|
||||
<label for="is_active" class="ml-2 block text-sm text-gray-900">{{ t('protocols.active_label') }}</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Form Actions -->
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="/settings/protocols" class="px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{{ t('common.cancel') }}
|
||||
</a>
|
||||
<button type="submit" class="px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{{ editing ? t('protocols.update_protocol') : t('protocols.create_protocol') }}
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Assistant Modal -->
|
||||
<div id="ai-assistant-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">{{ t('ai.assistant') }}</h3>
|
||||
<button id="close-ai-modal" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.select_model') }}</label>
|
||||
<select id="ai-model-select" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="openai/gpt-3.5-turbo">{{ t('ai.model_gpt35_turbo') }}</option>
|
||||
<option value="openai/gpt-4">{{ t('ai.model_gpt4') }}</option>
|
||||
<option value="anthropic/claude-3-haiku">{{ t('ai.model_claude3_haiku') }}</option>
|
||||
<option value="anthropic/claude-3-sonnet">{{ t('ai.model_claude3_sonnet') }}</option>
|
||||
</select>
|
||||
<div class="mt-2 flex items-center space-x-2">
|
||||
<input id="ai-model-custom" type="text" placeholder="{{ t('ai.custom_model_placeholder') }}" class="flex-1 px-3 py-2 border border-gray-300 rounded-md">
|
||||
<button id="ai-model-test-btn" type="button" class="px-3 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50">{{ t('ai.check_availability') }}</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<a href="https://openrouter.ai/models" target="_blank" class="text-purple-600">openrouter.ai/models</a>
|
||||
</p>
|
||||
{% if not openrouter_key %}
|
||||
<div class="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-yellow-800">{{ t('settings.no_api_key') }}</span>
|
||||
<a href="/settings#api" class="px-2 py-1 text-xs bg-purple-600 text-white rounded">{{ t('settings.enter_api_key') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.protocol_type') }}</label>
|
||||
<select id="ai-protocol-type" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">{{ t('ai.general_vpn') }}</option>
|
||||
<option value="wireguard">WireGuard</option>
|
||||
<option value="openvpn">OpenVPN</option>
|
||||
<option value="shadowsocks">Shadowsocks</option>
|
||||
<option value="cloak">Cloak</option>
|
||||
<option value="ikev2">IKEv2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.describe_requirements') }}</label>
|
||||
<textarea id="ai-prompt" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="{{ t('ai.prompt_placeholder') }}"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button id="generate-script-btn" class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
{{ t('ai.generate_script') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="ai-loading" class="hidden text-center py-4">
|
||||
<div class="inline-flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-purple-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>{{ t('ai.generating_script') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ai-result" class="hidden">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.generated_script') }}</label>
|
||||
<div class="bg-gray-900 text-green-400 p-4 rounded-md overflow-x-auto">
|
||||
<pre id="generated-script" class="text-sm whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ai-suggestions" class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.suggestions') }}</label>
|
||||
<ul id="suggestions-list" class="list-disc list-inside space-y-1 text-sm text-gray-600"></ul>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('common.compatibility') }}</label>
|
||||
<div id="ubuntu-compatibility" class="flex items-center"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button id="apply-to-current-btn" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||
{{ t('ai.apply_to_current_protocol') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// AI Assistant Modal
|
||||
const aiModal = document.getElementById('ai-assistant-modal');
|
||||
const aiHelpBtns = document.querySelectorAll('.ai-help-btn');
|
||||
const closeAIModal = document.getElementById('close-ai-modal');
|
||||
const generateScriptBtn = document.getElementById('generate-script-btn');
|
||||
const aiLoading = document.getElementById('ai-loading');
|
||||
const aiResult = document.getElementById('ai-result');
|
||||
const applyToCurrentBtn = document.getElementById('apply-to-current-btn');
|
||||
const installScriptTextarea = document.getElementById('install_script');
|
||||
const uninstallScriptTextarea = document.getElementById('uninstall_script');
|
||||
const outputTemplateTextarea = document.getElementById('output_template');
|
||||
|
||||
// QR Template Section Toggle
|
||||
const qrSectionToggle = document.getElementById('qr_section_toggle');
|
||||
const qrSectionContent = document.getElementById('qr_section_content');
|
||||
|
||||
if (qrSectionToggle && qrSectionContent) {
|
||||
qrSectionToggle.addEventListener('change', function() {
|
||||
qrSectionContent.style.display = this.checked ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
let currentAiTarget = 'install'; // install, uninstall, template
|
||||
|
||||
function showAIModal(target) {
|
||||
currentAiTarget = target || 'install';
|
||||
aiModal.classList.remove('hidden');
|
||||
aiResult.classList.add('hidden');
|
||||
aiLoading.classList.add('hidden');
|
||||
|
||||
// Update modal title or prompt placeholder based on target if needed
|
||||
const promptArea = document.getElementById('ai-prompt');
|
||||
if (currentAiTarget === 'template') {
|
||||
promptArea.placeholder = "{{ t('ai.prompt_placeholder_template') }}";
|
||||
} else if (currentAiTarget === 'qr_template') {
|
||||
promptArea.placeholder = "{{ t('ai.prompt_placeholder_qr_template') }}";
|
||||
} else if (currentAiTarget === 'uninstall') {
|
||||
promptArea.placeholder = "{{ t('ai.prompt_placeholder_uninstall') }}";
|
||||
} else {
|
||||
promptArea.placeholder = "{{ t('ai.prompt_placeholder') }}";
|
||||
}
|
||||
}
|
||||
|
||||
function hideAIModal() {
|
||||
aiModal.classList.add('hidden');
|
||||
}
|
||||
|
||||
// Attach event listeners to all AI help buttons
|
||||
aiHelpBtns.forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const target = this.getAttribute('data-target') || 'install';
|
||||
showAIModal(target);
|
||||
});
|
||||
});
|
||||
|
||||
// Also attach to the original ID if it exists (for backward compatibility or if I missed updating one)
|
||||
const originalAiBtn = document.getElementById('ai-help-btn');
|
||||
if (originalAiBtn) {
|
||||
originalAiBtn.addEventListener('click', function() {
|
||||
showAIModal('install');
|
||||
});
|
||||
}
|
||||
|
||||
closeAIModal.addEventListener('click', hideAIModal);
|
||||
|
||||
// Close modal when clicking outside
|
||||
aiModal.addEventListener('click', function(e) {
|
||||
if (e.target === aiModal) {
|
||||
hideAIModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Generate script with AI
|
||||
generateScriptBtn.addEventListener('click', async function() {
|
||||
const model = document.getElementById('ai-model-select').value;
|
||||
const customModel = document.getElementById('ai-model-custom').value.trim();
|
||||
const effectiveModel = customModel !== '' ? customModel : model;
|
||||
const protocolType = document.getElementById('ai-protocol-type').value;
|
||||
const prompt = document.getElementById('ai-prompt').value;
|
||||
|
||||
if (!prompt.trim()) {
|
||||
alert('{{ t('ai.please_enter_requirements') }}');
|
||||
return;
|
||||
}
|
||||
|
||||
aiLoading.classList.remove('hidden');
|
||||
generateScriptBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/ai/assist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: prompt,
|
||||
model: effectiveModel,
|
||||
protocol_type: protocolType,
|
||||
target: currentAiTarget
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
displayAIResult(result.data);
|
||||
} else {
|
||||
alert('{{ t('ai.error_generating_script') }}: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('{{ t('ai.error_generating_script') }}: ' + error.message);
|
||||
} finally {
|
||||
aiLoading.classList.add('hidden');
|
||||
generateScriptBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function displayAIResult(data) {
|
||||
document.getElementById('generated-script').textContent = data.script;
|
||||
|
||||
const suggestionsList = document.getElementById('suggestions-list');
|
||||
suggestionsList.innerHTML = '';
|
||||
data.suggestions.forEach(suggestion => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = suggestion;
|
||||
suggestionsList.appendChild(li);
|
||||
});
|
||||
|
||||
const compatibilityDiv = document.getElementById('ubuntu-compatibility');
|
||||
if (data.ubuntu_compatible) {
|
||||
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg><span class="text-green-700">Compatible with Ubuntu 22.04-24.04</span>';
|
||||
} else {
|
||||
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg><span class="text-red-700">May not be compatible with Ubuntu 22.04-24.04</span>';
|
||||
}
|
||||
|
||||
aiResult.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Apply to current protocol
|
||||
applyToCurrentBtn.addEventListener('click', function() {
|
||||
const generatedScript = document.getElementById('generated-script').textContent;
|
||||
if (generatedScript && confirm('{{ t('ai.confirm_apply_script') }}')) {
|
||||
if (currentAiTarget === 'uninstall') {
|
||||
uninstallScriptTextarea.value = generatedScript;
|
||||
} else if (currentAiTarget === 'template') {
|
||||
outputTemplateTextarea.value = generatedScript;
|
||||
} else if (currentAiTarget === 'qr_template') {
|
||||
document.getElementById('qr_code_template').value = generatedScript;
|
||||
} else {
|
||||
installScriptTextarea.value = generatedScript;
|
||||
}
|
||||
hideAIModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Form validation
|
||||
document.getElementById('protocol-form').addEventListener('submit', function(e) {
|
||||
const name = document.getElementById('name').value.trim();
|
||||
const slug = document.getElementById('slug').value.trim();
|
||||
|
||||
if (!name || !slug) {
|
||||
e.preventDefault();
|
||||
alert('{{ t('protocols.please_fill_required_fields') }}');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!/^[a-z0-9_-]+$/i.test(slug)) {
|
||||
e.preventDefault();
|
||||
alert('{{ t('protocols.invalid_slug_format') }}');
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
// Auto-generate slug from name
|
||||
document.getElementById('name').addEventListener('blur', function() {
|
||||
const name = this.value.trim();
|
||||
const slugField = document.getElementById('slug');
|
||||
|
||||
if (name && !slugField.value.trim()) {
|
||||
slugField.value = name.toLowerCase()
|
||||
.replace(/[^a-z0-9\s-]/g, '')
|
||||
.replace(/\s+/g, '-')
|
||||
.replace(/-+/g, '-')
|
||||
.replace(/^-|-$/g, '');
|
||||
}
|
||||
});
|
||||
|
||||
// Test Install Script
|
||||
const testBtn = document.getElementById('test-install-btn');
|
||||
const testBox = document.getElementById('test-install-result');
|
||||
const testOut = document.getElementById('test-install-output');
|
||||
const clientPrev = document.getElementById('test-client-preview');
|
||||
|
||||
if (testBtn) {
|
||||
testBtn.addEventListener('click', function() {
|
||||
const protocolId = {{ editing ? editing.id : 'null' }};
|
||||
if (!protocolId) {
|
||||
return;
|
||||
}
|
||||
testBtn.disabled = true;
|
||||
testBtn.classList.add('opacity-50');
|
||||
testOut.textContent = '';
|
||||
clientPrev.textContent = '';
|
||||
testBox.classList.remove('hidden');
|
||||
|
||||
const appendCmd = (cmd) => {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'text-xs text-gray-800';
|
||||
line.innerHTML = `<span class="text-blue-600">$</span> <code>${cmd}</code>`;
|
||||
testOut.appendChild(line);
|
||||
};
|
||||
const appendOut = (text) => {
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'mt-1 p-2 bg-gray-100 border border-gray-200 rounded text-xs whitespace-pre-wrap';
|
||||
pre.textContent = text;
|
||||
testOut.appendChild(pre);
|
||||
};
|
||||
const setError = (msg) => {
|
||||
const err = document.createElement('div');
|
||||
err.className = 'mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700';
|
||||
err.textContent = msg;
|
||||
testOut.appendChild(err);
|
||||
};
|
||||
|
||||
let es;
|
||||
try {
|
||||
es = new EventSource(`/api/protocols/${protocolId}/test-install/stream`);
|
||||
} catch (e) {
|
||||
es = null;
|
||||
}
|
||||
|
||||
if (es) {
|
||||
es.onmessage = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'start') {
|
||||
appendOut('{{ t('protocols.testing_on_ubuntu22') }}');
|
||||
} else if (data.type === 'cmd') {
|
||||
appendCmd(data.cmd);
|
||||
} else if (data.type === 'out') {
|
||||
appendOut(data.line);
|
||||
} else if (data.type === 'cmd_done') {
|
||||
if (data.rc !== 0) {
|
||||
setError('Command failed');
|
||||
}
|
||||
} else if (data.type === 'preview') {
|
||||
clientPrev.textContent = data.preview || '';
|
||||
} else if (data.type === 'done') {
|
||||
es.close();
|
||||
testBtn.disabled = false;
|
||||
testBtn.classList.remove('opacity-50');
|
||||
} else if (data.type === 'error') {
|
||||
setError(data.error || 'Unknown error');
|
||||
es.close();
|
||||
testBtn.disabled = false;
|
||||
testBtn.classList.remove('opacity-50');
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
testBtn.disabled = false;
|
||||
testBtn.classList.remove('opacity-50');
|
||||
setError('Connection failed');
|
||||
};
|
||||
} else {
|
||||
// Fallback to non-stream if needed, but we implemented stream
|
||||
testBtn.disabled = false;
|
||||
testBtn.classList.remove('opacity-50');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Test Uninstall Script
|
||||
const testUninstallBtn = document.getElementById('test-uninstall-btn');
|
||||
const testUninstallBox = document.getElementById('test-uninstall-result');
|
||||
const testUninstallOut = document.getElementById('test-uninstall-output');
|
||||
|
||||
if (testUninstallBtn) {
|
||||
testUninstallBtn.addEventListener('click', function() {
|
||||
const protocolId = {{ editing ? editing.id : 'null' }};
|
||||
if (!protocolId) {
|
||||
return;
|
||||
}
|
||||
testUninstallBtn.disabled = true;
|
||||
testUninstallBtn.classList.add('opacity-50');
|
||||
testUninstallOut.textContent = '';
|
||||
testUninstallBox.classList.remove('hidden');
|
||||
|
||||
const appendCmd = (cmd) => {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'text-xs text-gray-800';
|
||||
line.innerHTML = `<span class="text-blue-600">$</span> <code>${cmd}</code>`;
|
||||
testUninstallOut.appendChild(line);
|
||||
};
|
||||
const appendOut = (text) => {
|
||||
const pre = document.createElement('pre');
|
||||
pre.className = 'mt-1 p-2 bg-gray-100 border border-gray-200 rounded text-xs whitespace-pre-wrap';
|
||||
pre.textContent = text;
|
||||
testUninstallOut.appendChild(pre);
|
||||
};
|
||||
const setError = (msg) => {
|
||||
const err = document.createElement('div');
|
||||
err.className = 'mt-2 p-2 bg-red-50 border border-red-200 rounded text-xs text-red-700';
|
||||
err.textContent = msg;
|
||||
testUninstallOut.appendChild(err);
|
||||
};
|
||||
|
||||
let es;
|
||||
try {
|
||||
es = new EventSource(`/api/protocols/${protocolId}/test-uninstall/stream`);
|
||||
} catch (e) {
|
||||
es = null;
|
||||
}
|
||||
|
||||
if (es) {
|
||||
es.onmessage = (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.type === 'start') {
|
||||
appendOut('{{ t('protocols.testing_on_ubuntu22') }}');
|
||||
} else if (data.type === 'cmd') {
|
||||
appendCmd(data.cmd);
|
||||
} else if (data.type === 'out') {
|
||||
appendOut(data.line);
|
||||
} else if (data.type === 'cmd_done') {
|
||||
if (data.rc !== 0) {
|
||||
setError('Command failed');
|
||||
}
|
||||
} else if (data.type === 'done') {
|
||||
es.close();
|
||||
testUninstallBtn.disabled = false;
|
||||
testUninstallBtn.classList.remove('opacity-50');
|
||||
} else if (data.type === 'error') {
|
||||
setError(data.error || 'Unknown error');
|
||||
es.close();
|
||||
testUninstallBtn.disabled = false;
|
||||
testUninstallBtn.classList.remove('opacity-50');
|
||||
}
|
||||
} catch (_) {}
|
||||
};
|
||||
es.onerror = () => {
|
||||
es.close();
|
||||
testUninstallBtn.disabled = false;
|
||||
testUninstallBtn.classList.remove('opacity-50');
|
||||
setError('Connection failed');
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,272 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block title %}{{ t('protocols.template_editor') }} - {{ parent() }}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="min-h-screen bg-gray-50">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">{{ t('protocols.template_editor') }}</h1>
|
||||
<p class="mt-2 text-gray-600">{{ t('protocols.template_editor_description') }}</p>
|
||||
</div>
|
||||
<a href="/settings/protocols" class="inline-flex items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
|
||||
</svg>
|
||||
{{ t('protocols.back_to_protocols') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Editor -->
|
||||
<div class="bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ protocol.name }} - {{ t('protocols.output_template') }}</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">{{ t('protocols.template_editor_help') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<!-- Template Editor -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('protocols.template_content') }}</label>
|
||||
<div class="flex space-x-2">
|
||||
<button id="format-template" class="px-3 py-1 text-xs bg-gray-100 text-gray-700 rounded hover:bg-gray-200">{{ t('common.format') }}</button>
|
||||
<button id="clear-template" class="px-3 py-1 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200">{{ t('common.clear') }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<textarea id="template-editor" rows="20" class="w-full px-3 py-2 border border-gray-300 rounded-md font-mono text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">{{ protocol.output_template }}</textarea>
|
||||
|
||||
<div class="mt-3 flex space-x-2">
|
||||
<button id="save-template" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"/>
|
||||
</svg>
|
||||
{{ t('protocols.save_template') }}
|
||||
</button>
|
||||
<button id="preview-template" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-gray-300 rounded-md shadow-sm text-sm font-medium text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
||||
</svg>
|
||||
{{ t('common.preview') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Preview Panel -->
|
||||
<div>
|
||||
<div class="flex justify-between items-center mb-3">
|
||||
<label class="block text-sm font-medium text-gray-700">{{ t('common.preview') }}</label>
|
||||
<button id="refresh-preview" class="px-3 py-1 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200">{{ t('common.refresh') }}</button>
|
||||
</div>
|
||||
<div class="bg-gray-900 text-green-400 p-4 rounded-md h-96 overflow-auto">
|
||||
<pre id="template-preview" class="text-sm whitespace-pre-wrap">{{ t('protocols.click_preview_to_see_output') }}</pre>
|
||||
</div>
|
||||
|
||||
<div class="mt-3">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.test_variables') }}</label>
|
||||
<div class="space-y-2">
|
||||
<input type="text" id="test-private-key" placeholder="{{ t('protocols.private_key') }}" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" value="test_private_key_example_1234567890abcdef">
|
||||
<input type="text" id="test-client-ip" placeholder="{{ t('protocols.client_ip') }}" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" value="10.8.1.2">
|
||||
<input type="text" id="test-server-host" placeholder="{{ t('protocols.server_host') }}" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" value="vpn.example.com">
|
||||
<input type="text" id="test-server-port" placeholder="{{ t('protocols.server_port') }}" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" value="51820">
|
||||
<input type="text" id="test-preshared-key" placeholder="{{ t('protocols.preshared_key') }}" class="w-full px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500" value="test_preshared_key_example">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Template Variables -->
|
||||
<div class="mt-6 bg-white shadow rounded-lg">
|
||||
<div class="px-6 py-4 border-b border-gray-200">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.template_variables') }}</h2>
|
||||
<p class="mt-1 text-sm text-gray-600">{{ t('protocols.template_variables_help') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="p-6">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div class="variable-card p-3 border border-gray-200 rounded-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{private_key}}</code>
|
||||
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{private_key}}">{{ t('common.copy') }}</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_private_key_desc') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="variable-card p-3 border border-gray-200 rounded-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{public_key}}</code>
|
||||
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{public_key}}">{{ t('common.copy') }}</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_public_key_desc') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="variable-card p-3 border border-gray-200 rounded-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{client_ip}}</code>
|
||||
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{client_ip}}">{{ t('common.copy') }}</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_client_ip_desc') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="variable-card p-3 border border-gray-200 rounded-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{server_host}}</code>
|
||||
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{server_host}}">{{ t('common.copy') }}</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_server_host_desc') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="variable-card p-3 border border-gray-200 rounded-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{server_port}}</code>
|
||||
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{server_port}}">{{ t('common.copy') }}</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_server_port_desc') }}</p>
|
||||
</div>
|
||||
|
||||
<div class="variable-card p-3 border border-gray-200 rounded-md">
|
||||
<div class="flex justify-between items-center">
|
||||
<code class="text-sm bg-gray-100 px-2 py-1 rounded">{{preshared_key}}</code>
|
||||
<button class="copy-variable text-xs text-blue-600 hover:text-blue-800" data-variable="{{preshared_key}}">{{ t('common.copy') }}</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-600">{{ t('protocols.variable_preshared_key_desc') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const templateEditor = document.getElementById('template-editor');
|
||||
const templatePreview = document.getElementById('template-preview');
|
||||
const saveTemplateBtn = document.getElementById('save-template');
|
||||
const previewBtn = document.getElementById('preview-template');
|
||||
const refreshBtn = document.getElementById('refresh-preview');
|
||||
const formatBtn = document.getElementById('format-template');
|
||||
const clearBtn = document.getElementById('clear-template');
|
||||
|
||||
const testPrivateKey = document.getElementById('test-private-key');
|
||||
const testClientIp = document.getElementById('test-client-ip');
|
||||
const testServerHost = document.getElementById('test-server-host');
|
||||
const testServerPort = document.getElementById('test-server-port');
|
||||
const testPresharedKey = document.getElementById('test-preshared-key');
|
||||
|
||||
// Preview template
|
||||
function previewTemplate() {
|
||||
let template = templateEditor.value;
|
||||
|
||||
// Replace variables with test values
|
||||
template = template.replace(/\{\{private_key\}\}/g, testPrivateKey.value);
|
||||
template = template.replace(/\{\{public_key\}\}/g, 'test_public_key_example');
|
||||
template = template.replace(/\{\{client_ip\}\}/g, testClientIp.value);
|
||||
template = template.replace(/\{\{server_host\}\}/g, testServerHost.value);
|
||||
template = template.replace(/\{\{server_port\}\}/g, testServerPort.value);
|
||||
template = template.replace(/\{\{preshared_key\}\}/g, testPresharedKey.value);
|
||||
|
||||
templatePreview.textContent = template;
|
||||
}
|
||||
|
||||
previewBtn.addEventListener('click', previewTemplate);
|
||||
refreshBtn.addEventListener('click', previewTemplate);
|
||||
|
||||
// Auto-preview on input change
|
||||
[testPrivateKey, testClientIp, testServerHost, testServerPort, testPresharedKey].forEach(input => {
|
||||
input.addEventListener('input', function() {
|
||||
if (templatePreview.textContent !== '{{ t('protocols.click_preview_to_see_output') }}') {
|
||||
previewTemplate();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Save template
|
||||
saveTemplateBtn.addEventListener('click', function() {
|
||||
const protocolId = {{ protocol.id }};
|
||||
const template = templateEditor.value;
|
||||
|
||||
fetch(`/api/protocols/${protocolId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
output_template: template
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert('{{ t('protocols.template_saved_successfully') }}');
|
||||
} else {
|
||||
alert('{{ t('protocols.error_saving_template') }}: ' + result.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('{{ t('protocols.error_saving_template') }}: ' + error.message);
|
||||
});
|
||||
});
|
||||
|
||||
// Format template (basic formatting)
|
||||
formatBtn.addEventListener('click', function() {
|
||||
let template = templateEditor.value;
|
||||
|
||||
// Basic formatting for WireGuard configs
|
||||
if (template.includes('[Interface]') || template.includes('[Peer]')) {
|
||||
template = template.replace(/\n\s*/g, '\n');
|
||||
template = template.replace(/\[/g, '\n[');
|
||||
template = template.trim();
|
||||
}
|
||||
|
||||
templateEditor.value = template;
|
||||
alert('{{ t('protocols.template_formatted') }}');
|
||||
});
|
||||
|
||||
// Clear template
|
||||
clearBtn.addEventListener('click', function() {
|
||||
if (confirm('{{ t('protocols.confirm_clear_template') }}')) {
|
||||
templateEditor.value = '';
|
||||
templatePreview.textContent = '{{ t('protocols.click_preview_to_see_output') }}';
|
||||
}
|
||||
});
|
||||
|
||||
// Copy variables
|
||||
document.querySelectorAll('.copy-variable').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const variable = this.dataset.variable;
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = variable;
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
document.execCommand('copy');
|
||||
document.body.removeChild(textarea);
|
||||
|
||||
// Show feedback
|
||||
const originalText = this.textContent;
|
||||
this.textContent = '{{ t('common.copied') }}';
|
||||
setTimeout(() => {
|
||||
this.textContent = originalText;
|
||||
}, 1000);
|
||||
});
|
||||
});
|
||||
|
||||
// Auto-save functionality (optional)
|
||||
let autoSaveTimeout;
|
||||
templateEditor.addEventListener('input', function() {
|
||||
clearTimeout(autoSaveTimeout);
|
||||
autoSaveTimeout = setTimeout(function() {
|
||||
// Could implement auto-save here
|
||||
console.log('Template changed, could auto-save...');
|
||||
}, 2000);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,522 @@
|
||||
<div class="max-w-6xl mx-auto px-1 py-2">
|
||||
<!-- Header -->
|
||||
<div class="mb-8">
|
||||
<div class="flex justify-between items-center">
|
||||
<div>
|
||||
<h1 class="text-3xl font-bold text-gray-900">{{ t('protocols.management') }}</h1>
|
||||
<p class="mt-2 text-gray-600">{{ t('protocols.management_description') }}</p>
|
||||
</div>
|
||||
<div class="flex space-x-3">
|
||||
<button id="ai-assistant-btn" class="inline-flex items-center px-4 py-2 border border-purple-300 rounded-md shadow-sm text-sm font-medium text-purple-700 bg-white hover:bg-purple-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
{{ t('ai.assistant') }}
|
||||
</button>
|
||||
<a href="/settings/protocols/new" class="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
{{ t('protocols.add_protocol') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Success/Error Messages -->
|
||||
{% if success %}
|
||||
<div class="mb-4 bg-green-50 border border-green-200 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<svg class="w-5 h-5 text-green-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<p class="text-green-800">{{ success }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if error %}
|
||||
<div class="mb-4 bg-red-50 border border-red-200 rounded-md p-4">
|
||||
<div class="flex">
|
||||
<svg class="w-5 h-5 text-red-400 mr-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
|
||||
</svg>
|
||||
<p class="text-red-800">{{ error }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Protocols Grid -->
|
||||
<div class="bg-white shadow overflow-hidden sm:rounded-md">
|
||||
<div class="px-4 py-5 sm:p-6">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-lg font-medium text-gray-900">{{ t('protocols.available_protocols') }}</h2>
|
||||
<div class="flex space-x-2">
|
||||
<input type="text" id="protocol-search" placeholder="{{ t('protocols.search_protocols') }}" class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<select id="protocol-filter" class="px-3 py-2 border border-gray-300 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">{{ t('protocols.all_protocols') }}</option>
|
||||
<option value="active">{{ t('protocols.active_only') }}</option>
|
||||
<option value="ubuntu">{{ t('protocols.ubuntu_compatible') }}</option>
|
||||
<option value="with-ai">{{ t('protocols.with_ai_generations') }}</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="protocols-list" class="space-y-4">
|
||||
{% for protocol in protocols %}
|
||||
<div class="protocol-card border border-gray-200 rounded-lg p-4 hover:shadow-md transition-shadow" data-protocol-id="{{ protocol.id }}" data-protocol-name="{{ protocol.name }}" data-protocol-slug="{{ protocol.slug }}" data-active="{{ protocol.is_active }}" data-ubuntu="{{ protocol.ubuntu_compatible }}" data-ai-generations="{{ protocol.ai_generation_count }}">
|
||||
<div class="flex justify-between items-start">
|
||||
<div class="flex-1">
|
||||
<div class="flex items-center space-x-3">
|
||||
<h3 class="text-lg font-semibold text-gray-900">{{ protocol.name }}</h3>
|
||||
{% if protocol.is_active %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
{{ t('common.active') }}
|
||||
</span>
|
||||
{% else %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-800">
|
||||
{{ t('common.inactive') }}
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if protocol.ubuntu_compatible %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-blue-100 text-blue-800">
|
||||
Ubuntu 22-24
|
||||
</span>
|
||||
{% endif %}
|
||||
{% if protocol.ai_generation_count > 0 %}
|
||||
<span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 text-purple-800">
|
||||
AI {{ protocol.ai_generation_count }}
|
||||
</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<p class="mt-1 text-sm text-gray-600">{{ protocol.description }}</p>
|
||||
<div class="mt-2 flex items-center space-x-4 text-xs text-gray-500">
|
||||
<span>{{ t('common.slug') }}: <code class="bg-gray-100 px-1 rounded">{{ protocol.slug }}</code></span>
|
||||
<span>{{ t('common.servers') }}: {{ protocol.server_count }}</span>
|
||||
<span>{{ t('common.templates') }}: {{ protocol.template_count }}</span>
|
||||
<span>{{ t('common.variables') }}: {{ protocol.variable_count }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex space-x-2">
|
||||
<button class="ai-generate-btn text-purple-600 hover:text-purple-900" data-protocol-id="{{ protocol.id }}" title="{{ t('ai.generate_with_ai') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<a href="/settings/protocols/{{ protocol.id }}/edit" class="text-blue-600 hover:text-blue-900" title="{{ t('common.edit') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
||||
</svg>
|
||||
</a>
|
||||
<a href="/settings/protocols/{{ protocol.id }}/template" class="text-green-600 hover:text-green-900" title="{{ t('protocols.edit_template') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4"/>
|
||||
</svg>
|
||||
</a>
|
||||
{% if protocol.server_count == 0 %}
|
||||
<button class="delete-protocol-btn text-red-600 hover:text-red-900" data-protocol-id="{{ protocol.id }}" data-protocol-name="{{ protocol.name }}" title="{{ t('common.delete') }}">
|
||||
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
||||
</svg>
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center py-8">
|
||||
<svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/>
|
||||
</svg>
|
||||
<h3 class="mt-2 text-sm font-medium text-gray-900">{{ t('protocols.no_protocols') }}</h3>
|
||||
<p class="mt-1 text-sm text-gray-500">{{ t('protocols.no_protocols_description') }}</p>
|
||||
<div class="mt-6">
|
||||
<a href="/settings/protocols/new" class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
|
||||
</svg>
|
||||
{{ t('protocols.create_first_protocol') }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- AI Assistant Modal -->
|
||||
<div id="ai-assistant-modal" class="fixed inset-0 bg-gray-600 bg-opacity-50 hidden overflow-y-auto h-full w-full z-50">
|
||||
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h3 class="text-lg font-semibold text-gray-900">{{ t('ai.assistant') }}</h3>
|
||||
<button id="close-ai-modal" class="text-gray-400 hover:text-gray-600">
|
||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.select_model') }}</label>
|
||||
<select id="ai-model-select" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="openai/gpt-3.5-turbo">{{ t('ai.model_gpt35_turbo') }}</option>
|
||||
<option value="openai/gpt-4">{{ t('ai.model_gpt4') }}</option>
|
||||
<option value="anthropic/claude-3-haiku">{{ t('ai.model_claude3_haiku') }}</option>
|
||||
<option value="anthropic/claude-3-sonnet">{{ t('ai.model_claude3_sonnet') }}</option>
|
||||
</select>
|
||||
<div class="mt-2 flex items-center space-x-2">
|
||||
<input id="ai-model-custom" type="text" placeholder="{{ t('ai.custom_model_placeholder') }}" class="flex-1 px-3 py-2 border border-gray-300 rounded-md">
|
||||
<button id="ai-model-test-btn" type="button" class="px-3 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50">{{ t('ai.check_availability') }}</button>
|
||||
</div>
|
||||
<p class="mt-2 text-xs text-gray-500">
|
||||
<i class="fas fa-external-link-alt"></i>
|
||||
<a href="https://openrouter.ai/models" target="_blank" class="text-purple-600">openrouter.ai/models</a>
|
||||
</p>
|
||||
{% if not openrouter_key %}
|
||||
<div class="mt-3 p-3 bg-yellow-50 border border-yellow-200 rounded">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="text-xs text-yellow-800">{{ t('settings.no_api_key') }}</span>
|
||||
<a href="/settings#api" class="px-2 py-1 text-xs bg-purple-600 text-white rounded">{{ t('settings.enter_api_key') }}</a>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.protocol_type') }}</label>
|
||||
<select id="ai-protocol-type" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500">
|
||||
<option value="">{{ t('ai.general_vpn') }}</option>
|
||||
<option value="wireguard">WireGuard</option>
|
||||
<option value="openvpn">OpenVPN</option>
|
||||
<option value="shadowsocks">Shadowsocks</option>
|
||||
<option value="cloak">Cloak</option>
|
||||
<option value="ikev2">IKEv2</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.describe_requirements') }}</label>
|
||||
<textarea id="ai-prompt" rows="4" class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" placeholder="{{ t('ai.prompt_placeholder') }}"></textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<button id="generate-script-btn" class="w-full inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
|
||||
</svg>
|
||||
{{ t('ai.generate_script') }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="ai-loading" class="hidden text-center py-4">
|
||||
<div class="inline-flex items-center">
|
||||
<svg class="animate-spin -ml-1 mr-3 h-5 w-5 text-purple-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
<span>{{ t('ai.generating_script') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ai-result" class="hidden">
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.generated_script') }}</label>
|
||||
<div class="bg-gray-900 text-green-400 p-4 rounded-md overflow-x-auto">
|
||||
<pre id="generated-script" class="text-sm whitespace-pre-wrap"></pre>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="ai-suggestions" class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('ai.suggestions') }}</label>
|
||||
<ul id="suggestions-list" class="list-disc list-inside space-y-1 text-sm text-gray-600"></ul>
|
||||
</div>
|
||||
<div class="mb-4 grid grid-cols-1 md:grid-cols-2 gap-3">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.name') }}</label>
|
||||
<input id="ai-protocol-name" type="text" placeholder="Protocol Name" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('protocols.slug') }}</label>
|
||||
<input id="ai-protocol-slug" type="text" placeholder="protocol-slug" class="w-full px-3 py-2 border border-gray-300 rounded-md">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">{{ t('common.compatibility') }}</label>
|
||||
<div id="ubuntu-compatibility" class="flex items-center"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex space-x-3">
|
||||
<button id="apply-to-protocol-btn" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
|
||||
{{ t('ai.apply_to_protocol') }}
|
||||
</button>
|
||||
<button id="create-new-protocol-btn" class="flex-1 inline-flex justify-center items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
|
||||
{{ t('ai.create_new_protocol') }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
// Protocol search and filter
|
||||
const searchInput = document.getElementById('protocol-search');
|
||||
const filterSelect = document.getElementById('protocol-filter');
|
||||
const protocolCards = document.querySelectorAll('.protocol-card');
|
||||
|
||||
function filterProtocols() {
|
||||
const searchTerm = searchInput.value.toLowerCase();
|
||||
const filterValue = filterSelect.value;
|
||||
|
||||
protocolCards.forEach(card => {
|
||||
const name = card.dataset.protocolName.toLowerCase();
|
||||
const slug = card.dataset.protocolSlug.toLowerCase();
|
||||
const isActive = card.dataset.active === '1';
|
||||
const isUbuntu = card.dataset.ubuntu === '1';
|
||||
const hasAI = parseInt(card.dataset.aiGenerations) > 0;
|
||||
|
||||
let show = true;
|
||||
|
||||
// Search filter
|
||||
if (searchTerm && !name.includes(searchTerm) && !slug.includes(searchTerm)) {
|
||||
show = false;
|
||||
}
|
||||
|
||||
// Filter by type
|
||||
if (filterValue === 'active' && !isActive) show = false;
|
||||
if (filterValue === 'ubuntu' && !isUbuntu) show = false;
|
||||
if (filterValue === 'with-ai' && !hasAI) show = false;
|
||||
|
||||
card.style.display = show ? 'block' : 'none';
|
||||
});
|
||||
}
|
||||
|
||||
searchInput.addEventListener('input', filterProtocols);
|
||||
filterSelect.addEventListener('change', filterProtocols);
|
||||
|
||||
// AI Assistant Modal
|
||||
const aiModal = document.getElementById('ai-assistant-modal');
|
||||
const aiAssistantBtn = document.getElementById('ai-assistant-btn');
|
||||
const closeAIModal = document.getElementById('close-ai-modal');
|
||||
const generateScriptBtn = document.getElementById('generate-script-btn');
|
||||
const aiLoading = document.getElementById('ai-loading');
|
||||
const aiResult = document.getElementById('ai-result');
|
||||
const applyToProtocolBtn = document.getElementById('apply-to-protocol-btn');
|
||||
const createNewProtocolBtn = document.getElementById('create-new-protocol-btn');
|
||||
|
||||
let currentGeneration = null;
|
||||
let currentProtocolId = null;
|
||||
|
||||
function showAIModal() {
|
||||
aiModal.classList.remove('hidden');
|
||||
aiResult.classList.add('hidden');
|
||||
aiLoading.classList.add('hidden');
|
||||
}
|
||||
|
||||
function hideAIModal() {
|
||||
aiModal.classList.add('hidden');
|
||||
currentGeneration = null;
|
||||
currentProtocolId = null;
|
||||
}
|
||||
|
||||
aiAssistantBtn.addEventListener('click', showAIModal);
|
||||
closeAIModal.addEventListener('click', hideAIModal);
|
||||
|
||||
// Close modal when clicking outside
|
||||
aiModal.addEventListener('click', function(e) {
|
||||
if (e.target === aiModal) {
|
||||
hideAIModal();
|
||||
}
|
||||
});
|
||||
|
||||
// Generate script with AI
|
||||
generateScriptBtn.addEventListener('click', async function() {
|
||||
const model = document.getElementById('ai-model-select').value;
|
||||
const customModel = document.getElementById('ai-model-custom').value.trim();
|
||||
const effectiveModel = customModel !== '' ? customModel : model;
|
||||
const protocolType = document.getElementById('ai-protocol-type').value;
|
||||
const prompt = document.getElementById('ai-prompt').value;
|
||||
|
||||
if (!prompt.trim()) {
|
||||
alert('{{ t('ai.please_enter_requirements') }}');
|
||||
return;
|
||||
}
|
||||
|
||||
aiLoading.classList.remove('hidden');
|
||||
generateScriptBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/ai/assist', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
prompt: prompt,
|
||||
model: effectiveModel,
|
||||
protocol_type: protocolType,
|
||||
protocol_id: currentProtocolId
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
currentGeneration = result.data;
|
||||
displayAIResult(result.data);
|
||||
} else {
|
||||
alert('{{ t('ai.error_generating_script') }}: ' + result.error);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('{{ t('ai.error_generating_script') }}: ' + error.message);
|
||||
} finally {
|
||||
aiLoading.classList.add('hidden');
|
||||
generateScriptBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
function displayAIResult(data) {
|
||||
document.getElementById('generated-script').textContent = data.script;
|
||||
|
||||
const suggestionsList = document.getElementById('suggestions-list');
|
||||
suggestionsList.innerHTML = '';
|
||||
data.suggestions.forEach(suggestion => {
|
||||
const li = document.createElement('li');
|
||||
li.textContent = suggestion;
|
||||
suggestionsList.appendChild(li);
|
||||
});
|
||||
|
||||
const compatibilityDiv = document.getElementById('ubuntu-compatibility');
|
||||
if (data.ubuntu_compatible) {
|
||||
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-green-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg><span class="text-green-700">Compatible with Ubuntu 22.04-24.04</span>';
|
||||
} else {
|
||||
compatibilityDiv.innerHTML = '<svg class="w-5 h-5 text-red-500 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg><span class="text-red-700">May not be compatible with Ubuntu 22.04-24.04</span>';
|
||||
}
|
||||
|
||||
aiResult.classList.remove('hidden');
|
||||
}
|
||||
|
||||
// Apply to existing protocol
|
||||
applyToProtocolBtn.addEventListener('click', function() {
|
||||
if (!currentGeneration) return;
|
||||
|
||||
const protocolId = prompt('{{ t('ai.enter_protocol_id_to_apply') }}:');
|
||||
if (!protocolId) return;
|
||||
|
||||
fetch(`/api/protocols/${protocolId}`, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
install_script: currentGeneration.script,
|
||||
ubuntu_compatible: currentGeneration.ubuntu_compatible
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert('{{ t('ai.script_applied_successfully') }}');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('{{ t('ai.error_applying_script') }}: ' + result.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('{{ t('ai.error_applying_script') }}: ' + error.message);
|
||||
});
|
||||
});
|
||||
|
||||
// Create new protocol with generated script
|
||||
createNewProtocolBtn.addEventListener('click', function() {
|
||||
if (!currentGeneration) return;
|
||||
const nameInput = document.getElementById('ai-protocol-name');
|
||||
const slugInput = document.getElementById('ai-protocol-slug');
|
||||
const name = (nameInput.value || '').trim();
|
||||
let slug = (slugInput.value || '').trim();
|
||||
if (!name) { alert('{{ t('protocols.enter_protocol_name') }}'); return; }
|
||||
if (!slug) { slug = name.toLowerCase().replace(/\s+/g, '-'); }
|
||||
|
||||
fetch('/api/protocols', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: name,
|
||||
slug: slug,
|
||||
install_script: currentGeneration.script,
|
||||
ubuntu_compatible: currentGeneration.ubuntu_compatible,
|
||||
description: `Generated with AI using ${document.getElementById('ai-model-select').value}`
|
||||
})
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
alert('{{ t('protocols.protocol_created_successfully') }}');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('{{ t('protocols.error_creating_protocol') }}: ' + result.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('{{ t('protocols.error_creating_protocol') }}: ' + error.message);
|
||||
});
|
||||
});
|
||||
|
||||
// AI generate for specific protocol
|
||||
document.querySelectorAll('.ai-generate-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
currentProtocolId = this.dataset.protocolId;
|
||||
showAIModal();
|
||||
document.getElementById('ai-prompt').value = `{{ t('ai.improve_protocol') }} ${this.closest('.protocol-card').dataset.protocolName}`;
|
||||
});
|
||||
});
|
||||
|
||||
// Test custom model availability
|
||||
document.getElementById('ai-model-test-btn').addEventListener('click', async function() {
|
||||
const customModel = document.getElementById('ai-model-custom').value.trim();
|
||||
if (!customModel) { alert('Введите идентификатор модели'); return; }
|
||||
try {
|
||||
const resp = await fetch('/api/ai/test-model', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ model: customModel })
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.success) {
|
||||
alert('Модель доступна');
|
||||
} else {
|
||||
alert('Модель недоступна: ' + (result.error || result.message || ''));
|
||||
}
|
||||
} catch (e) {
|
||||
alert('Ошибка проверки модели: ' + e.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Delete protocol
|
||||
document.querySelectorAll('.delete-protocol-btn').forEach(btn => {
|
||||
btn.addEventListener('click', function() {
|
||||
const protocolId = this.dataset.protocolId;
|
||||
const protocolName = this.dataset.protocolName;
|
||||
|
||||
if (confirm(`{{ t('protocols.confirm_delete_protocol') }} '${protocolName}'?`)) {
|
||||
fetch(`/api/protocols/${protocolId}`, {
|
||||
method: 'DELETE'
|
||||
})
|
||||
.then(response => response.json())
|
||||
.then(result => {
|
||||
if (result.success) {
|
||||
location.reload();
|
||||
} else {
|
||||
alert('{{ t('protocols.error_deleting_protocol') }}: ' + result.error);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
alert('{{ t('protocols.error_deleting_protocol') }}: ' + error.message);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
</script>
|
||||
@@ -0,0 +1,250 @@
|
||||
{% extends "layout.twig" %}
|
||||
|
||||
{% block content %}
|
||||
<div class="container mt-4">
|
||||
<div class="row">
|
||||
<div class="col-md-10 offset-md-1">
|
||||
<div class="card">
|
||||
<div class="card-header bg-primary text-white">
|
||||
<h5 class="mb-0">
|
||||
{% if scenario %}
|
||||
{{ 'Редактирование сценария:' | trans }} {{ scenario.name }}
|
||||
{% else %}
|
||||
{{ 'Новый сценарий' | trans }}
|
||||
{% endif %}
|
||||
</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form id="scenarioForm">
|
||||
<input type="hidden" name="id" value="{{ scenario.id | default('') }}">
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="slug" class="form-label">{{ 'Уникальный идентификатор' | trans }} *</label>
|
||||
<input type="text" class="form-control" id="slug" name="slug"
|
||||
value="{{ scenario.slug | default('') }}" required
|
||||
pattern="^[a-z0-9\-]+$" title="{{ 'Только строчные буквы, цифры и дефисы' | trans }}">
|
||||
<small class="form-text text-muted">{{ 'например: xray-vless, openvpn-tls' | trans }}</small>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="name" class="form-label">{{ 'Название протокола' | trans }} *</label>
|
||||
<input type="text" class="form-control" id="name" name="name"
|
||||
value="{{ scenario.name | default('') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="description" class="form-label">{{ 'Описание' | trans }}</label>
|
||||
<textarea class="form-control" id="description" name="description" rows="2">{{ scenario.description | default('') }}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="definition" class="form-label">{{ 'Определение сценария (JSON)' | trans }} *</label>
|
||||
<textarea class="form-control font-monospace" id="definition" name="definition"
|
||||
rows="20" required>{{ templateDefinition }}</textarea>
|
||||
<small class="form-text text-muted d-block mt-2">
|
||||
<strong>{{ 'Структура JSON:' | trans }}</strong><br>
|
||||
<code>{ "engine": "shell|builtin_awg", "metadata": {...}, "scripts": { "detect": "...", "install": "...", "restore": "..." } }</code>
|
||||
</small>
|
||||
<small class="form-text text-muted d-block mt-2">
|
||||
<strong>{{ 'Доступные переменные в скриптах:' | trans }}</strong><br>
|
||||
<code>{{ "{{server.host}}, {{server.username}}, {{server.container_name}}, {{metadata.*}}" | trans }}</code>
|
||||
</small>
|
||||
<div id="jsonError" class="alert alert-danger mt-2" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<div class="form-check mb-3">
|
||||
<input type="checkbox" class="form-check-input" id="is_active" name="is_active" value="1"
|
||||
{% if scenario.is_active ?? true %}checked{% endif %}>
|
||||
<label class="form-check-label" for="is_active">
|
||||
{{ 'Активный сценарий' | trans }}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-flex gap-2">
|
||||
<button type="submit" class="btn btn-success">
|
||||
<i class="fas fa-save"></i> {{ 'Сохранить' | trans }}
|
||||
</button>
|
||||
<a href="/admin/scenarios" class="btn btn-secondary">
|
||||
{{ 'Отмена' | trans }}
|
||||
</a>
|
||||
{% if scenario %}
|
||||
<button type="button" class="btn btn-info ms-auto" id="testBtn">
|
||||
<i class="fas fa-flask"></i> {{ 'Тест на сервере' | trans }}
|
||||
</button>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- JSON Validation Helper -->
|
||||
<div class="card mt-3">
|
||||
<div class="card-header bg-light">
|
||||
<h6 class="mb-0">{{ 'Справка по формату' | trans }}</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h6>{{ 'Поля сценария:' | trans }}</h6>
|
||||
<ul>
|
||||
<li><strong>engine:</strong> Тип движка ("shell" или "builtin_awg")</li>
|
||||
<li><strong>metadata:</strong> Объект с параметрами протокола (container_name, config_path и т.д.)</li>
|
||||
<li><strong>scripts:</strong> Объект со скриптами (detect, install, restore)</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="mt-3">{{ 'Поля скриптов:' | trans }}</h6>
|
||||
<ul>
|
||||
<li><strong>detect:</strong> Bash скрипт для определения установленной конфигурации. Должен вывести JSON с полями "status" (absent/partial/existing) и "details"</li>
|
||||
<li><strong>install:</strong> Bash скрипт для установки протокола. Должен вывести JSON с "success": true/false</li>
|
||||
<li><strong>restore:</strong> Bash скрипт для восстановления конфигурации из detection результата</li>
|
||||
</ul>
|
||||
|
||||
<h6 class="mt-3">{{ 'Переменные окружения в скриптах:' | trans }}</h6>
|
||||
<ul>
|
||||
<li><code>SERVER_HOST</code> - IP/домен сервера</li>
|
||||
<li><code>SERVER_USER</code> - SSH пользователь</li>
|
||||
<li><code>SERVER_CONTAINER</code> - имя контейнера</li>
|
||||
<li><code>PROTOCOL_*</code> - все поля из metadata (например, PROTOCOL_CONTAINER_NAME, PROTOCOL_CONFIG_PATH)</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Test Modal -->
|
||||
<div class="modal fade" id="testModal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">{{ 'Тест сценария' | trans }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="testServer" class="form-label">{{ 'Выбрать сервер' | trans }}</label>
|
||||
<select class="form-control" id="testServer">
|
||||
<option value="">{{ 'Загружаю...' | trans }}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="testResult" class="mt-3" style="display: none;">
|
||||
<strong>{{ 'Результат:' | trans }}</strong>
|
||||
<pre id="testResultContent" class="bg-light p-3 rounded" style="max-height: 300px; overflow-y: auto;"></pre>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">{{ 'Закрыть' | trans }}</button>
|
||||
<button type="button" class="btn btn-primary" id="runTestBtn">{{ 'Запустить тест' | trans }}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const form = document.getElementById('scenarioForm');
|
||||
const definitionTextarea = document.getElementById('definition');
|
||||
const jsonError = document.getElementById('jsonError');
|
||||
const testBtn = document.getElementById('testBtn');
|
||||
|
||||
// Validate JSON on change
|
||||
definitionTextarea.addEventListener('change', validateJson);
|
||||
definitionTextarea.addEventListener('blur', validateJson);
|
||||
|
||||
function validateJson() {
|
||||
jsonError.style.display = 'none';
|
||||
try {
|
||||
JSON.parse(definitionTextarea.value);
|
||||
} catch (e) {
|
||||
jsonError.textContent = `{{ "Ошибка JSON:" | trans }} ${e.message}`;
|
||||
jsonError.style.display = 'block';
|
||||
}
|
||||
}
|
||||
|
||||
// Form submission
|
||||
form.addEventListener('submit', async function(e) {
|
||||
e.preventDefault();
|
||||
|
||||
if (!validateJson()) return;
|
||||
|
||||
const formData = new FormData(this);
|
||||
|
||||
try {
|
||||
const response = await fetch('/admin/scenario', {
|
||||
method: 'POST',
|
||||
body: formData
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert('{{ "Сценарий успешно сохранен" | trans }}');
|
||||
window.location.href = data.redirect;
|
||||
} else {
|
||||
alert(`{{ "Ошибка:" | trans }} ${data.message}`);
|
||||
}
|
||||
} catch (err) {
|
||||
alert(`{{ "Ошибка отправки:" | trans }} ${err}`);
|
||||
}
|
||||
});
|
||||
|
||||
// Test button
|
||||
if (testBtn) {
|
||||
testBtn.addEventListener('click', function() {
|
||||
loadServers();
|
||||
new bootstrap.Modal(document.getElementById('testModal')).show();
|
||||
});
|
||||
}
|
||||
|
||||
// Load available servers
|
||||
async function loadServers() {
|
||||
try {
|
||||
const response = await fetch('/api/servers?limit=50');
|
||||
const data = await response.json();
|
||||
|
||||
const select = document.getElementById('testServer');
|
||||
select.innerHTML = '';
|
||||
|
||||
if (data.servers && data.servers.length > 0) {
|
||||
data.servers.forEach(server => {
|
||||
const option = document.createElement('option');
|
||||
option.value = server.id;
|
||||
option.textContent = `${server.name} (${server.host})`;
|
||||
select.appendChild(option);
|
||||
});
|
||||
} else {
|
||||
select.innerHTML = '<option value="">{{ "Сервера не найдены" | trans }}</option>';
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error loading servers:', err);
|
||||
}
|
||||
}
|
||||
|
||||
// Run test
|
||||
document.getElementById('runTestBtn').addEventListener('click', async function() {
|
||||
const serverId = document.getElementById('testServer').value;
|
||||
if (!serverId) {
|
||||
alert('{{ "Выберите сервер" | trans }}');
|
||||
return;
|
||||
}
|
||||
|
||||
const scenarioId = document.querySelector('input[name="id"]').value;
|
||||
|
||||
try {
|
||||
const response = await fetch(`/admin/scenario/${scenarioId}/test`, {
|
||||
method: 'POST',
|
||||
headers: {'Content-Type': 'application/x-www-form-urlencoded'},
|
||||
body: `server_id=${serverId}`
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
const resultDiv = document.getElementById('testResult');
|
||||
const resultContent = document.getElementById('testResultContent');
|
||||
|
||||
resultContent.textContent = JSON.stringify(data.result, null, 2);
|
||||
resultDiv.style.display = 'block';
|
||||
} catch (err) {
|
||||
alert(`{{ "Ошибка теста:" | trans }} ${err}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user