feat: Implement subpath support for normal subscriptions

This commit is contained in:
Whispering Wind
2025-02-28 14:24:14 +03:30
committed by GitHub
parent e01dde6464
commit a5dd5f6f9f
3 changed files with 23 additions and 11 deletions

View File

@ -19,7 +19,8 @@ get_normalsub_domain_and_port() {
local domain port local domain port
domain=$(grep -E '^HYSTERIA_DOMAIN=' "$NORMALSUB_ENV" | cut -d'=' -f2) domain=$(grep -E '^HYSTERIA_DOMAIN=' "$NORMALSUB_ENV" | cut -d'=' -f2)
port=$(grep -E '^HYSTERIA_PORT=' "$NORMALSUB_ENV" | cut -d'=' -f2) port=$(grep -E '^HYSTERIA_PORT=' "$NORMALSUB_ENV" | cut -d'=' -f2)
echo "$domain" "$port" subpath=$(grep -E '^SUBPATH=' "$NORMALSUB_ENV" | cut -d'=' -f2)
echo "$domain" "$port" "$subpath"
else else
echo "" echo ""
fi fi
@ -145,9 +146,9 @@ show_uri() {
fi fi
fi fi
if [ "$generate_normalsub" = true ] && systemctl is-active --quiet hysteria-normal-sub.service; then if [ "$generate_normalsub" = true ] && systemctl is-active --quiet hysteria-normal-sub.service; then
read -r domain port < <(get_normalsub_domain_and_port) read -r domain port subpath < <(get_normalsub_domain_and_port)
if [ -n "$domain" ] && [ -n "$port" ]; then if [ -n "$domain" ] && [ -n "$port" ]; then
echo -e "\nNormal-SUB Sublink:\nhttps://$domain:$port/sub/normal/$username#Hysteria2\n" echo -e "\nNormal-SUB Sublink:\nhttps://$domain:$port/$subpath/sub/normal/$username#Hysteria2\n"
fi fi
fi fi
else else

View File

@ -13,7 +13,7 @@ from io import BytesIO
from aiohttp import web from aiohttp import web
from aiohttp.web_middlewares import middleware from aiohttp.web_middlewares import middleware
from urllib.parse import unquote, parse_qs, urlparse from urllib.parse import unquote, parse_qs, urlparse, urljoin
from dotenv import load_dotenv from dotenv import load_dotenv
import qrcode import qrcode
from jinja2 import Environment, FileSystemLoader from jinja2 import Environment, FileSystemLoader
@ -36,6 +36,7 @@ class AppConfig:
rate_limit_window: int rate_limit_window: int
sni: str sni: str
template_dir: str template_dir: str
subpath: str
class RateLimiter: class RateLimiter:
@ -178,6 +179,11 @@ class Utils:
size /= 1024 size /= 1024
return f"{size:.2f} PB" return f"{size:.2f} PB"
@staticmethod
def build_url(base: str, path: str) -> str:
"""Constructs a URL, handling potential double slashes correctly."""
return urljoin(base, path)
class HysteriaCLI: class HysteriaCLI:
"""Interface for Hysteria CLI commands""" """Interface for Hysteria CLI commands"""
@ -426,8 +432,9 @@ class HysteriaServer:
self.template_renderer = TemplateRenderer(self.config.template_dir, self.config) self.template_renderer = TemplateRenderer(self.config.template_dir, self.config)
self.app = web.Application(middlewares=[self._rate_limit_middleware]) self.app = web.Application(middlewares=[self._rate_limit_middleware])
self.app.add_routes([web.get('/sub/normal/{username}', self.handle)]) self.app.add_routes([web.get(Utils.build_url('/{subpath}/sub/normal/', '{username}'), self.handle)])
self.app.router.add_route('*', '/sub/normal/{tail:.*}', self.handle_404) self.app.router.add_route('*', '/{subpath:[^{}]+}/{tail:.*}', self.handle_404)
self.app.router.add_route('*', '/{tail:.*}', self.handle_404)
def _load_config(self) -> AppConfig: def _load_config(self) -> AppConfig:
"""Loads application configuration from environment variables""" """Loads application configuration from environment variables"""
@ -435,6 +442,7 @@ class HysteriaServer:
cert_file = os.getenv('HYSTERIA_CERTFILE') cert_file = os.getenv('HYSTERIA_CERTFILE')
key_file = os.getenv('HYSTERIA_KEYFILE') key_file = os.getenv('HYSTERIA_KEYFILE')
port = int(os.getenv('HYSTERIA_PORT', '3326')) port = int(os.getenv('HYSTERIA_PORT', '3326'))
subpath = os.getenv('SUBPATH', '').strip().strip("/")
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'
hysteria_cli_path = '/etc/hysteria/core/cli.py' hysteria_cli_path = '/etc/hysteria/core/cli.py'
@ -455,7 +463,8 @@ class HysteriaServer:
rate_limit=rate_limit, rate_limit=rate_limit,
rate_limit_window=rate_limit_window, rate_limit_window=rate_limit_window,
sni=sni, sni=sni,
template_dir=template_dir template_dir=template_dir,
subpath=subpath
) )
def _load_sni_from_env(self, sni_file: str) -> str: def _load_sni_from_env(self, sni_file: str) -> str:
@ -483,10 +492,10 @@ class HysteriaServer:
async def handle(self, request: web.Request) -> web.Response: async def handle(self, request: web.Request) -> web.Response:
"""Main request handler""" """Main request handler"""
try: try:
# No need to extract subpath here; aiohttp handles it in the route
username = Utils.sanitize_input(request.match_info.get('username', ''), r'^[a-zA-Z0-9_-]+$') username = Utils.sanitize_input(request.match_info.get('username', ''), r'^[a-zA-Z0-9_-]+$')
if not username: if not username:
return web.Response(status=400, text="Error: Missing 'username' parameter.") return web.Response(status=400, text="Error: Missing 'username' parameter.")
user_agent = request.headers.get('User-Agent', '').lower() user_agent = request.headers.get('User-Agent', '').lower()
if any(browser in user_agent for browser in ['chrome', 'firefox', 'safari', 'edge', 'opera']): if any(browser in user_agent for browser in ['chrome', 'firefox', 'safari', 'edge', 'opera']):
@ -527,11 +536,12 @@ class HysteriaServer:
return web.Response(text=subscription, content_type='text/plain') return web.Response(text=subscription, content_type='text/plain')
async def _get_template_context(self, username: str) -> TemplateContext: async def _get_template_context(self, username: str) -> TemplateContext:
"""Generates the context for HTML template rendering""" """Generates the context for HTML template rendering, incorporating subpath"""
user_info = self.hysteria_cli.get_user_info(username) user_info = self.hysteria_cli.get_user_info(username)
ipv4_uri, ipv6_uri = self.hysteria_cli.get_uris(username) ipv4_uri, ipv6_uri = self.hysteria_cli.get_uris(username)
sub_link = f"https://{self.config.domain}:{self.config.port}/sub/normal/{username}" base_url = f"https://{self.config.domain}:{self.config.port}"
sub_link = Utils.build_url(base_url, f"/{self.config.subpath}/sub/normal/{username}")
ipv4_qrcode = Utils.generate_qrcode_base64(ipv4_uri) ipv4_qrcode = Utils.generate_qrcode_base64(ipv4_uri)
ipv6_qrcode = Utils.generate_qrcode_base64(ipv6_uri) ipv6_qrcode = Utils.generate_qrcode_base64(ipv6_uri)
sublink_qrcode = Utils.generate_qrcode_base64(sub_link) sublink_qrcode = Utils.generate_qrcode_base64(sub_link)
@ -565,4 +575,4 @@ class HysteriaServer:
if __name__ == '__main__': if __name__ == '__main__':
server = HysteriaServer() server = HysteriaServer()
server.run() server.run()

View File

@ -22,6 +22,7 @@ HYSTERIA_DOMAIN=$domain
HYSTERIA_PORT=$port HYSTERIA_PORT=$port
HYSTERIA_CERTFILE=$cert_dir/fullchain.pem HYSTERIA_CERTFILE=$cert_dir/fullchain.pem
HYSTERIA_KEYFILE=$cert_dir/privkey.pem HYSTERIA_KEYFILE=$cert_dir/privkey.pem
SUBPATH=$(pwgen -s 32 1)
EOL EOL
} }