feat(normalsub): Implement flexible, path-based subscription URLs

Refactors the NormalSub feature to move away from a hardcoded `/sub/normal/` path and a restrictive single-word subpath. This change introduces support for multi-segment, slash-separated subpaths (e.g., `path/to/resource`), providing greater flexibility for creating descriptive and structured subscription URLs.
This commit is contained in:
ReturnFI
2025-11-29 20:22:22 +00:00
parent 5049209df5
commit 76472dfde5
9 changed files with 31 additions and 24 deletions

View File

@ -432,12 +432,12 @@ class HysteriaServer:
self._noindex_middleware
])
safe_subpath = self.validate_and_escape_subpath(self.config.subpath)
safe_subpath = self.validate_subpath_for_routing(self.config.subpath)
base_path = f'/{safe_subpath}'
self.app.router.add_get(f'{base_path}/sub/normal/style.css', self.handle_style)
self.app.router.add_get(f'{base_path}/sub/normal/script.js', self.handle_script)
self.app.router.add_get(f'{base_path}/sub/normal/{{password_token}}', self.handle)
self.app.router.add_get(f'{base_path}/style.css', self.handle_style)
self.app.router.add_get(f'{base_path}/script.js', self.handle_script)
self.app.router.add_get(f'{base_path}/{{password_token}}', self.handle)
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)
@ -450,7 +450,7 @@ class HysteriaServer:
subpath = os.getenv('SUBPATH', '').strip().strip("/")
if not subpath or not self.is_valid_subpath(subpath):
raise ValueError(
f"Invalid or empty SUBPATH: '{subpath}'. Subpath must be non-empty and contain only alphanumeric characters.")
f"Invalid or empty SUBPATH: '{subpath}'. Subpath must be valid segments separated by slashes (e.g., 'path' or 'path/to/resource').")
sni_file = '/etc/hysteria/.configs.env'
singbox_template_path = '/etc/hysteria/core/scripts/normalsub/singbox.json'
@ -485,12 +485,12 @@ class HysteriaServer:
return "bts.com"
def is_valid_subpath(self, subpath: str) -> bool:
return bool(re.match(r"^[a-zA-Z0-9]+$", subpath))
return bool(re.match(r"^[a-zA-Z0-9]+(?:/[a-zA-Z0-9]+)*$", subpath))
def validate_and_escape_subpath(self, subpath: str) -> str:
def validate_subpath_for_routing(self, subpath: str) -> str:
if not self.is_valid_subpath(subpath):
raise ValueError(f"Invalid subpath: {subpath}")
return re.escape(subpath)
return subpath
@middleware
async def _rate_limit_middleware(self, request: web.Request, handler):
@ -610,7 +610,7 @@ class HysteriaServer:
if not Utils.is_valid_url(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/{user_info.password}"
sub_link = f"{base_url}/{self.config.subpath}/{user_info.password}"
sub_link_encoded = quote(sub_link, safe='')
sublink_qrcode = Utils.generate_qrcode_base64(sub_link)
@ -665,7 +665,7 @@ class HysteriaServer:
def run(self):
print(f"Starting Hysteria Normalsub server on {self.config.aiohttp_listen_address}:{self.config.aiohttp_listen_port}")
print(f"External access via Caddy should be at https://{self.config.domain}:{self.config.external_port}/{self.config.subpath}/sub/normal/<USER_PASSWORD>")
print(f"External access via Caddy should be at https://{self.config.domain}:{self.config.external_port}/{self.config.subpath}/<USER_PASSWORD>")
web.run_app(
self.app,
host=self.config.aiohttp_listen_address,

View File

@ -140,7 +140,7 @@ start_service() {
local aiohttp_listen_port="$DEFAULT_AIOHTTP_LISTEN_PORT"
update_env_file "$domain" "$external_port" "$aiohttp_listen_address" "$aiohttp_listen_port"
source "$NORMALSUB_ENV_FILE" # To get SUBPATH for Caddyfile
source "$NORMALSUB_ENV_FILE"
update_caddy_file_normalsub "$HYSTERIA_DOMAIN" "$HYSTERIA_PORT" "$SUBPATH" "$AIOHTTP_LISTEN_ADDRESS" "$AIOHTTP_LISTEN_PORT"
@ -157,7 +157,7 @@ start_service() {
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.${NC}"
echo -e "${green}Access base URL: https://$HYSTERIA_DOMAIN:$HYSTERIA_PORT/$SUBPATH/sub/normal/{username}${NC}"
echo -e "${green}Access base URL: https://$HYSTERIA_DOMAIN:$HYSTERIA_PORT/$SUBPATH/{username}${NC}"
else
echo -e "${red}Normalsub setup completed, but one or more services failed to start.${NC}"
systemctl status hysteria-normal-sub.service --no-pager
@ -192,8 +192,8 @@ edit_subpath() {
exit 1
fi
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}"
if [[ ! "$new_path" =~ ^[a-zA-Z0-9]+(/[a-zA-Z0-9]+)*$ ]]; then
echo -e "${red}Error: Invalid subpath format. Must be alphanumeric segments separated by single slashes (e.g., 'path' or 'path/to/resource'). Cannot start/end with a slash or have consecutive slashes.${NC}"
exit 1
fi
@ -224,7 +224,7 @@ edit_subpath() {
if systemctl is-active --quiet hysteria-normal-sub.service && systemctl is-active --quiet hysteria-caddy-normalsub.service; then
echo -e "${green}Services updated successfully.${NC}"
echo -e "${green}New access base URL: https://$HYSTERIA_DOMAIN:$HYSTERIA_PORT/$new_path/sub/normal/{username}${NC}"
echo -e "${green}New access base URL: https://$HYSTERIA_DOMAIN:$HYSTERIA_PORT/$new_path/{username}${NC}"
echo -e "${cyan}Old subpath '$old_subpath' is no longer accessible.${NC}"
else
echo -e "${red}Error: One or more services failed to restart/reload. Please check logs.${NC}"