Merge pull request #133 from ReturnFI/beta

Introducing Decoy Sites & Smarter Management!
This commit is contained in:
Whispering Wind
2025-04-27 18:56:38 +03:30
committed by GitHub
11 changed files with 731 additions and 331 deletions

View File

@ -1,6 +1,13 @@
## [1.7.1] - 2025-04-25
## [1.8.0] - 2025-04-27
### Changed
- feat: Add init_paths to handle sys.path setup for importing shared modules
- feat: Add insecure parameter to generate_uri() function
- feat: Add SNI checker and certificate manager
🛡️ feat: Add Decoy Site feature
🖥️ feat: Add Decoy status API for web panel integration
🔧 feat (API): Add endpoints for Decoy site management using background tasks
🛠️ feat (CLI): Add Decoy Site management commands to CLI
🌐 feat: Integrate Decoy Site functionality into the Hysteria web panel
🧹 improve: Enhance restore.sh to automatically fix config.json based on active network interface
🛡️ improve: Prevent duplicate WARP outbound entries and improve the installation flow
🛠️ fix: Completely remove WARP ACL rules when the wg-quick@wgcf service is inactive
🛠️ fix: Properly handle domain names in the IP4 configuration variable
📝 fix: Correct minor typos across the project

View File

@ -447,15 +447,17 @@ def normalsub(action: str, domain: str, port: int):
@click.option('--port', '-p', required=False, help='Port number for WebPanel service', type=int)
@click.option('--admin-username', '-au', required=False, help='Admin username for WebPanel', type=str)
@click.option('--admin-password', '-ap', required=False, help='Admin password for WebPanel', type=str)
@click.option('--expiration-minutes', '-e', required=False, help='Expiration minutes for WebPanel', type=int, default=20)
@click.option('--expiration-minutes', '-e', required=False, help='Expiration minutes for WebPanel session', type=int, default=20)
@click.option('--debug', '-g', is_flag=True, help='Enable debug mode for WebPanel', default=False)
def webpanel(action: str, domain: str, port: int, admin_username: str, admin_password: str, expiration_minutes: int, debug: bool):
@click.option('--decoy-path', '-dp', required=False, type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True), help='Optional path to serve as a decoy site (only for start action)') # Add decoy_path option
def webpanel(action: str, domain: str, port: int, admin_username: str, admin_password: str, expiration_minutes: int, debug: bool, decoy_path: str | None): # Add decoy_path parameter
"""Manages the Hysteria Web Panel service."""
try:
if action == 'start':
if not domain or not port or not admin_username or not admin_password:
raise click.UsageError('Error: the --domain, --port, --admin-username, and --admin-password are required for the start action.')
cli_api.start_webpanel(domain, port, admin_username, admin_password, expiration_minutes, debug)
cli_api.start_webpanel(domain, port, admin_username, admin_password, expiration_minutes, debug, decoy_path)
services_status = cli_api.get_services_status()
if not services_status:
@ -467,12 +469,37 @@ def webpanel(action: str, domain: str, port: int, admin_username: str, admin_pas
url = cli_api.get_webpanel_url()
click.echo(f'Hysteria web panel is now running. The service is accessible on: {url}')
if decoy_path:
click.echo(f'Decoy site configured using path: {decoy_path}')
elif action == 'stop':
if decoy_path:
click.echo('Warning: --decoy-path option is ignored for the stop action.', err=True)
cli_api.stop_webpanel()
click.echo(f'WebPanel stopped successfully.')
except Exception as e:
click.echo(f'{e}', err=True)
@cli.command('setup-webpanel-decoy')
@click.option('--domain', '-d', required=True, help='Domain name associated with the web panel', type=str)
@click.option('--decoy-path', '-dp', required=True, type=click.Path(exists=True, file_okay=False, dir_okay=True, readable=True), help='Path to the directory containing the decoy website files (e.g., /var/www/html)')
def setup_webpanel_decoy(domain: str, decoy_path: str):
"""Sets up or updates the decoy site for the running Web Panel."""
try:
cli_api.setup_webpanel_decoy(domain, decoy_path)
click.echo(f'Web Panel decoy site configured successfully for domain {domain} using path {decoy_path}.')
click.echo('Note: Caddy service was restarted.')
except Exception as e:
click.echo(f'{e}', err=True)
@cli.command('stop-webpanel-decoy')
def stop_webpanel_decoy():
"""Stops the decoy site functionality for the Web Panel."""
try:
cli_api.stop_webpanel_decoy()
click.echo(f'Web Panel decoy site stopped and configuration removed successfully.')
click.echo('Note: Caddy service was restarted.')
except Exception as e:
click.echo(f'{e}', err=True)
@cli.command('get-webpanel-url')
def get_web_panel_url():

View File

@ -12,7 +12,7 @@ DEBUG = False
SCRIPT_DIR = '/etc/hysteria/core/scripts'
CONFIG_FILE = '/etc/hysteria/config.json'
CONFIG_ENV_FILE = '/etc/hysteria/.configs.env'
WEBPANEL_ENV_FILE = '/etc/hysteria/core/scripts/webpanel/.env'
class Command(Enum):
'''Contains path to command's script'''
@ -507,13 +507,13 @@ def stop_normalsub():
run_cmd(['bash', Command.INSTALL_NORMALSUB.value, 'stop'])
def start_webpanel(domain: str, port: int, admin_username: str, admin_password: str, expiration_minutes: int, debug: bool):
def start_webpanel(domain: str, port: int, admin_username: str, admin_password: str, expiration_minutes: int, debug: bool, decoy_path: str):
'''Starts WebPanel.'''
if not domain or not port or not admin_username or not admin_password or not expiration_minutes:
raise InvalidInputError('Error: Both --domain and --port are required for the start action.')
run_cmd(
['bash', Command.SHELL_WEBPANEL.value, 'start',
domain, str(port), admin_username, admin_password, str(expiration_minutes), str(debug).lower()]
domain, str(port), admin_username, admin_password, str(expiration_minutes), str(debug).lower(), str(decoy_path)]
)
@ -521,6 +521,32 @@ def stop_webpanel():
'''Stops WebPanel.'''
run_cmd(['bash', Command.SHELL_WEBPANEL.value, 'stop'])
def setup_webpanel_decoy(domain: str, decoy_path: str):
'''Sets up or updates the decoy site for the web panel.'''
if not domain or not decoy_path:
raise InvalidInputError('Error: Both domain and decoy_path are required.')
run_cmd(['bash', Command.SHELL_WEBPANEL.value, 'decoy', domain, decoy_path])
def stop_webpanel_decoy():
'''Stops and removes the decoy site configuration for the web panel.'''
run_cmd(['bash', Command.SHELL_WEBPANEL.value, 'stopdecoy'])
def get_webpanel_decoy_status() -> dict[str, Any]:
"""Checks the status of the webpanel decoy site configuration."""
try:
if not os.path.exists(WEBPANEL_ENV_FILE):
return {"active": False, "path": None}
env_vars = dotenv_values(WEBPANEL_ENV_FILE)
decoy_path = env_vars.get('DECOY_PATH')
if decoy_path and decoy_path.strip():
return {"active": True, "path": decoy_path.strip()}
else:
return {"active": False, "path": None}
except Exception as e:
print(f"Error checking decoy status: {e}")
return {"active": False, "path": None}
def get_webpanel_url() -> str | None:
'''Gets the URL of WebPanel.'''

