diff --git a/core/cli.py b/core/cli.py index a62cf39..4e983cd 100644 --- a/core/cli.py +++ b/core/cli.py @@ -586,8 +586,9 @@ def singbox(action: str, domain: str, port: int): help='Action to perform: start, stop, or edit_subpath') @click.option('--domain', '-d', required=False, help='Domain name for SSL (for start action)', type=str) @click.option('--port', '-p', required=False, help='Port number for NormalSub service (for start action)', type=int) -@click.option('--subpath', '-sp', required=False, help='New subpath (alphanumeric, for edit_subpath action)', type=str) +@click.option('--subpath', '-sp', required=False, help="New subpath (e.g., 'path' or 'path/to/resource', for edit_subpath action)", type=str) def normalsub(action: str, domain: str, port: int, subpath: str): + """Manage the NormalSub service.""" try: if action == 'start': if not domain or not port: diff --git a/core/cli_api.py b/core/cli_api.py index edc382e..312db64 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -5,6 +5,7 @@ from datetime import datetime import json from typing import Any, Optional from dotenv import dotenv_values +import re import traffic @@ -661,8 +662,8 @@ def edit_normalsub_subpath(new_subpath: str): '''Edits the subpath for NormalSub service.''' if not new_subpath: raise InvalidInputError('Error: New subpath cannot be empty.') - if not new_subpath.isalnum(): - raise InvalidInputError('Error: New subpath must contain only alphanumeric characters (a-z, A-Z, 0-9).') + if not re.match(r"^[a-zA-Z0-9]+(?:/[a-zA-Z0-9]+)*$", new_subpath): + raise InvalidInputError("Error: Invalid subpath format. Must be alphanumeric segments separated by single slashes (e.g., 'path' or 'path/to/resource').") run_cmd(['bash', Command.INSTALL_NORMALSUB.value, 'edit_subpath', new_subpath]) diff --git a/core/scripts/hysteria2/show_user_uri.py b/core/scripts/hysteria2/show_user_uri.py index c2eb8c0..5981369 100644 --- a/core/scripts/hysteria2/show_user_uri.py +++ b/core/scripts/hysteria2/show_user_uri.py @@ -206,7 +206,7 @@ def show_uri(args: argparse.Namespace) -> None: if args.normalsub and is_service_active("hysteria-normal-sub.service"): domain, port, subpath = get_normalsub_domain_and_port() if domain and port: - print(f"\nNormal-SUB Sublink:\nhttps://{domain}:{port}/{subpath}/sub/normal/{auth_password}#Hysteria2\n") + print(f"\nNormal-SUB Sublink:\nhttps://{domain}:{port}/{subpath}/{auth_password}#Hysteria2\n") def main(): parser = argparse.ArgumentParser(description="Hysteria2 URI Generator") diff --git a/core/scripts/hysteria2/wrapper_uri.py b/core/scripts/hysteria2/wrapper_uri.py index 6f50542..d0246d3 100644 --- a/core/scripts/hysteria2/wrapper_uri.py +++ b/core/scripts/hysteria2/wrapper_uri.py @@ -117,7 +117,7 @@ def process_users(target_usernames: List[str]) -> List[Dict[str, Any]]: user_output["nodes"].append({"name": node_name, "uri": uri}) if ns_domain and ns_port and ns_subpath: - user_output["normal_sub"] = f"https://{ns_domain}:{ns_port}/{ns_subpath}/sub/normal/{auth_password}#{username}" + user_output["normal_sub"] = f"https://{ns_domain}:{ns_port}/{ns_subpath}/{auth_password}#{username}" results.append(user_output) diff --git a/core/scripts/normalsub/normalsub.py b/core/scripts/normalsub/normalsub.py index 3284951..af4906b 100644 --- a/core/scripts/normalsub/normalsub.py +++ b/core/scripts/normalsub/normalsub.py @@ -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/") + print(f"External access via Caddy should be at https://{self.config.domain}:{self.config.external_port}/{self.config.subpath}/") web.run_app( self.app, host=self.config.aiohttp_listen_address, diff --git a/core/scripts/normalsub/normalsub.sh b/core/scripts/normalsub/normalsub.sh index 674d53e..11a8e00 100644 --- a/core/scripts/normalsub/normalsub.sh +++ b/core/scripts/normalsub/normalsub.sh @@ -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}" diff --git a/core/scripts/webpanel/assets/js/settings.js b/core/scripts/webpanel/assets/js/settings.js index 7fc3780..fd7a7eb 100644 --- a/core/scripts/webpanel/assets/js/settings.js +++ b/core/scripts/webpanel/assets/js/settings.js @@ -88,7 +88,8 @@ $(document).ready(function () { function isValidSubPath(subpath) { if (!subpath) return false; - return /^[a-zA-Z0-9]+$/.test(subpath); + const subpathRegex = /^[a-zA-Z0-9]+(?:\/[a-zA-Z0-9]+)*$/; + return subpathRegex.test(subpath); } function isValidIPorDomain(input) { diff --git a/core/scripts/webpanel/routers/api/v1/schema/config/normalsub.py b/core/scripts/webpanel/routers/api/v1/schema/config/normalsub.py index 7f2af1f..d22975a 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/config/normalsub.py +++ b/core/scripts/webpanel/routers/api/v1/schema/config/normalsub.py @@ -6,7 +6,7 @@ class StartInputBody(BaseModel): port: int class EditSubPathInputBody(BaseModel): - subpath: str = Field(..., min_length=1, pattern=r"^[a-zA-Z0-9]+$", description="The new subpath, must be alphanumeric.") + subpath: str = Field(..., min_length=1, pattern=r"^[a-zA-Z0-9]+(?:/[a-zA-Z0-9]+)*$", description="The new subpath, must be alphanumeric.") class GetSubPathResponse(BaseModel): subpath: Optional[str] = Field(None, description="The current NormalSub subpath, or null if not set/found.") \ No newline at end of file diff --git a/core/scripts/webpanel/templates/settings.html b/core/scripts/webpanel/templates/settings.html index ff95b4f..b01978e 100644 --- a/core/scripts/webpanel/templates/settings.html +++ b/core/scripts/webpanel/templates/settings.html @@ -142,9 +142,13 @@
+ placeholder='e.g., mysub or path/to/resource'> + + This path will be part of the URL. Do not use a leading or trailing slash. + Example: path/to/resource +
- Please enter a valid subpath (alphanumeric characters only, e.g., mysub). + Invalid format. Use alphanumeric segments separated by single slashes.