diff --git a/changelog b/changelog index f34e965..5ae9294 100644 --- a/changelog +++ b/changelog @@ -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 \ No newline at end of file diff --git a/core/cli.py b/core/cli.py index d5bb261..3440a0d 100644 --- a/core/cli.py +++ b/core/cli.py @@ -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(): diff --git a/core/cli_api.py b/core/cli_api.py index 3e880cf..5219226 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -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.''' diff --git a/core/scripts/hysteria2/change_sni.sh b/core/scripts/hysteria2/change_sni.sh index a00affb..6f835d1 100644 --- a/core/scripts/hysteria2/change_sni.sh +++ b/core/scripts/hysteria2/change_sni.sh @@ -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 - server_ip="$IP4" - echo "Using server IP from config: $server_ip" + if [[ $IP4 =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + server_ip="$IP4" + echo "Using server IP from config: $server_ip" + else + 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 ifconfig.me) + server_ip=$(curl -s -4 ifconfig.me) echo "Using auto-detected server IP: $server_ip" fi diff --git a/core/scripts/hysteria2/restore.sh b/core/scripts/hysteria2/restore.sh index 43e3557..c581103 100644 --- a/core/scripts/hysteria2/restore.sh +++ b/core/scripts/hysteria2/restore.sh @@ -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'." @@ -101,4 +147,4 @@ if [[ "$existing_backup_dir" != "" ]]; then rm -rf "$existing_backup_dir" fi -exit 0 +exit 0 \ No newline at end of file diff --git a/core/scripts/warp/install.sh b/core/scripts/warp/install.sh index 22cbeb0..ecd29e2 100644 --- a/core/scripts/warp/install.sh +++ b/core/scripts/warp/install.sh @@ -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 [ -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 installed and outbound added to config.json." + 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 "Error: Config file $CONFIG_FILE not found." + echo "WARP installation failed." fi -fi +fi \ No newline at end of file diff --git a/core/scripts/webpanel/config/config.py b/core/scripts/webpanel/config/config.py index 3960896..2b48d56 100644 --- a/core/scripts/webpanel/config/config.py +++ b/core/scripts/webpanel/config/config.py @@ -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' diff --git a/core/scripts/webpanel/routers/api/v1/config/hysteria.py b/core/scripts/webpanel/routers/api/v1/config/hysteria.py index 7837892..20535ea 100644 --- a/core/scripts/webpanel/routers/api/v1/config/hysteria.py +++ b/core/scripts/webpanel/routers/api/v1/config/hysteria.py @@ -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 @@ -333,4 +333,55 @@ async def config_ip_limit_api(config: IPLimitConfig): details += f' Max IPs per user: {config.max_ips}.' return DetailResponse(detail=details) except Exception as e: - raise HTTPException(status_code=400, detail=f'Error configuring IP Limiter: {str(e)}') \ No newline at end of file + 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)}') \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/schema/response.py b/core/scripts/webpanel/routers/api/v1/schema/response.py index 9a80bf6..22ebbd2 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/response.py +++ b/core/scripts/webpanel/routers/api/v1/schema/response.py @@ -1,5 +1,5 @@ from typing import Optional -from pydantic import BaseModel +from pydantic import BaseModel, Field class DetailResponse(BaseModel): @@ -7,4 +7,12 @@ class DetailResponse(BaseModel): class IPLimitConfig(BaseModel): block_duration: Optional[int] = None - max_ips: Optional[int] = None \ No newline at end of file + 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") diff --git a/core/scripts/webpanel/templates/settings.html b/core/scripts/webpanel/templates/settings.html index 44a3363..88097bd 100644 --- a/core/scripts/webpanel/templates/settings.html +++ b/core/scripts/webpanel/templates/settings.html @@ -29,8 +29,7 @@ - - +
@@ -74,35 +76,6 @@

- -
@@ -134,7 +107,6 @@
-
@@ -158,13 +130,11 @@ Start -
-
@@ -180,7 +150,6 @@
-
@@ -199,40 +168,41 @@
-
- Please enter a valid IPv4 address. + Please enter a valid IPv4 address or Domain.
-
- Please enter a valid IPv6 address. + Please enter a valid IPv6 address or Domain.
- + +
- + -
+
- +
+ +
+
+
+ + +
+ Please enter a valid domain (without http:// or https://). +
+
+
+ + +
+ Please enter a valid directory path. +
+
+ + +
+ +
+
Decoy Status
+
+
Loading status...
+
+
+
+
@@ -298,15 +302,20 @@ - {% endblock %} \ No newline at end of file diff --git a/core/scripts/webpanel/webpanel_shell.sh b/core/scripts/webpanel/webpanel_shell.sh index 9d232de..629d127 100644 --- a/core/scripts/webpanel/webpanel_shell.sh +++ b/core/scripts/webpanel/webpanel_shell.sh @@ -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,8 +72,40 @@ update_caddy_file() { return 1 fi - # Update the Caddyfile without the email directive - cat < "$CADDY_CONFIG_FILE" + if [ -n "$DECOY_PATH" ] && [ "$PORT" -eq 443 ]; then + cat < "$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 < "$CADDY_CONFIG_FILE" # Global configuration { # Disable admin panel of the Caddy @@ -97,8 +135,19 @@ $DOMAIN:$PORT { abort @blocked } EOL -} + if [ -n "$DECOY_PATH" ] && [ "$PORT" -ne 443 ]; then + cat <> "$CADDY_CONFIG_FILE" + +# Decoy site on port 443 +$DOMAIN:443 { + root * $DECOY_PATH + file_server +} +EOL + fi + fi +} create_webpanel_service_file() { cat < /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 ${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 "

Website Under Construction

" > "$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 < "$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 ${NC}" + echo -e "${red}Usage: $0 start [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 ${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} ${NC}" + echo -e "${red}Usage: $0 {start|stop|decoy|stopdecoy|url|api-token} [options]${NC}" + echo -e "${yellow}start [ADMIN_USERNAME] [ADMIN_PASSWORD] [EXPIRATION_MINUTES] [DEBUG] [DECOY_PATH]${NC}" + echo -e "${yellow}stop${NC}" + echo -e "${yellow}decoy ${NC}" + echo -e "${yellow}stopdecoy${NC}" + echo -e "${yellow}url${NC}" + echo -e "${yellow}api-token${NC}" exit 1 ;; -esac - - +esac \ No newline at end of file