View File

@ -10,6 +10,7 @@ else
echo "Error: Config file $CONFIG_ENV not found."
exit 1
fi
update_sni() {
local sni=$1
local server_ip
@ -21,10 +22,21 @@ update_sni() {
fi
if [ -n "$IP4" ]; then
if [[ $IP4 =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
server_ip="$IP4"
echo "Using server IP from config: $server_ip"
else
server_ip=$(curl -s ifconfig.me)
domain_ip=$(dig +short "$IP4" A | head -n 1)
if [ -n "$domain_ip" ]; then
server_ip="$domain_ip"
echo "Resolved domain $IP4 to IP: $server_ip"
else
server_ip=$(curl -s -4 ifconfig.me)
echo "Could not resolve domain $IP4. Using auto-detected server IP: $server_ip"
fi
fi
else
server_ip=$(curl -s -4 ifconfig.me)
echo "Using auto-detected server IP: $server_ip"
fi

View File

@ -61,7 +61,6 @@ for file in "${expected_files[@]}"; do
fi
done
timestamp=$(date +%Y%m%d_%H%M%S)
existing_backup_dir="/opt/hysbackup/restore_pre_backup_$timestamp"
mkdir -p "$existing_backup_dir"
@ -85,11 +84,58 @@ for file in "${expected_files[@]}"; do
fi
done
CONFIG_FILE="$TARGET_DIR/config.json"
if [ -f "$CONFIG_FILE" ]; then
echo "Checking and adjusting config.json based on system state..."
networkdef=$(ip route | grep "^default" | awk '{print $5}')
if [ -n "$networkdef" ]; then
current_v4_device=$(jq -r '.outbounds[] | select(.name=="v4") | .direct.bindDevice' "$CONFIG_FILE")
if [ "$current_v4_device" != "$networkdef" ]; then
echo "Updating v4 outbound bindDevice from '$current_v4_device' to '$networkdef'..."
tmpfile=$(mktemp)
jq --arg newdev "$networkdef" '
.outbounds = (.outbounds | map(
if .name == "v4" then
.direct.bindDevice = $newdev
else
.
end
))
' "$CONFIG_FILE" > "$tmpfile"
cat "$tmpfile" > "$CONFIG_FILE"
rm -f "$tmpfile"
fi
fi
if ! systemctl is-active --quiet wg-quick@wgcf.service; then
echo "wgcf service is NOT active. Removing warps outbound and any ACL rules..."
tmpfile=$(mktemp)
jq '
.outbounds = (.outbounds | map(select(.name != "warps"))) |
.acl.inline = (.acl.inline | map(
select(test("^warps\\(") | not)
))
' "$CONFIG_FILE" > "$tmpfile"
cat "$tmpfile" > "$CONFIG_FILE"
rm -f "$tmpfile"
fi
fi
rm -rf "$RESTORE_DIR"
echo "Hysteria configuration restored successfully."
echo "Hysteria configuration restored and updated successfully."
chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt
chmod 640 /etc/hysteria/ca.key /etc/hysteria/ca.crt
python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "Error: Restart service failed'."

View File

@ -3,16 +3,39 @@
source /etc/hysteria/core/scripts/path.sh
if systemctl is-active --quiet wg-quick@wgcf.service; then
echo "WARP is already active. Skipping installation and configuration update."
echo "WARP is already active. Checking configuration..."
if [ -f "$CONFIG_FILE" ] && jq -e '.outbounds[] | select(.name == "warps")' "$CONFIG_FILE" > /dev/null 2>&1; then
echo "WARP outbound already exists in the configuration. No changes needed."
else
if [ -f "$CONFIG_FILE" ]; then
jq '.outbounds += [{"name": "warps", "type": "direct", "direct": {"mode": 4, "bindDevice": "wgcf"}}]' "$CONFIG_FILE" > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json "$CONFIG_FILE"
python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
echo "WARP outbound added to config.json."
else
echo "Error: Config file $CONFIG_FILE not found."
fi
fi
else
echo "Installing WARP..."
bash <(curl -fsSL https://raw.githubusercontent.com/ReturnFI/Warp/main/warp.sh) wgx
if systemctl is-active --quiet wg-quick@wgcf.service; then
echo "WARP installation successful."
if [ -f "$CONFIG_FILE" ]; then
if jq -e '.outbounds[] | select(.name == "warps")' "$CONFIG_FILE" > /dev/null 2>&1; then
echo "WARP outbound already exists in the configuration."
else
jq '.outbounds += [{"name": "warps", "type": "direct", "direct": {"mode": 4, "bindDevice": "wgcf"}}]' "$CONFIG_FILE" > /etc/hysteria/config_temp.json && mv /etc/hysteria/config_temp.json "$CONFIG_FILE"
echo "WARP outbound added to config.json."
fi
python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
echo "WARP installed and outbound added to config.json."
echo "Hysteria2 restarted with updated configuration."
else
echo "Error: Config file $CONFIG_FILE not found."
fi
else
echo "WARP installation failed."
fi
fi

View File

@ -10,6 +10,7 @@ class Configs(BaseSettings):
API_TOKEN: str
EXPIRATION_MINUTES: int
ROOT_PATH: str
DECOY_PATH: str | None = None
class Config:
env_file = '.env'

View File

@ -1,6 +1,6 @@
from fastapi import APIRouter, HTTPException, UploadFile, File
from fastapi import APIRouter, BackgroundTasks, HTTPException, UploadFile, File
from ..schema.config.hysteria import ConfigFile, GetPortResponse, GetSniResponse
from ..schema.response import DetailResponse, IPLimitConfig
from ..schema.response import DetailResponse, IPLimitConfig, SetupDecoyRequest, DecoyStatusResponse
from fastapi.responses import FileResponse
import shutil
import zipfile
@ -334,3 +334,54 @@ async def config_ip_limit_api(config: IPLimitConfig):
return DetailResponse(detail=details)
except Exception as e:
raise HTTPException(status_code=400, detail=f'Error configuring IP Limiter: {str(e)}')
def run_setup_decoy_background(domain: str, decoy_path: str):
"""Function to run decoy setup in the background."""
try:
cli_api.setup_webpanel_decoy(domain, decoy_path)
except Exception:
pass
def run_stop_decoy_background():
"""Function to run decoy stop in the background."""
try:
cli_api.stop_webpanel_decoy()
except Exception:
pass
@router.post('/webpanel/decoy/setup', response_model=DetailResponse, summary='Setup/Update WebPanel Decoy Site (Background Task)')
async def setup_decoy_api(request_body: SetupDecoyRequest, background_tasks: BackgroundTasks):
"""
Initiates the setup or update of the decoy site configuration for the web panel.
Requires the web panel service to be running.
The actual operation (including Caddy restart) runs in the background.
"""
if not os.path.isdir(request_body.decoy_path):
raise HTTPException(status_code=400, detail=f"Decoy path does not exist or is not a directory: {request_body.decoy_path}")
background_tasks.add_task(run_setup_decoy_background, request_body.domain, request_body.decoy_path)
return DetailResponse(detail=f'Web Panel decoy site setup initiated for domain {request_body.domain}. Caddy will restart in the background.')
@router.post('/webpanel/decoy/stop', response_model=DetailResponse, summary='Stop WebPanel Decoy Site (Background Task)')
async def stop_decoy_api(background_tasks: BackgroundTasks):
"""
Initiates the removal of the decoy site configuration for the web panel.
The actual operation (including Caddy restart) runs in the background.
"""
background_tasks.add_task(run_stop_decoy_background)
return DetailResponse(detail='Web Panel decoy site stop initiated. Caddy will restart in the background.')
@router.get('/webpanel/decoy/status', response_model=DecoyStatusResponse, summary='Get WebPanel Decoy Site Status')
async def get_decoy_status_api():
"""
Checks if the decoy site is currently configured and active.
"""
try:
status = cli_api.get_webpanel_decoy_status()
return DecoyStatusResponse(**status)
except Exception as e:
raise HTTPException(status_code=500, detail=f'Error retrieving decoy status: {str(e)}')

View File

@ -1,5 +1,5 @@
from typing import Optional
from pydantic import BaseModel
from pydantic import BaseModel, Field
class DetailResponse(BaseModel):
@ -8,3 +8,11 @@ class DetailResponse(BaseModel):
class IPLimitConfig(BaseModel):
block_duration: Optional[int] = None
max_ips: Optional[int] = None
class SetupDecoyRequest(BaseModel):
domain: str = Field(..., description="Domain name associated with the web panel")
decoy_path: str = Field(..., description="Absolute path to the directory containing the decoy website files")
class DecoyStatusResponse(BaseModel):
active: bool = Field(..., description="Whether the decoy site is currently configured and active")
path: Optional[str] = Field(None, description="The configured path for the decoy site, if active")

View File

@ -29,8 +29,7 @@
<li class='nav-item'>
<a class='nav-link' id='telegram-tab' data-toggle='pill' href='#telegram' role='tab'
aria-controls='telegram' aria-selected='true'><i class="fab fa-telegram"></i>
Telegram
Bot</a>
Telegram Bot</a>
</li>
<li class='nav-item'>
<a class='nav-link' id='port-tab' data-toggle='pill' href='#port' role='tab'
@ -47,18 +46,21 @@
aria-controls='change_ip' aria-selected='false'><i class="fas fa-network-wired"></i>
Change IP</a>
</li>
<!-- New Backup Tab -->
<li class='nav-item'>
<a class='nav-link' id='backup-tab' data-toggle='pill' href='#backup' role='tab'
aria-controls='backup' aria-selected='false'><i class="fas fa-download"></i>
Backup</a>
</li>
<!-- IP Limiter Tab -->
<li class='nav-item'>
<a class='nav-link' id='ip-limit-tab' data-toggle='pill' href='#ip-limit' role='tab'
aria-controls='ip-limit' aria-selected='false'><i class="fas fa-user-slash"></i>
IP Limit</a>
</li>
<li class='nav-item'>
<a class='nav-link' id='decoy-tab' data-toggle='pill' href='#decoy' role='tab'
aria-controls='decoy' aria-selected='false'><i class="fas fa-mask"></i>
Decoy Site</a>
</li>
</ul>
</div>
<div class='card-body' style="margin-left: 25px;">
@ -74,35 +76,6 @@
</ul>
<div class='tab-content' id='subs-tabs-content'>
<br>
<!-- <div class='tab-pane fade show active' id='singbox' role='tabpanel'
aria-labelledby='singbox-tab'>
<form id="singbox">
<div class='form-group'>
<label for='singbox_domain'>Domain:</label>
<input type='text' class='form-control' id='singbox_domain'
placeholder='Enter Domain'>
<div class="invalid-feedback">
Please enter a valid domain (without http:// or https://).
</div>
</div>
<div class='form-group'>
<label for='singbox_port'>Port:</label>
<input type='text' class='form-control' id='singbox_port'
placeholder='Enter Port'>
<div class="invalid-feedback">
Please enter a valid port number.
</div>
</div>
<button id="singbox_start" type='button' class='btn btn-success'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Start
</button>
<button id="singbox_stop" type='button' class='btn btn-danger'
style="display: none;">Stop</button>
</form>
</div> -->
<!-- Normal Sub Tab -->
<div class='tab-pane fade show active' id='normal' role='tabpanel' aria-labelledby='normal-tab'>
<form id="normal">
@ -134,7 +107,6 @@
</div>
</div>
<!-- Telegram Bot Tab -->
<div class='tab-pane fade' id='telegram' role='tabpanel' aria-labelledby='telegram-tab'>
<form id="telegram">
@ -158,13 +130,11 @@
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>Start</button>
<button id="telegram_stop" type='button' class='btn btn-danger'
style="display: none;">Stop</button>
</form>
</div>
<!-- Port Tab -->
<div class='tab-pane fade' id='port' role='tabpanel' aria-labelledby='port-tab'>
<form id="port">
<div class='form-group'>
<label for='hysteria_port'>Port:</label>
@ -180,7 +150,6 @@
<!-- SNI Tab -->
<div class='tab-pane fade' id='sni' role='tabpanel' aria-labelledby='sni-tab'>
<form id="sni">
<div class='form-group'>
<label for='sni_domain'>Domain:</label>
@ -199,24 +168,25 @@
<form id="change_ip">
<div class='form-group'>
<label for='ipv4'>IPv4:</label>
<input type='text' class='form-control' id='ipv4' placeholder='Enter IPv4'
<input type='text' class='form-control' id='ipv4' placeholder='Enter IPv4 or Domain'
value="{{ ipv4 or '' }}">
<div class="invalid-feedback">
Please enter a valid IPv4 address.
Please enter a valid IPv4 address or Domain.
</div>
</div>
<div class='form-group'>
<label for='ipv6'>IPv6:</label>
<input type='text' class='form-control' id='ipv6' placeholder='Enter IPv6'
<input type='text' class='form-control' id='ipv6' placeholder='Enter IPv6 or Domain'
value="{{ ipv6 or '' }}">
<div class="invalid-feedback">
Please enter a valid IPv6 address.
Please enter a valid IPv6 address or Domain.
</div>
</div>
<button id="ip_change" type='button' class='btn btn-primary'>Save</button>
</form>
</div>
<!-- Backup Tab (New) -->
<!-- Backup Tab -->
<div class='tab-pane fade' id='backup' role='tabpanel' aria-labelledby='backup-tab'>
<div class="form-group">
<label for="backup_file">Upload Backup:</label>
@ -229,10 +199,10 @@
<div id="backup_progress_bar" class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div>
<div id="backup_status" class="mt-2"></div> <!-- Status messages -->
<div id="backup_status" class="mt-2"></div>
</div>
<!-- IP Limit Tab (New) -->
<!-- IP Limit Tab -->
<div class='tab-pane fade' id='ip-limit' role='tabpanel' aria-labelledby='ip-limit-tab'>
<ul class='nav nav-tabs' id='ip-limit-tabs' role='tablist'>
<li class='nav-item'>
@ -284,6 +254,40 @@
</div>
</div>
<!-- Decoy Site Tab -->
<div class='tab-pane fade' id='decoy' role='tabpanel' aria-labelledby='decoy-tab'>
<form id="decoy_form">
<div class='form-group'>
<label for='decoy_domain'>Domain:</label>
<input type='text' class='form-control' id='decoy_domain'
placeholder='Enter Domain'>
<div class="invalid-feedback">
Please enter a valid domain (without http:// or https://).
</div>
</div>
<div class='form-group'>
<label for='decoy_path'>Decoy Site Path:</label>
<input type='text' class='form-control' id='decoy_path'
placeholder='Enter Path to Decoy Site Files'>
<div class="invalid-feedback">
Please enter a valid directory path.
</div>
</div>
<button id="decoy_setup" type='button' class='btn btn-success'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Setup Decoy
</button>
<button id="decoy_stop" type='button' class='btn btn-danger' style="display: none;">Stop Decoy</button>
</form>
<div class="mt-4">
<h5>Decoy Status</h5>
<div id="decoy_status_container" class="p-3 border rounded">
<div id="decoy_status_message">Loading status...</div>
</div>
</div>
</div>
</div>
</div>
<!-- /.card -->
@ -298,15 +302,20 @@
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- Font Awesome -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/js/all.min.js"
integrity="sha512-yFjZbTYRCJodnuyGlsKamNE/LlEaEA/3apsIOPr7/l+jCMq9Dn9x5qyuAGqgpr4/NBZ95p8yrl/sLhJvoazg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>
$(document).ready(function () {
initUI();
fetchDecoyStatus();
function isValidPath(path) {
if (!path) return false;
return path.trim() !== '';
}
function isValidDomain(domain) {
if (!domain) return false;
@ -318,12 +327,13 @@
if (!port) return false;
return /^[0-9]+$/.test(port) && parseInt(port) > 0 && parseInt(port) <= 65535;
}
function isValidIPorDomain(input) {
if (!input) return true;
const ipV4Regex = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
const ipV6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}([0-9a-fA-F]{1,4}|:)|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,2}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,3}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,5}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,6}){1,6})|:((:[0-9a-fA-F]{1,7}){1,7}|:))$/;
const domainRegex = /^(?!\-)(?:[a-zA-Z\d\-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+)[a-zA-Z\d]{1,63}$/;
const ipV6Regex = /^(([0-9a-fA-F]{1,4}:){7,7}([0-9a-fA-F]{1,4}|:)|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))$/;
const domainRegex = /^(?!-)(?:[a-zA-Z\d-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+$)[a-zA-Z\d]{1,63}$/;
const lowerInput = input.toLowerCase();
if (ipV4Regex.test(input)) return true;
@ -338,7 +348,6 @@
return /^[0-9]+$/.test(value) && parseInt(value) > 0;
}
function confirmAction(actionName, callback) {
Swal.fire({
title: `Are you sure?`,
@ -369,14 +378,14 @@
}
},
success: function (response) {
Swal.fire("Success!", successMessage, "success");
if (showReload) {
Swal.fire("Success!", successMessage, "success").then(() => {
location.reload();
});
} else {
Swal.fire("Success!", successMessage, "success");
}
console.log(response);
console.log("Success Response:", response);
},
error: function (xhr, status, error) {
let errorMessage = "Something went wrong.";
@ -384,7 +393,7 @@
errorMessage = xhr.responseJSON.detail;
}
Swal.fire("Error!", errorMessage, "error");
console.error(error);
console.error("AJAX Error:", status, error, xhr.responseText);
},
complete: function() {
if (buttonSelector) {
@ -400,47 +409,28 @@
$(`#${formId} .form-control`).each(function () {
const input = $(this);
const id = input.attr('id');
let fieldValid = true;
if (id.includes('domain')) {
if (!isValidDomain(input.val())) {
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
fieldValid = isValidDomain(input.val());
} else if (id.includes('port')) {
if (!isValidPort(input.val())) {
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
fieldValid = isValidPort(input.val());
} else if (id === 'ipv4' || id === 'ipv6') {
if (!isValidIPorDomain(input.val())) {
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
fieldValid = (input.val().trim() === '') ? true : isValidIPorDomain(input.val());
} else if (id === 'block_duration' || id === 'max_ips') {
if (!isValidPositiveNumber(input.val())) {
fieldValid = isValidPositiveNumber(input.val());
} else if (id === 'decoy_path') {
fieldValid = isValidPath(input.val());
} else {
fieldValid = input.val().trim() !== "";
}
if (!fieldValid) {
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
}
else {
if (!input.val().trim()) {
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
}
});
return isValid;
}
@ -452,94 +442,21 @@
success: function (data) {
updateServiceUI(data);
},
error: function () {
console.error("Failed to fetch service status.");
error: function (xhr, status, error) {
console.error("Failed to fetch service status:", error, xhr.responseText);
Swal.fire("Error!", "Could not fetch service statuses.", "error");
}
});
function updateServiceUI(data) {
const servicesMap = {
"hysteria_telegram_bot": "#telegram",
// "hysteria_singbox": "#singbox", // singbox removed
"hysteria_normal_sub": "#normal",
"hysteria_iplimit": "#ip-limit-service"
};
Object.keys(servicesMap).forEach(service => {
let selector = servicesMap[service];
let isRunning = data[service];
let serviceName = service.replace("hysteria_", "");
if (isRunning) {
$(selector + " input, " + selector + " label").remove();
$(selector + " .btn-success").hide();
$(selector).prepend(`<div class='alert alert-info'>Service is running. You can stop it if needed.</div>`);
$(selector + " .btn-danger").show();
// if (service === "hysteria_singbox") { // singbox removed
// $("#singbox_start").prop('disabled', true);
// }
if(service === "hysteria_telegram_bot"){
$("#telegram_start").prop('disabled', true);
}
if(service === "hysteria_normal_sub"){
$("#normal_start").prop('disabled', true);
}
if(service === "hysteria_iplimit"){
$("#ip_limit_start").hide();
$("#ip_limit_stop").show();
$(".ip-limit-config-tab-li").show();
if (!$("#ip-limit-config-tab").hasClass('active')) {
$('#ip-limit-service-tab').tab('show');
}
}
} else {
$(selector + " input, " + selector + " label").show();
$(selector + " .btn-success").show();
$(selector + " .btn-danger").hide();
$(selector + " .alert-info").remove();
// if (service === "hysteria_singbox") { // singbox removed
// $("#singbox_start").prop('disabled', false);
// }
if(service === "hysteria_telegram_bot"){
$("#telegram_start").prop('disabled', false);
}
if(service === "hysteria_normal_sub"){
$("#normal_start").prop('disabled', false);
}
if(service === "hysteria_iplimit"){
$("#ip_limit_start").show();
$("#ip_limit_stop").hide();
$(".ip-limit-config-tab-li").hide();
$('#ip-limit-service-tab').tab('show');
}
}
});
}
$.ajax({
url: "{{ url_for('get_ip_api') }}",
type: "GET",
success: function (data) {
$("#ipv4").val(data.ipv4 || "");
$("#ipv6").val(data.ipv6 || "");
$("#ipv4").attr("placeholder", "Enter IPv4 or Domain");
$("#ipv6").attr("placeholder", "Enter IPv6 or Domain");
},
error: function () {
console.error("Failed to fetch IP addresses.");
$("#ipv4").attr("placeholder", "Enter IPv4 or Domain");
$("#ipv6").attr("placeholder", "Enter IPv6 or Domain");
error: function (xhr, status, error) {
console.error("Failed to fetch IP addresses:", error, xhr.responseText);
}
});
@ -549,8 +466,8 @@
success: function (data) {
$("#hysteria_port").val(data.port || "");
},
error: function () {
console.error("Failed to fetch port.");
error: function (xhr, status, error) {
console.error("Failed to fetch port:", error, xhr.responseText);
}
});
@ -560,11 +477,123 @@
success: function (data) {
$("#sni_domain").val(data.sni || "");
},
error: function () {
console.error("Failed to fetch SNI domain.");
error: function (xhr, status, error) {
console.error("Failed to fetch SNI domain:", error, xhr.responseText);
}
});
}
function updateServiceUI(data) {
const servicesMap = {
"hysteria_telegram_bot": "#telegram",
"hysteria_normal_sub": "#normal",
"hysteria_iplimit": "#ip-limit-service"
};
Object.keys(servicesMap).forEach(service => {
let selector = servicesMap[service];
let isRunning = data[service];
if (isRunning) {
$(selector + " .form-group").hide();
$(selector + " .btn-success").hide();
$(selector + " .btn-danger").show();
if ($(selector + " .alert-info").length === 0) {
$(selector).prepend(`<div class='alert alert-info'>Service is running. You can stop it if needed.</div>`);
}
if(service === "hysteria_iplimit"){
$("#ip_limit_start").hide();
$("#ip_limit_stop").show();
$(".ip-limit-config-tab-li").show();
}
} else {
$(selector + " .form-group").show();
$(selector + " .btn-success").show();
$(selector + " .btn-danger").hide();
$(selector + " .alert-info").remove();
if(service === "hysteria_iplimit"){
$("#ip_limit_start").show();
$("#ip_limit_stop").hide();
$(".ip-limit-config-tab-li").hide();
$('#ip-limit-service-tab').tab('show');
}
}
});
}
function setupDecoy() {
if (!validateForm('decoy_form')) return;
const domain = $("#decoy_domain").val();
const path = $("#decoy_path").val();
confirmAction("set up the decoy site", function () {
sendRequest(
"{{ url_for('setup_decoy_api') }}",
"POST",
{ domain: domain, decoy_path: path },
"Decoy site setup initiated successfully!",
"#decoy_setup",
false
);
setTimeout(fetchDecoyStatus, 2000);
});
}
function stopDecoy() {
confirmAction("stop the decoy site", function () {
sendRequest(
"{{ url_for('stop_decoy_api') }}",
"POST",
null,
"Decoy site stop initiated successfully!",
"#decoy_stop",
false
);
setTimeout(fetchDecoyStatus, 2000);
});
}
function fetchDecoyStatus() {
$.ajax({
url: "{{ url_for('get_decoy_status_api') }}",
type: "GET",
success: function (data) {
updateDecoyStatusUI(data);
},
error: function (xhr, status, error) {
$("#decoy_status_message").html('<div class="alert alert-danger">Failed to fetch decoy status.</div>');
console.error("Failed to fetch decoy status:", error, xhr.responseText);
}
});
}
function updateDecoyStatusUI(data) {
if (data.active) {
$("#decoy_form .form-group").hide();
$("#decoy_setup").hide();
$("#decoy_stop").show();
$("#decoy_form .alert-info").remove();
if ($("#decoy_form .alert-info").length === 0) {
$("#decoy_form").prepend(`<div class='alert alert-info'>Decoy site is running. You can stop it if needed.</div>`);
}
$("#decoy_status_message").html(`
<strong>Status:</strong> <span class="text-success">Active</span><br>
<strong>Path:</strong> ${data.path || 'N/A'}
`);
} else {
$("#decoy_form .form-group").show();
$("#decoy_setup").show();
$("#decoy_stop").hide();
$("#decoy_form .alert-info").remove();
$("#decoy_status_message").html('<strong>Status:</strong> <span class="text-danger">Not Active</span>');
}
}
function startTelegram() {
if (!validateForm('telegram')) return;
const apiToken = $("#telegram_api_token").val();
@ -587,38 +616,11 @@
"DELETE",
null,
"Telegram bot stopped successfully!",
null
"#telegram_stop"
);
});
}
// function startSingbox() { // singbox removed
// if (!validateForm('singbox')) return;
// const domain = $("#singbox_domain").val();
// const port = $("#singbox_port").val();
// confirmAction("start SingBox", function () {
// sendRequest(
// "{{ url_for('singbox_start_api') }}",
// "POST",
// { domain: domain, port: port },
// "SingBox started successfully!",
// "#singbox_start"
// );
// });
// }
// function stopSingbox() { // singbox removed
// confirmAction("stop SingBox", function () {
// sendRequest(
// "{{ url_for('singbox_stop_api') }}",
// "DELETE",
// null,
// "SingBox stopped successfully!",
// null
// );
// });
// }
function startNormal() {
if (!validateForm('normal')) return;
const domain = $("#normal_domain").val();
@ -641,8 +643,7 @@
"DELETE",
null,
"Normal subscription stopped successfully!",
null
"#normal_stop"
);
});
}
@ -653,7 +654,7 @@
const baseUrl = "{{ url_for('set_port_api', port='PORT_PLACEHOLDER') }}";
const url = baseUrl.replace("PORT_PLACEHOLDER", port);
confirmAction("change the port", function () {
sendRequest(url, "GET", null, "Port changed successfully!",null);
sendRequest(url, "GET", null, "Port changed successfully!", "#port_change");
});
}
@ -663,43 +664,44 @@
const baseUrl = "{{ url_for('set_sni_api', sni='SNI_PLACEHOLDER') }}";
const url = baseUrl.replace("SNI_PLACEHOLDER", domain);
confirmAction("change the SNI", function () {
sendRequest(url, "GET", null, "SNI changed successfully!",null);
sendRequest(url, "GET", null, "SNI changed successfully!", "#sni_change");
});
}
function saveIP() {
if (!validateForm('change_ip')) return;
confirmAction("save the new IP", function () {
const ipv4 = $("#ipv4").val().trim() || null;
const ipv6 = $("#ipv6").val().trim() || null;
confirmAction("save the new IP settings", function () {
sendRequest(
"{{ url_for('edit_ip_api') }}",
"POST",
{
ipv4: $("#ipv4").val() || null,
ipv6: $("#ipv6").val() || null
},
"New IP saved successfully!",
null
{ ipv4: ipv4, ipv6: ipv6 },
"IP settings saved successfully!",
"#ip_change"
);
});
}
function downloadBackup() {
window.location.href = "{{ url_for('backup_api') }}";
Swal.fire("Starting Download", "Your backup download should start shortly.", "info");
}
function uploadBackup() {
var fileInput = document.getElementById('backup_file');
var file = fileInput.files[0];
if (!file) {
Swal.fire("Error!", "Please select a file to upload.", "error");
return;
}
if (file.name.split('.').pop() !== 'zip') {
Swal.fire("Error!", "Only zip file.", "error");
if (!file.name.toLowerCase().endsWith('.zip')) {
Swal.fire("Error!", "Only .zip files are allowed for restore.", "error");
return;
}
confirmAction("upload the backup", function() {
confirmAction(`restore the system from the selected backup file (${file.name})`, function() {
var formData = new FormData();
formData.append('file', file);
@ -711,6 +713,7 @@
progressBar.style.width = '0%';
progressBar.setAttribute('aria-valuenow', 0);
statusDiv.innerText = 'Uploading...';
statusDiv.className = 'mt-2';
$.ajax({
url: "{{ url_for('restore_api') }}",
@ -722,34 +725,41 @@
var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function(evt) {
if (evt.lengthComputable) {
var percentComplete = (evt.loaded / evt.total) * 100;
var percentComplete = Math.round((evt.loaded / evt.total) * 100);
progressBar.style.width = percentComplete + '%';
progressBar.setAttribute('aria-valuenow', percentComplete);
statusDiv.innerText = `Uploading... ${percentComplete}%`;
}
}, false);
return xhr;
},
success: function(response) {
statusDiv.innerText = 'Backup restored successfully!';
progressBar.style.width = '100%';
progressBar.classList.add('bg-success');
statusDiv.innerText = 'Backup restored successfully! Reloading page...';
statusDiv.className = 'mt-2 text-success';
Swal.fire("Success!", "Backup restored successfully!", "success").then(() => {
location.reload();
});
console.log(response);
console.log("Restore Success:", response);
},
error: function(xhr, status, error) {
statusDiv.innerText = 'Error restoring backup.';
progressBar.classList.add('bg-danger');
let detail = (xhr.responseJSON && xhr.responseJSON.detail) ? xhr.responseJSON.detail : 'Check console for details.';
statusDiv.innerText = `Error restoring backup: ${detail}`;
statusDiv.className = 'mt-2 text-danger';
Swal.fire("Error!", xhr.responseJSON.detail, "error");
console.error(error);
Swal.fire("Error!", `Failed to restore backup. ${detail}`, "error");
console.error("Restore Error:", status, error, xhr.responseText);
},
complete: function() {
setTimeout(function(){ progressContainer.style.display = 'none'; }, 1000);
fileInput.value = '';
}
});
});
}
function startIPLimit() {
confirmAction("start the IP Limit service", function () {
sendRequest(
"{{ url_for('start_ip_limit_api') }}",
"POST",
@ -757,37 +767,41 @@
"IP Limit service started successfully!",
"#ip_limit_start"
);
});
}
function stopIPLimit() {
confirmAction("stop the IP Limit service", function () {
sendRequest(
"{{ url_for('stop_ip_limit_api') }}",
"POST",
null,
"IP Limit service stopped successfully!",
null
"#ip_limit_stop"
);
});
}
function configIPLimit() {
if (!validateForm('ip_limit_config')) return;
const blockDuration = $("#block_duration").val();
const maxIps = $("#max_ips").val();
confirmAction("save the IP Limit configuration", function () {
sendRequest(
"{{ url_for('config_ip_limit_api') }}",
"POST",
{ block_duration: blockDuration, max_ips: maxIps },
{ block_duration: parseInt(blockDuration), max_ips: parseInt(maxIps) },
"IP Limit configuration saved successfully!",
"#ip_limit_change_config",
false
);
});
}
$("#telegram_start").on("click", startTelegram);
$("#telegram_stop").on("click", stopTelegram);
// $("#singbox_start").on("click", startSingbox); // singbox removed
// $("#singbox_stop").on("click", stopSingbox); // singbox removed
$("#normal_start").on("click", startNormal);
$("#normal_stop").on("click", stopNormal);
$("#port_change").on("click", changePort);
@ -798,27 +812,33 @@
$("#ip_limit_start").on("click", startIPLimit);
$("#ip_limit_stop").on("click", stopIPLimit);
$("#ip_limit_change_config").on("click", configIPLimit);
$("#decoy_setup").on("click", setupDecoy);
$("#decoy_stop").on("click", stopDecoy);
// $('#singbox_domain, #normal_domain, #sni_domain').on('input', function () { // singbox removed
$('#normal_domain, #sni_domain').on('input', function () {
$('#normal_domain, #sni_domain, #decoy_domain').on('input', function () {
if (isValidDomain($(this).val())) {
$(this).removeClass('is-invalid');
} else {
} else if ($(this).val().trim() !== "") {
$(this).addClass('is-invalid');
} else {
$(this).removeClass('is-invalid');
}
});
// $('#singbox_port, #normal_port, #hysteria_port').on('input', function () { // singbox removed
$('#normal_port, #hysteria_port').on('input', function () {
if (isValidPort($(this).val())) {
$(this).removeClass('is-invalid');
} else {
} else if ($(this).val().trim() !== "") {
$(this).addClass('is-invalid');
} else {
$(this).removeClass('is-invalid');
}
});
$('#ipv4, #ipv6').on('input', function () {
if (isValidIPorDomain($(this).val())) {
if (isValidIPorDomain($(this).val()) || $(this).val().trim() === '') {
$(this).removeClass('is-invalid');
} else {
$(this).addClass('is-invalid');
@ -832,13 +852,28 @@
$(this).addClass('is-invalid');
}
});
$('#block_duration, #max_ips').on('input', function () {
if (isValidPositiveNumber($(this).val())) {
$(this).removeClass('is-invalid');
} else {
} else if ($(this).val().trim() !== "") {
$(this).addClass('is-invalid');
} else {
$(this).removeClass('is-invalid');
}
});
$('#decoy_path').on('input', function () {
if (isValidPath($(this).val())) {
$(this).removeClass('is-invalid');
} else if ($(this).val().trim() !== "") {
$(this).addClass('is-invalid');
} else {
$(this).removeClass('is-invalid');
}
});
});
</script>
{% endblock %}

View File

@ -33,6 +33,7 @@ install_dependencies() {
echo -e "${green}Caddy installed successfully. ${NC}"
}
update_env_file() {
local domain=$1
local port=$2
@ -41,6 +42,7 @@ update_env_file() {
local admin_password_hash=$(echo -n "$admin_password" | sha256sum | cut -d' ' -f1) # hash the password
local expiration_minutes=$5
local debug=$6
local decoy_path=$7
local api_token=$(openssl rand -hex 32)
local root_path=$(openssl rand -hex 16)
@ -55,6 +57,10 @@ ADMIN_USERNAME=$admin_username
ADMIN_PASSWORD=$admin_password_hash
EXPIRATION_MINUTES=$expiration_minutes
EOL
if [ -n "$decoy_path" ]; then
echo "DECOY_PATH=$decoy_path" >> /etc/hysteria/core/scripts/webpanel/.env
fi
}
update_caddy_file() {
@ -66,7 +72,39 @@ update_caddy_file() {
return 1
fi
# Update the Caddyfile without the email directive
if [ -n "$DECOY_PATH" ] && [ "$PORT" -eq 443 ]; then
cat <<EOL > "$CADDY_CONFIG_FILE"
# Global configuration
{
# Disable admin panel of the Caddy
admin off
# Disable automatic HTTP to HTTPS redirects so the Caddy won't listen on port 80 (We need this port for other parts of the project)
auto_https disable_redirects
}
# Listen for incoming requests on the specified domain and port
$DOMAIN:$PORT {
# Define a route to handle all requests starting with ROOT_PATH('/$ROOT_PATH/')
route /$ROOT_PATH/* {
# We don't strip the ROOT_PATH('/$ROOT_PATH/') from the request
# uri strip_prefix /$ROOT_PATH
# We are proxying all requests under the ROOT_PATH to FastAPI at 127.0.0.1:28260
# FastAPI handles these requests because we set the 'root_path' parameter in the FastAPI instance.
reverse_proxy http://127.0.0.1:28260
}
@otherPaths {
not path /$ROOT_PATH/*
}
handle @otherPaths {
root * $DECOY_PATH
file_server
}
}
EOL
else
cat <<EOL > "$CADDY_CONFIG_FILE"
# Global configuration
{
@ -97,8 +135,19 @@ $DOMAIN:$PORT {
abort @blocked
}
EOL
}
if [ -n "$DECOY_PATH" ] && [ "$PORT" -ne 443 ]; then
cat <<EOL >> "$CADDY_CONFIG_FILE"
# Decoy site on port 443
$DOMAIN:443 {
root * $DECOY_PATH
file_server
}
EOL
fi
fi
}
create_webpanel_service_file() {
cat <<EOL > /etc/systemd/system/hysteria-webpanel.service
@ -147,21 +196,13 @@ start_service() {
local admin_password=$4
local expiration_minutes=$5
local debug=$6
# MAYBE I WANT TO CHANGE CONFIGS WITHOUT RESTARTING THE SERVICE MYSELF
# # Check if the services are already active
# if systemctl is-active --quiet hysteria-webpanel.service && systemctl is-active --quiet hysteria-caddy.service; then
# echo -e "${green}Hysteria web panel is already running with Caddy.${NC}"
# source /etc/hysteria/core/scripts/webpanel/.env
# echo -e "${yellow}The web panel is accessible at: http://$domain:$port/$ROOT_PATH${NC}"
# return
# fi
local decoy_path=$7
# Install required dependencies
install_dependencies
# Update environment file
update_env_file "$domain" "$port" "$admin_username" "$admin_password" "$expiration_minutes" "$debug"
update_env_file "$domain" "$port" "$admin_username" "$admin_password" "$expiration_minutes" "$debug" "$decoy_path"
if [ $? -ne 0 ]; then
echo -e "${red}Error: Failed to update the environment file.${NC}"
return 1
@ -214,11 +255,116 @@ start_service() {
source /etc/hysteria/core/scripts/webpanel/.env
local webpanel_url="http://$domain:$port/$ROOT_PATH/"
echo -e "${green}Hysteria web panel is now running. The service is accessible on: $webpanel_url ${NC}"
if [ -n "$decoy_path" ]; then
if [ "$port" -eq 443 ]; then
echo -e "${green}Decoy site is configured on the same port (443) and will handle non-webpanel paths.${NC}"
else
echo -e "${green}Decoy site is configured on port 443 at: http://$domain:443/${NC}"
fi
fi
else
echo -e "${red}Error: Hysteria web panel failed to start after Caddy restart.${NC}"
fi
}
setup_decoy_site() {
local domain=$1
local decoy_path=$2
if [ -z "$domain" ] || [ -z "$decoy_path" ]; then
echo -e "${red}Usage: $0 decoy <DOMAIN> <PATH_TO_DECOY_SITE>${NC}"
return 1
fi
if [ ! -d "$decoy_path" ]; then
echo -e "${yellow}Warning: Decoy site path does not exist. Creating directory...${NC}"
mkdir -p "$decoy_path"
echo "<html><body><h1>Website Under Construction</h1></body></html>" > "$decoy_path/index.html"
fi
if [ -f "/etc/hysteria/core/scripts/webpanel/.env" ]; then
source /etc/hysteria/core/scripts/webpanel/.env
sed -i "/DECOY_PATH=/d" /etc/hysteria/core/scripts/webpanel/.env
echo "DECOY_PATH=$decoy_path" >> /etc/hysteria/core/scripts/webpanel/.env
update_caddy_file
systemctl restart hysteria-caddy.service
echo -e "${green}Decoy site configured successfully for $domain${NC}"
if [ "$PORT" -eq 443 ]; then
echo -e "${green}Decoy site is accessible at non-webpanel paths on: https://$domain:443/${NC}"
else
echo -e "${green}Decoy site is accessible at: https://$domain:443/${NC}"
fi
else
echo -e "${red}Error: Web panel is not configured yet. Please start the web panel first.${NC}"
return 1
fi
}
stop_decoy_site() {
if [ ! -f "/etc/hysteria/core/scripts/webpanel/.env" ]; then
echo -e "${red}Error: Web panel is not configured.${NC}"
return 1
fi
source /etc/hysteria/core/scripts/webpanel/.env
if [ -z "$DECOY_PATH" ]; then
echo -e "${yellow}No decoy site is currently configured.${NC}"
return 0
fi
local was_separate_port=false
if [ "$PORT" -ne 443 ]; then
was_separate_port=true
fi
sed -i "/DECOY_PATH=/d" /etc/hysteria/core/scripts/webpanel/.env
cat <<EOL > "$CADDY_CONFIG_FILE"
# Global configuration
{
# Disable admin panel of the Caddy
admin off
# Disable automatic HTTP to HTTPS redirects so the Caddy won't listen on port 80 (We need this port for other parts of the project)
auto_https disable_redirects
}
# Listen for incoming requests on the specified domain and port
$DOMAIN:$PORT {
# Define a route to handle all requests starting with ROOT_PATH('/$ROOT_PATH/')
route /$ROOT_PATH/* {
# We don't strip the ROOT_PATH('/$ROOT_PATH/') from the request
# uri strip_prefix /$ROOT_PATH
# We are proxying all requests under the ROOT_PATH to FastAPI at 127.0.0.1:28260
# FastAPI handles these requests because we set the 'root_path' parameter in the FastAPI instance.
reverse_proxy http://127.0.0.1:28260
}
# Any request that doesn't start with the ROOT_PATH('/$ROOT_PATH/') will be blocked and no response will be sent to the client
@blocked {
not path /$ROOT_PATH/*
}
# Abort the request, effectively dropping the connection without a response for invalid paths
abort @blocked
}
EOL
systemctl restart hysteria-caddy.service
echo -e "${green}Decoy site has been stopped and removed from configuration.${NC}"
if [ "$was_separate_port" = true ]; then
echo -e "${green}Port 443 is no longer served by Caddy.${NC}"
else
echo -e "${green}Non-webpanel paths on port 443 will now return connection errors instead of serving the decoy site.${NC}"
fi
}
show_webpanel_url() {
source /etc/hysteria/core/scripts/webpanel/.env
local webpanel_url="https://$DOMAIN:$PORT/$ROOT_PATH/"
@ -240,19 +386,33 @@ stop_service() {
systemctl disable hysteria-webpanel.service
systemctl stop hysteria-webpanel.service
echo "Hysteria web panel stopped."
systemctl daemon-reload
rm /etc/hysteria/core/scripts/webpanel/.env
rm "$CADDY_CONFIG_FILE"
}
case "$1" in
start)
if [ -z "$2" ] || [ -z "$3" ]; then
echo -e "${red}Usage: $0 start <DOMAIN> <PORT> ${NC}"
echo -e "${red}Usage: $0 start <DOMAIN> <PORT> [ADMIN_USERNAME] [ADMIN_PASSWORD] [EXPIRATION_MINUTES] [DEBUG] [DECOY_PATH]${NC}"
exit 1
fi
start_service "$2" "$3" "$4" "$5" "$6" "$7"
start_service "$2" "$3" "$4" "$5" "$6" "$7" "$8"
;;
stop)
stop_service
;;
decoy)
if [ -z "$2" ] || [ -z "$3" ]; then
echo -e "${red}Usage: $0 decoy <DOMAIN> <PATH_TO_DECOY_SITE>${NC}"
exit 1
fi
setup_decoy_site "$2" "$3"
;;
stopdecoy)
stop_decoy_site
;;
url)
show_webpanel_url
;;
@ -260,9 +420,13 @@ case "$1" in
show_webpanel_api_token
;;
*)
echo -e "${red}Usage: $0 {start|stop} <DOMAIN> <PORT> ${NC}"
echo -e "${red}Usage: $0 {start|stop|decoy|stopdecoy|url|api-token} [options]${NC}"
echo -e "${yellow}start <DOMAIN> <PORT> [ADMIN_USERNAME] [ADMIN_PASSWORD] [EXPIRATION_MINUTES] [DEBUG] [DECOY_PATH]${NC}"
echo -e "${yellow}stop${NC}"
echo -e "${yellow}decoy <DOMAIN> <PATH_TO_DECOY_SITE>${NC}"
echo -e "${yellow}stopdecoy${NC}"
echo -e "${yellow}url${NC}"
echo -e "${yellow}api-token${NC}"
exit 1
;;
esac