feat: Refactor normalsub to use Caddy as reverse proxy

This commit is contained in:
Whispering Wind
2025-05-27 16:20:02 +03:30
committed by GitHub
parent b2cb712c08
commit 2168080843
2 changed files with 206 additions and 121 deletions

View File

@ -1,5 +1,4 @@
import os import os
import ssl
import json import json
import subprocess import subprocess
import re import re
@ -22,10 +21,10 @@ load_dotenv()
@dataclass @dataclass
class AppConfig: class AppConfig:
domain: str domain: str
cert_file: str external_port: int
key_file: str aiohttp_listen_address: str
port: int aiohttp_listen_port: int
sni_file: str sni_file: str
singbox_template_path: str singbox_template_path: str
hysteria_cli_path: str hysteria_cli_path: str
@ -154,7 +153,6 @@ class Utils:
@staticmethod @staticmethod
def is_valid_url(url: str) -> bool: def is_valid_url(url: str) -> bool:
"""Checks if the given string is a valid URL."""
try: try:
result = urlparse(url) result = urlparse(url)
return all([result.scheme, result.netloc]) return all([result.scheme, result.netloc])
@ -173,7 +171,7 @@ class HysteriaCLI:
stdout, stderr = process.communicate() stdout, stderr = process.communicate()
if process.returncode != 0: if process.returncode != 0:
if "User not found" in stderr: if "User not found" in stderr:
return None # Indicate user not found return None
else: else:
print(f"Hysteria CLI error: {stderr}") print(f"Hysteria CLI error: {stderr}")
raise subprocess.CalledProcessError(process.returncode, command, output=stdout, stderr=stderr) raise subprocess.CalledProcessError(process.returncode, command, output=stdout, stderr=stderr)
@ -185,7 +183,7 @@ class HysteriaCLI:
def get_user_info(self, username: str) -> Optional[UserInfo]: def get_user_info(self, username: str) -> Optional[UserInfo]:
raw_info_str = self._run_command(['get-user', '-u', username]) raw_info_str = self._run_command(['get-user', '-u', username])
if raw_info_str is None: if raw_info_str is None:
return None # User not found return None
try: try:
raw_info = json.loads(raw_info_str) raw_info = json.loads(raw_info_str)
return UserInfo( return UserInfo(
@ -394,22 +392,19 @@ class HysteriaServer:
base_path = f'/{safe_subpath}' base_path = f'/{safe_subpath}'
self.app.router.add_get(f'{base_path}/sub/normal/{{username}}', self.handle) self.app.router.add_get(f'{base_path}/sub/normal/{{username}}', self.handle)
self.app.router.add_get(f'{base_path}/robots.txt', self.robots_handler) self.app.router.add_get(f'{base_path}/robots.txt', self.robots_handler)
self.app.router.add_route('*', f'{base_path}/{{tail:.*}}', self.handle_404_subpath)
self.app.router.add_route('*', f'/{safe_subpath}/{{tail:.*}}', self.handle_404)
# This is handled by self._invalid_endpoint_middleware middleware
# self.app.router.add_route('*', '/{tail:.*}', self.handle_generic_404)
def _load_config(self) -> AppConfig: def _load_config(self) -> AppConfig:
domain = os.getenv('HYSTERIA_DOMAIN', 'localhost') domain = os.getenv('HYSTERIA_DOMAIN', 'localhost')
cert_file = os.getenv('HYSTERIA_CERTFILE') external_port = int(os.getenv('HYSTERIA_PORT', '443'))
key_file = os.getenv('HYSTERIA_KEYFILE') aiohttp_listen_address = os.getenv('AIOHTTP_LISTEN_ADDRESS', '127.0.0.1')
port = int(os.getenv('HYSTERIA_PORT', '3326')) aiohttp_listen_port = int(os.getenv('AIOHTTP_LISTEN_PORT', '28261'))
subpath = os.getenv('SUBPATH', '').strip().strip("/") subpath = os.getenv('SUBPATH', '').strip().strip("/")
if not subpath or not self.is_valid_subpath(subpath):
if not self.is_valid_subpath(subpath):
raise ValueError( raise ValueError(
f"Invalid SUBPATH: '{subpath}'. Subpath must contain only alphanumeric characters, hyphens, and underscores.") f"Invalid or empty SUBPATH: '{subpath}'. Subpath must be non-empty and contain only alphanumeric characters.")
sni_file = '/etc/hysteria/.configs.env' sni_file = '/etc/hysteria/.configs.env'
singbox_template_path = '/etc/hysteria/core/scripts/normalsub/singbox.json' singbox_template_path = '/etc/hysteria/core/scripts/normalsub/singbox.json'
@ -419,9 +414,14 @@ class HysteriaServer:
template_dir = os.path.dirname(__file__) template_dir = os.path.dirname(__file__)
sni = self._load_sni_from_env(sni_file) sni = self._load_sni_from_env(sni_file)
return AppConfig(domain=domain, cert_file=cert_file, key_file=key_file, port=port, sni_file=sni_file, return AppConfig(domain=domain, external_port=external_port,
singbox_template_path=singbox_template_path, hysteria_cli_path=hysteria_cli_path, aiohttp_listen_address=aiohttp_listen_address,
rate_limit=rate_limit, rate_limit_window=rate_limit_window, sni=sni, template_dir=template_dir, aiohttp_listen_port=aiohttp_listen_port,
sni_file=sni_file,
singbox_template_path=singbox_template_path,
hysteria_cli_path=hysteria_cli_path,
rate_limit=rate_limit, rate_limit_window=rate_limit_window,
sni=sni, template_dir=template_dir,
subpath=subpath) subpath=subpath)
def _load_sni_from_env(self, sni_file: str) -> str: def _load_sni_from_env(self, sni_file: str) -> str:
@ -435,26 +435,27 @@ class HysteriaServer:
return "bts.com" return "bts.com"
def is_valid_subpath(self, subpath: str) -> bool: def is_valid_subpath(self, subpath: str) -> bool:
"""Validates the subpath using a regex.""" return bool(re.match(r"^[a-zA-Z0-9]+$", subpath))
return bool(re.match(r"^[a-zA-Z0-9_-]+$", subpath))
def validate_and_escape_subpath(self, subpath: str) -> str: def validate_and_escape_subpath(self, subpath: str) -> str:
"""Validates the subpath and returns the escaped version."""
if not self.is_valid_subpath(subpath): if not self.is_valid_subpath(subpath):
raise ValueError(f"Invalid subpath: {subpath}") raise ValueError(f"Invalid subpath: {subpath}")
return re.escape(subpath) return re.escape(subpath)
@middleware @middleware
async def _rate_limit_middleware(self, request: web.Request, handler): async def _rate_limit_middleware(self, request: web.Request, handler):
client_ip = request.headers.get('X-Forwarded-For', request.headers.get('X-Real-IP', request.remote)) client_ip_hdr = request.headers.get('X-Forwarded-For', request.headers.get('X-Real-IP'))
if not self.rate_limiter.check_limit(client_ip): # type: ignore client_ip = client_ip_hdr.split(',')[0].strip() if client_ip_hdr else request.remote
if client_ip and not self.rate_limiter.check_limit(client_ip):
return web.Response(status=429, text="Rate limit exceeded.") return web.Response(status=429, text="Rate limit exceeded.")
return await handler(request) return await handler(request)
@middleware @middleware
async def _invalid_endpoint_middleware(self, request: web.Request, handler): async def _invalid_endpoint_middleware(self, request: web.Request, handler):
path = f'/{self.config.subpath}/' expected_prefix = f'/{self.config.subpath}/'
if not request.path.startswith(path): if not request.path.startswith(expected_prefix):
print(f"Warning: Request {request.path} reached aiohttp outside expected subpath {expected_prefix}. Closing connection.")
if request.transport is not None: if request.transport is not None:
request.transport.close() request.transport.close()
raise web.HTTPForbidden() raise web.HTTPForbidden()
@ -468,9 +469,11 @@ class HysteriaServer:
async def handle(self, request: web.Request) -> web.Response: async def handle(self, request: web.Request) -> web.Response:
try: try:
username = Utils.sanitize_input(request.match_info.get('username', ''), r'^[a-zA-Z0-9_-]+$') username_raw = request.match_info.get('username', '')
if not username: if not username_raw: # Should not happen due to route def
return web.Response(status=400, text="Error: Missing 'username' parameter.") return web.Response(status=400, text="Error: Missing 'username' parameter.")
username = Utils.sanitize_input(username_raw, r'^[a-zA-Z0-9_-]+$')
user_agent = request.headers.get('User-Agent', '').lower() user_agent = request.headers.get('User-Agent', '').lower()
user_info = self.hysteria_cli.get_user_info(username) user_info = self.hysteria_cli.get_user_info(username)
if user_info is None: if user_info is None:
@ -509,10 +512,12 @@ class HysteriaServer:
async def _get_template_context(self, username: str, user_info: UserInfo) -> TemplateContext: async def _get_template_context(self, username: str, user_info: UserInfo) -> TemplateContext:
ipv4_uri, ipv6_uri = self.hysteria_cli.get_uris(username) ipv4_uri, ipv6_uri = self.hysteria_cli.get_uris(username)
port_str = f":{self.config.external_port}" if self.config.external_port not in [80, 443, 0] else ""
base_url = f"https://{self.config.domain}{port_str}"
base_url = f"https://{self.config.domain}:{self.config.port}"
if not Utils.is_valid_url(base_url): if not Utils.is_valid_url(base_url):
raise ValueError(f"Invalid base URL constructed: {base_url}") print(f"Warning: Constructed base URL '{base_url}' might be invalid. Check domain and port config.")
sub_link = f"{base_url}/{self.config.subpath}/sub/normal/{username}" sub_link = f"{base_url}/{self.config.subpath}/sub/normal/{username}"
ipv4_qrcode = Utils.generate_qrcode_base64(ipv4_uri) ipv4_qrcode = Utils.generate_qrcode_base64(ipv4_uri)
@ -535,23 +540,20 @@ class HysteriaServer:
async def robots_handler(self, request: web.Request) -> web.Response: async def robots_handler(self, request: web.Request) -> web.Response:
return web.Response(text="User-agent: *\nDisallow: /", content_type="text/plain") return web.Response(text="User-agent: *\nDisallow: /", content_type="text/plain")
async def handle_404(self, request: web.Request) -> web.Response: async def handle_404_subpath(self, request: web.Request) -> web.Response:
"""Handles 404 Not Found errors *within* the subpath.""" print(f"404 Not Found (within subpath, unhandled by specific routes): {request.path}")
print(f"404 Not Found (within subpath): {request.path}")
return web.Response(status=404, text="Not Found within Subpath") return web.Response(status=404, text="Not Found within Subpath")
# async def handle_generic_404(self, request: web.Request) -> web.Response:
# """Handles 404 Not Found errors *outside* the subpath."""
# print(f"404 Not Found (generic): {request.path}")
# return web.Response(status=404, text="Not Found")
def run(self): def run(self):
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) print(f"Starting Hysteria Normalsub server on {self.config.aiohttp_listen_address}:{self.config.aiohttp_listen_port}")
ssl_context.load_cert_chain(certfile=self.config.cert_file, keyfile=self.config.key_file) print(f"External access via Caddy should be at https://{self.config.domain}:{self.config.external_port}/{self.config.subpath}/")
ssl_context.set_ciphers('ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384') web.run_app(
web.run_app(self.app, port=self.config.port, ssl_context=ssl_context) self.app,
host=self.config.aiohttp_listen_address,
port=self.config.aiohttp_listen_port
)
if __name__ == '__main__': if __name__ == '__main__':
server = HysteriaServer() server = HysteriaServer()
server.run() server.run()

View File

@ -2,30 +2,100 @@
source /etc/hysteria/core/scripts/utils.sh source /etc/hysteria/core/scripts/utils.sh
define_colors define_colors
CADDY_CONFIG_FILE_NORMALSUB="/etc/hysteria/core/scripts/normalsub/Caddyfile.normalsub"
NORMALSUB_ENV_FILE="/etc/hysteria/core/scripts/normalsub/.env"
DEFAULT_AIOHTTP_LISTEN_ADDRESS="127.0.0.1"
DEFAULT_AIOHTTP_LISTEN_PORT="28261"
install_caddy_if_needed() {
if command -v caddy &> /dev/null; then
echo -e "${green}Caddy is already installed.${NC}"
if systemctl list-units --full -all | grep -q 'caddy.service'; then
if systemctl is-active --quiet caddy.service; then
echo -e "${yellow}Stopping and disabling default caddy.service...${NC}"
systemctl stop caddy > /dev/null 2>&1
systemctl disable caddy > /dev/null 2>&1
fi
fi
return 0
fi
echo -e "${yellow}Installing Caddy...${NC}"
sudo apt update -y > /dev/null 2>&1
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl > /dev/null 2>&1
curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key | sudo tee /etc/apt/trusted.gpg.d/caddy-stable.asc > /dev/null 2>&1
echo "deb [signed-by=/etc/apt/trusted.gpg.d/caddy-stable.asc] https://dl.cloudsmith.io/public/caddy/stable/deb/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null 2>&1
sudo apt update -y > /dev/null 2>&1
sudo apt install -y caddy
if [ $? -ne 0 ]; then
echo -e "${red}Error: Failed to install Caddy. ${NC}"
exit 1
fi
systemctl stop caddy > /dev/null 2>&1
systemctl disable caddy > /dev/null 2>&1
echo -e "${green}Caddy installed successfully. ${NC}"
}
update_env_file() { update_env_file() {
local domain=$1 local domain=$1
local port=$2 local external_port=$2
local cert_dir="/etc/letsencrypt/live/$domain" local aiohttp_listen_address=$3
local aiohttp_listen_port=$4
local subpath_val=$(pwgen -s 32 1)
cat <<EOL > /etc/hysteria/core/scripts/normalsub/.env cat <<EOL > "$NORMALSUB_ENV_FILE"
HYSTERIA_DOMAIN=$domain HYSTERIA_DOMAIN=$domain
HYSTERIA_PORT=$port HYSTERIA_PORT=$external_port
HYSTERIA_CERTFILE=$cert_dir/fullchain.pem AIOHTTP_LISTEN_ADDRESS=$aiohttp_listen_address
HYSTERIA_KEYFILE=$cert_dir/privkey.pem AIOHTTP_LISTEN_PORT=$aiohttp_listen_port
SUBPATH=$(pwgen -s 32 1) SUBPATH=$subpath_val
EOL EOL
} }
create_service_file() { update_caddy_file_normalsub() {
local domain=$1
local external_port=$2
local subpath_val=$3
local aiohttp_address=$4
local aiohttp_port=$5
cat <<EOL > "$CADDY_CONFIG_FILE_NORMALSUB"
# Global configuration
{
admin off
auto_https disable_redirects
}
$domain:$external_port {
encode gzip zstd
route /$subpath_val/* {
reverse_proxy $aiohttp_address:$aiohttp_port {
header_up X-Real-IP {remote_host}
header_up X-Forwarded-For {remote_host}
header_up X-Forwarded-Port {server_port}
header_up X-Forwarded-Proto {scheme}
}
}
@blocked {
not path /$subpath_val/*
}
abort @blocked
}
EOL
}
create_normalsub_python_service_file() {
cat <<EOL > /etc/systemd/system/hysteria-normal-sub.service cat <<EOL > /etc/systemd/system/hysteria-normal-sub.service
[Unit] [Unit]
Description=normalsub Python Service Description=Hysteria Normalsub Python Service
After=network.target After=network.target
[Service] [Service]
ExecStart=/bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/hysteria2_venv/bin/python /etc/hysteria/core/scripts/normalsub/normalsub.py' ExecStart=/bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/hysteria2_venv/bin/python /etc/hysteria/core/scripts/normalsub/normalsub.py'
WorkingDirectory=/etc/hysteria/core/scripts/normalsub WorkingDirectory=/etc/hysteria/core/scripts/normalsub
EnvironmentFile=/etc/hysteria/core/scripts/normalsub/.env EnvironmentFile=$NORMALSUB_ENV_FILE
Restart=always Restart=always
User=root User=root
Group=root Group=root
@ -35,107 +105,120 @@ WantedBy=multi-user.target
EOL EOL
} }
create_caddy_normalsub_service_file() {
cat <<EOL > /etc/systemd/system/hysteria-caddy-normalsub.service
[Unit]
Description=Caddy for Hysteria Normalsub
After=network.target
[Service]
WorkingDirectory=/etc/hysteria/core/scripts/normalsub
ExecStart=/usr/bin/caddy run --environ --config $CADDY_CONFIG_FILE_NORMALSUB
ExecReload=/usr/bin/caddy reload --config $CADDY_CONFIG_FILE_NORMALSUB --force
TimeoutStopSec=5s
LimitNOFILE=1048576
PrivateTmp=true
User=root
Group=root
[Install]
WantedBy=multi-user.target
EOL
}
start_service() { start_service() {
local domain=$1 local domain=$1
local port=$2 local external_port=$2
if systemctl is-active --quiet hysteria-normal-sub.service; then install_caddy_if_needed
echo "The hysteria-normal-sub.service is already running."
return
fi
echo "Checking SSL certificates for $domain..." local aiohttp_listen_address="$DEFAULT_AIOHTTP_LISTEN_ADDRESS"
if certbot certificates | grep -q "$domain"; then local aiohttp_listen_port="$DEFAULT_AIOHTTP_LISTEN_PORT"
echo -e "${yellow}Certificate for $domain already exists. Renewing...${NC}"
certbot renew --cert-name "$domain" update_env_file "$domain" "$external_port" "$aiohttp_listen_address" "$aiohttp_listen_port"
if [ $? -ne 0 ]; then source "$NORMALSUB_ENV_FILE" # To get SUBPATH for Caddyfile
echo -e "${red}Error: Failed to renew SSL certificate. ${NC}"
exit 1 update_caddy_file_normalsub "$HYSTERIA_DOMAIN" "$HYSTERIA_PORT" "$SUBPATH" "$AIOHTTP_LISTEN_ADDRESS" "$AIOHTTP_LISTEN_PORT"
fi
echo -e "${green}Certificate renewed successfully. ${NC}" create_normalsub_python_service_file
else create_caddy_normalsub_service_file
echo -e "${yellow}Requesting new certificate for $domain...${NC}"
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d "$domain"
if [ $? -ne 0 ]; then
echo -e "${red}Error: Failed to generate SSL certificate. ${NC}"
exit 1
fi
echo -e "${green}Certificate generated successfully. ${NC}"
fi
update_env_file "$domain" "$port"
create_service_file
chown -R hysteria:hysteria "/etc/letsencrypt/live/$domain"
chown -R hysteria:hysteria /etc/hysteria/core/scripts/normalsub
systemctl daemon-reload systemctl daemon-reload
systemctl enable hysteria-normal-sub.service > /dev/null 2>&1 systemctl enable hysteria-normal-sub.service > /dev/null 2>&1
systemctl start hysteria-normal-sub.service > /dev/null 2>&1 systemctl start hysteria-normal-sub.service
systemctl daemon-reload > /dev/null 2>&1
systemctl enable hysteria-caddy-normalsub.service > /dev/null 2>&1
systemctl start hysteria-caddy-normalsub.service
if systemctl is-active --quiet hysteria-normal-sub.service; then if systemctl is-active --quiet hysteria-normal-sub.service && systemctl is-active --quiet hysteria-caddy-normalsub.service; then
echo -e "${green}normalsub service setup completed. The service is now running on port $port. ${NC}" echo -e "${green}Normalsub service setup completed.${NC}"
echo -e "${green}Access base URL: https://$HYSTERIA_DOMAIN:$HYSTERIA_PORT/$SUBPATH/sub/normal/{username}${NC}"
else else
echo -e "${red}normalsub setup completed. The service failed to start. ${NC}" echo -e "${red}Normalsub setup completed, but one or more services failed to start.${NC}"
systemctl status hysteria-normal-sub.service --no-pager
systemctl status hysteria-caddy-normalsub.service --no-pager
fi fi
} }
stop_service() { stop_service() {
if [ -f /etc/hysteria/core/scripts/normalsub/.env ]; then echo -e "${yellow}Stopping Hysteria Normalsub Python service...${NC}"
source /etc/hysteria/core/scripts/normalsub/.env
fi
if [ -n "$HYSTERIA_DOMAIN" ]; then
echo -e "${yellow}Deleting SSL certificate for domain: $HYSTERIA_DOMAIN...${NC}"
certbot delete --cert-name "$HYSTERIA_DOMAIN" --non-interactive > /dev/null 2>&1
else
echo -e "${red}HYSTERIA_DOMAIN not found in .env. Skipping certificate deletion.${NC}"
fi
systemctl stop hysteria-normal-sub.service > /dev/null 2>&1 systemctl stop hysteria-normal-sub.service > /dev/null 2>&1
systemctl disable hysteria-normal-sub.service > /dev/null 2>&1 systemctl disable hysteria-normal-sub.service > /dev/null 2>&1
echo -e "${yellow}Stopping Caddy service for Normalsub...${NC}"
systemctl stop hysteria-caddy-normalsub.service > /dev/null 2>&1
systemctl disable hysteria-caddy-normalsub.service > /dev/null 2>&1
systemctl daemon-reload > /dev/null 2>&1 systemctl daemon-reload > /dev/null 2>&1
rm -f /etc/hysteria/core/scripts/normalsub/.env rm -f "$NORMALSUB_ENV_FILE"
rm -f "$CADDY_CONFIG_FILE_NORMALSUB"
rm -f /etc/systemd/system/hysteria-normal-sub.service
rm -f /etc/systemd/system/hysteria-caddy-normalsub.service
systemctl daemon-reload > /dev/null 2>&1
echo -e "${yellow}normalsub service stopped and disabled. .env file removed.${NC}" echo -e "${green}Normalsub services stopped and disabled. Configuration files removed.${NC}"
} }
edit_subpath() { edit_subpath() {
local new_path="$1" local new_path="$1"
local env_file="/etc/hysteria/core/scripts/normalsub/.env"
if [ ! -f "$NORMALSUB_ENV_FILE" ]; then
echo -e "${red}Error: .env file ($NORMALSUB_ENV_FILE) not found. Please run the start command first.${NC}"
exit 1
fi
if [[ ! "$new_path" =~ ^[a-zA-Z0-9]+$ ]]; then if [[ ! "$new_path" =~ ^[a-zA-Z0-9]+$ ]]; then
echo -e "${red}Error: New subpath must contain only alphanumeric characters (a-z, A-Z, 0-9) and cannot be empty.${NC}" echo -e "${red}Error: New subpath must contain only alphanumeric characters (a-z, A-Z, 0-9) and cannot be empty.${NC}"
exit 1 exit 1
fi fi
if [ ! -f "$env_file" ]; then source "$NORMALSUB_ENV_FILE"
echo -e "${red}Error: .env file ($env_file) not found. Please run the start command first.${NC}" local old_subpath="$SUBPATH"
exit 1
fi sed -i "s|^SUBPATH=.*|SUBPATH=$new_path|" "$NORMALSUB_ENV_FILE"
echo -e "${green}SUBPATH updated to $new_path in $NORMALSUB_ENV_FILE.${NC}"
if grep -q "^SUBPATH=" "$env_file"; then update_caddy_file_normalsub "$HYSTERIA_DOMAIN" "$HYSTERIA_PORT" "$new_path" "$AIOHTTP_LISTEN_ADDRESS" "$AIOHTTP_LISTEN_PORT"
sed -i "s|^SUBPATH=.*|SUBPATH=$new_path|" "$env_file" echo -e "${green}Caddyfile for Normalsub updated.${NC}"
else
echo "SUBPATH=$new_path" >> "$env_file"
fi
echo -e "${green}SUBPATH updated to $new_path in $env_file.${NC}"
echo -e "${yellow}Restarting hysteria-normal-sub service...${NC}" echo -e "${yellow}Restarting hysteria-normal-sub service...${NC}"
systemctl daemon-reload
systemctl restart hysteria-normal-sub.service systemctl restart hysteria-normal-sub.service
echo -e "${yellow}Reloading hysteria-caddy-normalsub service...${NC}"
systemctl restart hysteria-caddy-normalsub.service
if systemctl is-active --quiet hysteria-normal-sub.service; then if systemctl is-active --quiet hysteria-normal-sub.service && systemctl is-active --quiet hysteria-caddy-normalsub.service; then
echo -e "${green}hysteria-normal-sub service restarted successfully.${NC}" echo -e "${green}Services restarted/reloaded successfully.${NC}"
echo -e "${green}New access base URL: https://$HYSTERIA_DOMAIN:$HYSTERIA_PORT/$new_path/sub/normal/{username}${NC}"
else else
echo -e "${red}Error: hysteria-normal-sub service failed to restart. Please check logs.${NC}" echo -e "${red}Error: One or more services failed to restart/reload. Please check logs.${NC}"
fi fi
} }
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 <EXTERNAL_DOMAIN> <EXTERNAL_PORT>${NC}"
exit 1 exit 1
fi fi
start_service "$2" "$3" start_service "$2" "$3"
@ -145,13 +228,13 @@ case "$1" in
;; ;;
edit_subpath) edit_subpath)
if [ -z "$2" ]; then if [ -z "$2" ]; then
echo -e "${red}Usage: $0 edit_subpath <NEW_SUBPATH> ${NC}" echo -e "${red}Usage: $0 edit_subpath <NEW_SUBPATH>${NC}"
exit 1 exit 1
fi fi
edit_subpath "$2" edit_subpath "$2"
;; ;;
*) *)
echo -e "${red}Usage: $0 {start <DOMAIN> <PORT> | stop | edit_subpath <NEW_SUBPATH>} ${NC}" echo -e "${red}Usage: $0 {start <EXTERNAL_DOMAIN> <EXTERNAL_PORT> | stop | edit_subpath <NEW_SUBPATH>}${NC}"
exit 1 exit 1
;; ;;
esac esac