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 ### Changed
- feat: Add init_paths to handle sys.path setup for importing shared modules 🛡️ feat: Add Decoy Site feature
- feat: Add insecure parameter to generate_uri() function 🖥️ feat: Add Decoy status API for web panel integration
- feat: Add SNI checker and certificate manager 🔧 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('--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-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('--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) @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: try:
if action == 'start': if action == 'start':
if not domain or not port or not admin_username or not admin_password: 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.') 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() services_status = cli_api.get_services_status()
if not 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() url = cli_api.get_webpanel_url()
click.echo(f'Hysteria web panel is now running. The service is accessible on: {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': elif action == 'stop':
if decoy_path:
click.echo('Warning: --decoy-path option is ignored for the stop action.', err=True)
cli_api.stop_webpanel() cli_api.stop_webpanel()
click.echo(f'WebPanel stopped successfully.') click.echo(f'WebPanel stopped successfully.')
except Exception as e: except Exception as e:
click.echo(f'{e}', err=True) 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') @cli.command('get-webpanel-url')
def get_web_panel_url(): def get_web_panel_url():

View File

@ -12,7 +12,7 @@ DEBUG = False
SCRIPT_DIR = '/etc/hysteria/core/scripts' SCRIPT_DIR = '/etc/hysteria/core/scripts'
CONFIG_FILE = '/etc/hysteria/config.json' CONFIG_FILE = '/etc/hysteria/config.json'
CONFIG_ENV_FILE = '/etc/hysteria/.configs.env' CONFIG_ENV_FILE = '/etc/hysteria/.configs.env'
WEBPANEL_ENV_FILE = '/etc/hysteria/core/scripts/webpanel/.env'
class Command(Enum): class Command(Enum):
'''Contains path to command's script''' '''Contains path to command's script'''
@ -507,13 +507,13 @@ def stop_normalsub():
run_cmd(['bash', Command.INSTALL_NORMALSUB.value, 'stop']) 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.''' '''Starts WebPanel.'''
if not domain or not port or not admin_username or not admin_password or not expiration_minutes: 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.') raise InvalidInputError('Error: Both --domain and --port are required for the start action.')
run_cmd( run_cmd(
['bash', Command.SHELL_WEBPANEL.value, 'start', ['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.''' '''Stops WebPanel.'''
run_cmd(['bash', Command.SHELL_WEBPANEL.value, 'stop']) 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: def get_webpanel_url() -> str | None:
'''Gets the URL of WebPanel.''' '''Gets the URL of WebPanel.'''

View File

@ -10,6 +10,7 @@ else
echo "Error: Config file $CONFIG_ENV not found." echo "Error: Config file $CONFIG_ENV not found."
exit 1 exit 1
fi fi
update_sni() { update_sni() {
local sni=$1 local sni=$1
local server_ip local server_ip
@ -21,10 +22,21 @@ update_sni() {
fi fi
if [ -n "$IP4" ]; then if [ -n "$IP4" ]; then
if [[ $IP4 =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
server_ip="$IP4" server_ip="$IP4"
echo "Using server IP from config: $server_ip" echo "Using server IP from config: $server_ip"
else 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" echo "Using auto-detected server IP: $server_ip"
fi fi

View File

@ -61,7 +61,6 @@ for file in "${expected_files[@]}"; do
fi fi
done done
timestamp=$(date +%Y%m%d_%H%M%S) timestamp=$(date +%Y%m%d_%H%M%S)
existing_backup_dir="/opt/hysbackup/restore_pre_backup_$timestamp" existing_backup_dir="/opt/hysbackup/restore_pre_backup_$timestamp"
mkdir -p "$existing_backup_dir" mkdir -p "$existing_backup_dir"
@ -85,11 +84,58 @@ for file in "${expected_files[@]}"; do
fi fi
done 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" 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 chown hysteria:hysteria /etc/hysteria/ca.key /etc/hysteria/ca.crt
chmod 640 /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 python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
echo "Error: Restart service failed'." echo "Error: Restart service failed'."

View File

@ -3,16 +3,39 @@
source /etc/hysteria/core/scripts/path.sh source /etc/hysteria/core/scripts/path.sh
if systemctl is-active --quiet wg-quick@wgcf.service; then 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..."
else
echo "Installing WARP..."
bash <(curl -fsSL https://raw.githubusercontent.com/ReturnFI/Warp/main/warp.sh) wgx
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 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" 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 python3 "$CLI_PATH" restart-hysteria2 > /dev/null 2>&1
echo "WARP installed and outbound added to config.json." echo "WARP outbound added to config.json."
else else
echo "Error: Config file $CONFIG_FILE not found." echo "Error: Config file $CONFIG_FILE not found."
fi fi
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 "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 API_TOKEN: str
EXPIRATION_MINUTES: int EXPIRATION_MINUTES: int
ROOT_PATH: str ROOT_PATH: str
DECOY_PATH: str | None = None
class Config: class Config:
env_file = '.env' 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.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 from fastapi.responses import FileResponse
import shutil import shutil
import zipfile import zipfile
@ -334,3 +334,54 @@ async def config_ip_limit_api(config: IPLimitConfig):
return DetailResponse(detail=details) return DetailResponse(detail=details)
except Exception as e: except Exception as e:
raise HTTPException(status_code=400, detail=f'Error configuring IP Limiter: {str(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 typing import Optional
from pydantic import BaseModel from pydantic import BaseModel, Field
class DetailResponse(BaseModel): class DetailResponse(BaseModel):
@ -8,3 +8,11 @@ class DetailResponse(BaseModel):
class IPLimitConfig(BaseModel): class IPLimitConfig(BaseModel):
block_duration: Optional[int] = None block_duration: Optional[int] = None
max_ips: 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'> <li class='nav-item'>
<a class='nav-link' id='telegram-tab' data-toggle='pill' href='#telegram' role='tab' <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> aria-controls='telegram' aria-selected='true'><i class="fab fa-telegram"></i>
Telegram Telegram Bot</a>
Bot</a>
</li> </li>
<li class='nav-item'> <li class='nav-item'>
<a class='nav-link' id='port-tab' data-toggle='pill' href='#port' role='tab' <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> aria-controls='change_ip' aria-selected='false'><i class="fas fa-network-wired"></i>
Change IP</a> Change IP</a>
</li> </li>
<!-- New Backup Tab -->
<li class='nav-item'> <li class='nav-item'>
<a class='nav-link' id='backup-tab' data-toggle='pill' href='#backup' role='tab' <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> aria-controls='backup' aria-selected='false'><i class="fas fa-download"></i>
Backup</a> Backup</a>
</li> </li>
<!-- IP Limiter Tab -->
<li class='nav-item'> <li class='nav-item'>
<a class='nav-link' id='ip-limit-tab' data-toggle='pill' href='#ip-limit' role='tab' <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> aria-controls='ip-limit' aria-selected='false'><i class="fas fa-user-slash"></i>
IP Limit</a> IP Limit</a>
</li> </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> </ul>
</div> </div>
<div class='card-body' style="margin-left: 25px;"> <div class='card-body' style="margin-left: 25px;">
@ -74,35 +76,6 @@
</ul> </ul>
<div class='tab-content' id='subs-tabs-content'> <div class='tab-content' id='subs-tabs-content'>
<br> <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 --> <!-- Normal Sub Tab -->
<div class='tab-pane fade show active' id='normal' role='tabpanel' aria-labelledby='normal-tab'> <div class='tab-pane fade show active' id='normal' role='tabpanel' aria-labelledby='normal-tab'>
<form id="normal"> <form id="normal">
@ -134,7 +107,6 @@
</div> </div>
</div> </div>
<!-- Telegram Bot Tab --> <!-- Telegram Bot Tab -->
<div class='tab-pane fade' id='telegram' role='tabpanel' aria-labelledby='telegram-tab'> <div class='tab-pane fade' id='telegram' role='tabpanel' aria-labelledby='telegram-tab'>
<form id="telegram"> <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> <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' <button id="telegram_stop" type='button' class='btn btn-danger'
style="display: none;">Stop</button> style="display: none;">Stop</button>
</form> </form>
</div> </div>
<!-- Port Tab --> <!-- Port Tab -->
<div class='tab-pane fade' id='port' role='tabpanel' aria-labelledby='port-tab'> <div class='tab-pane fade' id='port' role='tabpanel' aria-labelledby='port-tab'>
<form id="port"> <form id="port">
<div class='form-group'> <div class='form-group'>
<label for='hysteria_port'>Port:</label> <label for='hysteria_port'>Port:</label>
@ -180,7 +150,6 @@
<!-- SNI Tab --> <!-- SNI Tab -->
<div class='tab-pane fade' id='sni' role='tabpanel' aria-labelledby='sni-tab'> <div class='tab-pane fade' id='sni' role='tabpanel' aria-labelledby='sni-tab'>
<form id="sni"> <form id="sni">
<div class='form-group'> <div class='form-group'>
<label for='sni_domain'>Domain:</label> <label for='sni_domain'>Domain:</label>
@ -199,24 +168,25 @@
<form id="change_ip"> <form id="change_ip">
<div class='form-group'> <div class='form-group'>
<label for='ipv4'>IPv4:</label> <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 '' }}"> value="{{ ipv4 or '' }}">
<div class="invalid-feedback"> <div class="invalid-feedback">
Please enter a valid IPv4 address. Please enter a valid IPv4 address or Domain.
</div> </div>
</div> </div>
<div class='form-group'> <div class='form-group'>
<label for='ipv6'>IPv6:</label> <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 '' }}"> value="{{ ipv6 or '' }}">
<div class="invalid-feedback"> <div class="invalid-feedback">
Please enter a valid IPv6 address. Please enter a valid IPv6 address or Domain.
</div> </div>
</div> </div>
<button id="ip_change" type='button' class='btn btn-primary'>Save</button> <button id="ip_change" type='button' class='btn btn-primary'>Save</button>
</form> </form>
</div> </div>
<!-- Backup Tab (New) -->
<!-- Backup Tab -->
<div class='tab-pane fade' id='backup' role='tabpanel' aria-labelledby='backup-tab'> <div class='tab-pane fade' id='backup' role='tabpanel' aria-labelledby='backup-tab'>
<div class="form-group"> <div class="form-group">
<label for="backup_file">Upload Backup:</label> <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 id="backup_progress_bar" class="progress-bar" role="progressbar" style="width: 0%;" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100"></div>
</div> </div>
<div id="backup_status" class="mt-2"></div> <!-- Status messages --> <div id="backup_status" class="mt-2"></div>
</div> </div>
<!-- IP Limit Tab (New) --> <!-- IP Limit Tab -->
<div class='tab-pane fade' id='ip-limit' role='tabpanel' aria-labelledby='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'> <ul class='nav nav-tabs' id='ip-limit-tabs' role='tablist'>
<li class='nav-item'> <li class='nav-item'>
@ -284,6 +254,40 @@
</div> </div>
</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>
</div> </div>
<!-- /.card --> <!-- /.card -->
@ -298,15 +302,20 @@
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script> <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></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" <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==" integrity="sha512-yFjZbTYRCJodnuyGlsKamNE/LlEaEA/3apsIOPr7/l+jCMq9Dn9x5qyuAGqgpr4/NBZ95p8yrl/sLhJvoazg=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script> crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script> <script>
$(document).ready(function () { $(document).ready(function () {
initUI(); initUI();
fetchDecoyStatus();
function isValidPath(path) {
if (!path) return false;
return path.trim() !== '';
}
function isValidDomain(domain) { function isValidDomain(domain) {
if (!domain) return false; if (!domain) return false;
@ -318,12 +327,13 @@
if (!port) return false; if (!port) return false;
return /^[0-9]+$/.test(port) && parseInt(port) > 0 && parseInt(port) <= 65535; return /^[0-9]+$/.test(port) && parseInt(port) > 0 && parseInt(port) <= 65535;
} }
function isValidIPorDomain(input) { function isValidIPorDomain(input) {
if (!input) return true; 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 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 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 domainRegex = /^(?!-)(?:[a-zA-Z\d-]{0,62}[a-zA-Z\d]\.){1,126}(?!\d+$)[a-zA-Z\d]{1,63}$/;
const lowerInput = input.toLowerCase(); const lowerInput = input.toLowerCase();
if (ipV4Regex.test(input)) return true; if (ipV4Regex.test(input)) return true;
@ -338,7 +348,6 @@
return /^[0-9]+$/.test(value) && parseInt(value) > 0; return /^[0-9]+$/.test(value) && parseInt(value) > 0;
} }
function confirmAction(actionName, callback) { function confirmAction(actionName, callback) {
Swal.fire({ Swal.fire({
title: `Are you sure?`, title: `Are you sure?`,
@ -369,14 +378,14 @@
} }
}, },
success: function (response) { success: function (response) {
Swal.fire("Success!", successMessage, "success");
if (showReload) { if (showReload) {
Swal.fire("Success!", successMessage, "success").then(() => { Swal.fire("Success!", successMessage, "success").then(() => {
location.reload(); location.reload();
}); });
} else {
Swal.fire("Success!", successMessage, "success");
} }
console.log("Success Response:", response);
console.log(response);
}, },
error: function (xhr, status, error) { error: function (xhr, status, error) {
let errorMessage = "Something went wrong."; let errorMessage = "Something went wrong.";
@ -384,7 +393,7 @@
errorMessage = xhr.responseJSON.detail; errorMessage = xhr.responseJSON.detail;
} }
Swal.fire("Error!", errorMessage, "error"); Swal.fire("Error!", errorMessage, "error");
console.error(error); console.error("AJAX Error:", status, error, xhr.responseText);
}, },
complete: function() { complete: function() {
if (buttonSelector) { if (buttonSelector) {
@ -400,47 +409,28 @@
$(`#${formId} .form-control`).each(function () { $(`#${formId} .form-control`).each(function () {
const input = $(this); const input = $(this);
const id = input.attr('id'); const id = input.attr('id');
let fieldValid = true;
if (id.includes('domain')) { if (id.includes('domain')) {
if (!isValidDomain(input.val())) { fieldValid = isValidDomain(input.val());
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
} else if (id.includes('port')) { } else if (id.includes('port')) {
if (!isValidPort(input.val())) { fieldValid = isValidPort(input.val());
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
} else if (id === 'ipv4' || id === 'ipv6') { } else if (id === 'ipv4' || id === 'ipv6') {
if (!isValidIPorDomain(input.val())) { fieldValid = (input.val().trim() === '') ? true : isValidIPorDomain(input.val());
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
} else if (id === 'block_duration' || id === 'max_ips') { } 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'); input.addClass('is-invalid');
isValid = false; isValid = false;
} else { } else {
input.removeClass('is-invalid'); input.removeClass('is-invalid');
} }
}
else {
if (!input.val().trim()) {
input.addClass('is-invalid');
isValid = false;
} else {
input.removeClass('is-invalid');
}
}
}); });
return isValid; return isValid;
} }
@ -452,94 +442,21 @@
success: function (data) { success: function (data) {
updateServiceUI(data); updateServiceUI(data);
}, },
error: function () { error: function (xhr, status, error) {
console.error("Failed to fetch service status."); 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({ $.ajax({
url: "{{ url_for('get_ip_api') }}", url: "{{ url_for('get_ip_api') }}",
type: "GET", type: "GET",
success: function (data) { success: function (data) {
$("#ipv4").val(data.ipv4 || ""); $("#ipv4").val(data.ipv4 || "");
$("#ipv6").val(data.ipv6 || ""); $("#ipv6").val(data.ipv6 || "");
$("#ipv4").attr("placeholder", "Enter IPv4 or Domain");
$("#ipv6").attr("placeholder", "Enter IPv6 or Domain");
}, },
error: function () { error: function (xhr, status, error) {
console.error("Failed to fetch IP addresses."); console.error("Failed to fetch IP addresses:", error, xhr.responseText);
$("#ipv4").attr("placeholder", "Enter IPv4 or Domain");
$("#ipv6").attr("placeholder", "Enter IPv6 or Domain");
} }
}); });
@ -549,8 +466,8 @@
success: function (data) { success: function (data) {
$("#hysteria_port").val(data.port || ""); $("#hysteria_port").val(data.port || "");
}, },
error: function () { error: function (xhr, status, error) {
console.error("Failed to fetch port."); console.error("Failed to fetch port:", error, xhr.responseText);
} }
}); });
@ -560,11 +477,123 @@
success: function (data) { success: function (data) {
$("#sni_domain").val(data.sni || ""); $("#sni_domain").val(data.sni || "");
}, },
error: function () { error: function (xhr, status, error) {
console.error("Failed to fetch SNI domain."); 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() { function startTelegram() {
if (!validateForm('telegram')) return; if (!validateForm('telegram')) return;
const apiToken = $("#telegram_api_token").val(); const apiToken = $("#telegram_api_token").val();
@ -587,38 +616,11 @@
"DELETE", "DELETE",
null, null,
"Telegram bot stopped successfully!", "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() { function startNormal() {
if (!validateForm('normal')) return; if (!validateForm('normal')) return;
const domain = $("#normal_domain").val(); const domain = $("#normal_domain").val();
@ -641,8 +643,7 @@
"DELETE", "DELETE",
null, null,
"Normal subscription stopped successfully!", "Normal subscription stopped successfully!",
null "#normal_stop"
); );
}); });
} }
@ -653,7 +654,7 @@
const baseUrl = "{{ url_for('set_port_api', port='PORT_PLACEHOLDER') }}"; const baseUrl = "{{ url_for('set_port_api', port='PORT_PLACEHOLDER') }}";
const url = baseUrl.replace("PORT_PLACEHOLDER", port); const url = baseUrl.replace("PORT_PLACEHOLDER", port);
confirmAction("change the port", function () { 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 baseUrl = "{{ url_for('set_sni_api', sni='SNI_PLACEHOLDER') }}";
const url = baseUrl.replace("SNI_PLACEHOLDER", domain); const url = baseUrl.replace("SNI_PLACEHOLDER", domain);
confirmAction("change the SNI", function () { confirmAction("change the SNI", function () {
sendRequest(url, "GET", null, "SNI changed successfully!",null); sendRequest(url, "GET", null, "SNI changed successfully!", "#sni_change");
}); });
} }
function saveIP() { function saveIP() {
if (!validateForm('change_ip')) return; 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( sendRequest(
"{{ url_for('edit_ip_api') }}", "{{ url_for('edit_ip_api') }}",
"POST", "POST",
{ { ipv4: ipv4, ipv6: ipv6 },
ipv4: $("#ipv4").val() || null, "IP settings saved successfully!",
ipv6: $("#ipv6").val() || null "#ip_change"
},
"New IP saved successfully!",
null
); );
}); });
} }
function downloadBackup() { function downloadBackup() {
window.location.href = "{{ url_for('backup_api') }}"; window.location.href = "{{ url_for('backup_api') }}";
Swal.fire("Starting Download", "Your backup download should start shortly.", "info");
} }
function uploadBackup() { function uploadBackup() {
var fileInput = document.getElementById('backup_file'); var fileInput = document.getElementById('backup_file');
var file = fileInput.files[0]; var file = fileInput.files[0];
if (!file) { if (!file) {
Swal.fire("Error!", "Please select a file to upload.", "error"); Swal.fire("Error!", "Please select a file to upload.", "error");
return; return;
} }
if (!file.name.toLowerCase().endsWith('.zip')) {
if (file.name.split('.').pop() !== 'zip') { Swal.fire("Error!", "Only .zip files are allowed for restore.", "error");
Swal.fire("Error!", "Only zip file.", "error");
return; return;
} }
confirmAction(`restore the system from the selected backup file (${file.name})`, function() {
confirmAction("upload the backup", function() {
var formData = new FormData(); var formData = new FormData();
formData.append('file', file); formData.append('file', file);
@ -711,6 +713,7 @@
progressBar.style.width = '0%'; progressBar.style.width = '0%';
progressBar.setAttribute('aria-valuenow', 0); progressBar.setAttribute('aria-valuenow', 0);
statusDiv.innerText = 'Uploading...'; statusDiv.innerText = 'Uploading...';
statusDiv.className = 'mt-2';
$.ajax({ $.ajax({
url: "{{ url_for('restore_api') }}", url: "{{ url_for('restore_api') }}",
@ -722,34 +725,41 @@
var xhr = new window.XMLHttpRequest(); var xhr = new window.XMLHttpRequest();
xhr.upload.addEventListener("progress", function(evt) { xhr.upload.addEventListener("progress", function(evt) {
if (evt.lengthComputable) { if (evt.lengthComputable) {
var percentComplete = (evt.loaded / evt.total) * 100; var percentComplete = Math.round((evt.loaded / evt.total) * 100);
progressBar.style.width = percentComplete + '%'; progressBar.style.width = percentComplete + '%';
progressBar.setAttribute('aria-valuenow', percentComplete); progressBar.setAttribute('aria-valuenow', percentComplete);
statusDiv.innerText = `Uploading... ${percentComplete}%`;
} }
}, false); }, false);
return xhr; return xhr;
}, },
success: function(response) { 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'; statusDiv.className = 'mt-2 text-success';
Swal.fire("Success!", "Backup restored successfully!", "success").then(() => { Swal.fire("Success!", "Backup restored successfully!", "success").then(() => {
location.reload(); location.reload();
}); });
console.log(response); console.log("Restore Success:", response);
}, },
error: function(xhr, status, error) { 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'; statusDiv.className = 'mt-2 text-danger';
Swal.fire("Error!", xhr.responseJSON.detail, "error"); Swal.fire("Error!", `Failed to restore backup. ${detail}`, "error");
console.error(error); console.error("Restore Error:", status, error, xhr.responseText);
}, },
complete: function() { complete: function() {
setTimeout(function(){ progressContainer.style.display = 'none'; }, 1000); fileInput.value = '';
} }
}); });
}); });
} }
function startIPLimit() { function startIPLimit() {
confirmAction("start the IP Limit service", function () {
sendRequest( sendRequest(
"{{ url_for('start_ip_limit_api') }}", "{{ url_for('start_ip_limit_api') }}",
"POST", "POST",
@ -757,37 +767,41 @@
"IP Limit service started successfully!", "IP Limit service started successfully!",
"#ip_limit_start" "#ip_limit_start"
); );
});
} }
function stopIPLimit() { function stopIPLimit() {
confirmAction("stop the IP Limit service", function () {
sendRequest( sendRequest(
"{{ url_for('stop_ip_limit_api') }}", "{{ url_for('stop_ip_limit_api') }}",
"POST", "POST",
null, null,
"IP Limit service stopped successfully!", "IP Limit service stopped successfully!",
null "#ip_limit_stop"
); );
});
} }
function configIPLimit() { function configIPLimit() {
if (!validateForm('ip_limit_config')) return; if (!validateForm('ip_limit_config')) return;
const blockDuration = $("#block_duration").val(); const blockDuration = $("#block_duration").val();
const maxIps = $("#max_ips").val(); const maxIps = $("#max_ips").val();
confirmAction("save the IP Limit configuration", function () {
sendRequest( sendRequest(
"{{ url_for('config_ip_limit_api') }}", "{{ url_for('config_ip_limit_api') }}",
"POST", "POST",
{ block_duration: blockDuration, max_ips: maxIps }, { block_duration: parseInt(blockDuration), max_ips: parseInt(maxIps) },
"IP Limit configuration saved successfully!", "IP Limit configuration saved successfully!",
"#ip_limit_change_config", "#ip_limit_change_config",
false false
); );
});
} }
$("#telegram_start").on("click", startTelegram); $("#telegram_start").on("click", startTelegram);
$("#telegram_stop").on("click", stopTelegram); $("#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_start").on("click", startNormal);
$("#normal_stop").on("click", stopNormal); $("#normal_stop").on("click", stopNormal);
$("#port_change").on("click", changePort); $("#port_change").on("click", changePort);
@ -798,27 +812,33 @@
$("#ip_limit_start").on("click", startIPLimit); $("#ip_limit_start").on("click", startIPLimit);
$("#ip_limit_stop").on("click", stopIPLimit); $("#ip_limit_stop").on("click", stopIPLimit);
$("#ip_limit_change_config").on("click", configIPLimit); $("#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())) { if (isValidDomain($(this).val())) {
$(this).removeClass('is-invalid'); $(this).removeClass('is-invalid');
} else { } else if ($(this).val().trim() !== "") {
$(this).addClass('is-invalid'); $(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 () { $('#normal_port, #hysteria_port').on('input', function () {
if (isValidPort($(this).val())) { if (isValidPort($(this).val())) {
$(this).removeClass('is-invalid'); $(this).removeClass('is-invalid');
} else { } else if ($(this).val().trim() !== "") {
$(this).addClass('is-invalid'); $(this).addClass('is-invalid');
} else {
$(this).removeClass('is-invalid');
} }
}); });
$('#ipv4, #ipv6').on('input', function () { $('#ipv4, #ipv6').on('input', function () {
if (isValidIPorDomain($(this).val())) { if (isValidIPorDomain($(this).val()) || $(this).val().trim() === '') {
$(this).removeClass('is-invalid'); $(this).removeClass('is-invalid');
} else { } else {
$(this).addClass('is-invalid'); $(this).addClass('is-invalid');
@ -832,13 +852,28 @@
$(this).addClass('is-invalid'); $(this).addClass('is-invalid');
} }
}); });
$('#block_duration, #max_ips').on('input', function () { $('#block_duration, #max_ips').on('input', function () {
if (isValidPositiveNumber($(this).val())) { if (isValidPositiveNumber($(this).val())) {
$(this).removeClass('is-invalid'); $(this).removeClass('is-invalid');
} else { } else if ($(this).val().trim() !== "") {
$(this).addClass('is-invalid'); $(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> </script>
{% endblock %} {% endblock %}

View File

@ -33,6 +33,7 @@ install_dependencies() {
echo -e "${green}Caddy installed successfully. ${NC}" echo -e "${green}Caddy installed successfully. ${NC}"
} }
update_env_file() { update_env_file() {
local domain=$1 local domain=$1
local port=$2 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 admin_password_hash=$(echo -n "$admin_password" | sha256sum | cut -d' ' -f1) # hash the password
local expiration_minutes=$5 local expiration_minutes=$5
local debug=$6 local debug=$6
local decoy_path=$7
local api_token=$(openssl rand -hex 32) local api_token=$(openssl rand -hex 32)
local root_path=$(openssl rand -hex 16) local root_path=$(openssl rand -hex 16)
@ -55,6 +57,10 @@ ADMIN_USERNAME=$admin_username
ADMIN_PASSWORD=$admin_password_hash ADMIN_PASSWORD=$admin_password_hash
EXPIRATION_MINUTES=$expiration_minutes EXPIRATION_MINUTES=$expiration_minutes
EOL EOL
if [ -n "$decoy_path" ]; then
echo "DECOY_PATH=$decoy_path" >> /etc/hysteria/core/scripts/webpanel/.env
fi
} }
update_caddy_file() { update_caddy_file() {
@ -66,7 +72,39 @@ update_caddy_file() {
return 1 return 1
fi 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" cat <<EOL > "$CADDY_CONFIG_FILE"
# Global configuration # Global configuration
{ {
@ -97,8 +135,19 @@ $DOMAIN:$PORT {
abort @blocked abort @blocked
} }
EOL 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() { create_webpanel_service_file() {
cat <<EOL > /etc/systemd/system/hysteria-webpanel.service cat <<EOL > /etc/systemd/system/hysteria-webpanel.service
@ -147,21 +196,13 @@ start_service() {
local admin_password=$4 local admin_password=$4
local expiration_minutes=$5 local expiration_minutes=$5
local debug=$6 local debug=$6
local decoy_path=$7
# 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
# Install required dependencies # Install required dependencies
install_dependencies install_dependencies
# Update environment file # 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 if [ $? -ne 0 ]; then
echo -e "${red}Error: Failed to update the environment file.${NC}" echo -e "${red}Error: Failed to update the environment file.${NC}"
return 1 return 1
@ -214,11 +255,116 @@ start_service() {
source /etc/hysteria/core/scripts/webpanel/.env source /etc/hysteria/core/scripts/webpanel/.env
local webpanel_url="http://$domain:$port/$ROOT_PATH/" 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}" 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 else
echo -e "${red}Error: Hysteria web panel failed to start after Caddy restart.${NC}" echo -e "${red}Error: Hysteria web panel failed to start after Caddy restart.${NC}"
fi 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() { show_webpanel_url() {
source /etc/hysteria/core/scripts/webpanel/.env source /etc/hysteria/core/scripts/webpanel/.env
local webpanel_url="https://$DOMAIN:$PORT/$ROOT_PATH/" local webpanel_url="https://$DOMAIN:$PORT/$ROOT_PATH/"
@ -240,19 +386,33 @@ stop_service() {
systemctl disable hysteria-webpanel.service systemctl disable hysteria-webpanel.service
systemctl stop hysteria-webpanel.service systemctl stop hysteria-webpanel.service
echo "Hysteria web panel stopped." echo "Hysteria web panel stopped."
systemctl daemon-reload
rm /etc/hysteria/core/scripts/webpanel/.env
rm "$CADDY_CONFIG_FILE"
} }
case "$1" in case "$1" in
start) start)
if [ -z "$2" ] || [ -z "$3" ]; then 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 exit 1
fi fi
start_service "$2" "$3" "$4" "$5" "$6" "$7" start_service "$2" "$3" "$4" "$5" "$6" "$7" "$8"
;; ;;
stop) stop)
stop_service 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) url)
show_webpanel_url show_webpanel_url
;; ;;
@ -260,9 +420,13 @@ case "$1" in
show_webpanel_api_token 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 exit 1
;; ;;
esac esac