Implement noindex headers in responses & noindex tag in html & noindex
in robots.txt & close connection if the path is invalid
This commit is contained in:
@ -19,6 +19,7 @@ from jinja2 import Environment, FileSystemLoader
|
|||||||
|
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class AppConfig:
|
class AppConfig:
|
||||||
domain: str
|
domain: str
|
||||||
@ -34,6 +35,7 @@ class AppConfig:
|
|||||||
template_dir: str
|
template_dir: str
|
||||||
subpath: str
|
subpath: str
|
||||||
|
|
||||||
|
|
||||||
class RateLimiter:
|
class RateLimiter:
|
||||||
def __init__(self, limit: int, window: int):
|
def __init__(self, limit: int, window: int):
|
||||||
self.limit = limit
|
self.limit = limit
|
||||||
@ -51,6 +53,7 @@ class RateLimiter:
|
|||||||
self.store[client_ip] = (requests + 1, current_time)
|
self.store[client_ip] = (requests + 1, current_time)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UriComponents:
|
class UriComponents:
|
||||||
username: Optional[str]
|
username: Optional[str]
|
||||||
@ -59,6 +62,7 @@ class UriComponents:
|
|||||||
port: Optional[int]
|
port: Optional[int]
|
||||||
obfs_password: str
|
obfs_password: str
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class UserInfo:
|
class UserInfo:
|
||||||
username: str
|
username: str
|
||||||
@ -100,6 +104,7 @@ class UserInfo:
|
|||||||
download = Utils.human_readable_bytes(self.download_bytes)
|
download = Utils.human_readable_bytes(self.download_bytes)
|
||||||
return f"Upload: {upload}, Download: {download}, Total: {total}"
|
return f"Upload: {upload}, Download: {download}, Total: {total}"
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TemplateContext:
|
class TemplateContext:
|
||||||
username: str
|
username: str
|
||||||
@ -113,6 +118,7 @@ class TemplateContext:
|
|||||||
ipv4_uri: Optional[str]
|
ipv4_uri: Optional[str]
|
||||||
ipv6_uri: Optional[str]
|
ipv6_uri: Optional[str]
|
||||||
|
|
||||||
|
|
||||||
class Utils:
|
class Utils:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def sanitize_input(value: str, pattern: str) -> str:
|
def sanitize_input(value: str, pattern: str) -> str:
|
||||||
@ -155,6 +161,7 @@ class Utils:
|
|||||||
except ValueError:
|
except ValueError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
class HysteriaCLI:
|
class HysteriaCLI:
|
||||||
def __init__(self, cli_path: str):
|
def __init__(self, cli_path: str):
|
||||||
self.cli_path = cli_path
|
self.cli_path = cli_path
|
||||||
@ -190,6 +197,7 @@ class HysteriaCLI:
|
|||||||
ipv6_uri = re.search(r'IPv6:\s*(.*)', output)
|
ipv6_uri = re.search(r'IPv6:\s*(.*)', output)
|
||||||
return (ipv4_uri.group(1).strip() if ipv4_uri else None, ipv6_uri.group(1).strip() if ipv6_uri else None)
|
return (ipv4_uri.group(1).strip() if ipv4_uri else None, ipv6_uri.group(1).strip() if ipv6_uri else None)
|
||||||
|
|
||||||
|
|
||||||
class UriParser:
|
class UriParser:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def extract_uri_components(uri: Optional[str], prefix: str) -> Optional[UriComponents]:
|
def extract_uri_components(uri: Optional[str], prefix: str) -> Optional[UriComponents]:
|
||||||
@ -215,6 +223,7 @@ class UriParser:
|
|||||||
print(f"Error during URI parsing: {e}, URI: {uri}")
|
print(f"Error during URI parsing: {e}, URI: {uri}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class SingboxConfigGenerator:
|
class SingboxConfigGenerator:
|
||||||
def __init__(self, hysteria_cli: HysteriaCLI, default_sni: str):
|
def __init__(self, hysteria_cli: HysteriaCLI, default_sni: str):
|
||||||
self.hysteria_cli = hysteria_cli
|
self.hysteria_cli = hysteria_cli
|
||||||
@ -270,7 +279,8 @@ class SingboxConfigGenerator:
|
|||||||
|
|
||||||
def combine_configs(self, username: str, config_v4: Optional[Dict[str, Any]], config_v6: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
def combine_configs(self, username: str, config_v4: Optional[Dict[str, Any]], config_v6: Optional[Dict[str, Any]]) -> Dict[str, Any]:
|
||||||
combined_config = self.get_template()
|
combined_config = self.get_template()
|
||||||
combined_config['outbounds'] = [outbound for outbound in combined_config['outbounds'] if outbound.get('type') != 'hysteria2']
|
combined_config['outbounds'] = [outbound for outbound in combined_config['outbounds']
|
||||||
|
if outbound.get('type') != 'hysteria2']
|
||||||
|
|
||||||
modified_v4_outbounds = []
|
modified_v4_outbounds = []
|
||||||
if config_v4:
|
if config_v4:
|
||||||
@ -304,6 +314,7 @@ class SingboxConfigGenerator:
|
|||||||
combined_config['outbounds'].extend(modified_v4_outbounds + modified_v6_outbounds)
|
combined_config['outbounds'].extend(modified_v4_outbounds + modified_v6_outbounds)
|
||||||
return combined_config
|
return combined_config
|
||||||
|
|
||||||
|
|
||||||
class SubscriptionManager:
|
class SubscriptionManager:
|
||||||
def __init__(self, hysteria_cli: HysteriaCLI, config: AppConfig):
|
def __init__(self, hysteria_cli: HysteriaCLI, config: AppConfig):
|
||||||
self.hysteria_cli = hysteria_cli
|
self.hysteria_cli = hysteria_cli
|
||||||
@ -335,6 +346,7 @@ class SubscriptionManager:
|
|||||||
profile_lines = f"//profile-title: {username}-Hysteria2 🚀\n//profile-update-interval: 1\n"
|
profile_lines = f"//profile-title: {username}-Hysteria2 🚀\n//profile-update-interval: 1\n"
|
||||||
return profile_lines + subscription_info + "\n".join(processed_uris)
|
return profile_lines + subscription_info + "\n".join(processed_uris)
|
||||||
|
|
||||||
|
|
||||||
class TemplateRenderer:
|
class TemplateRenderer:
|
||||||
def __init__(self, template_dir: str, config: AppConfig):
|
def __init__(self, template_dir: str, config: AppConfig):
|
||||||
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
|
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
|
||||||
@ -344,6 +356,7 @@ class TemplateRenderer:
|
|||||||
def render(self, context: TemplateContext) -> str:
|
def render(self, context: TemplateContext) -> str:
|
||||||
return self.html_template.render(vars(context))
|
return self.html_template.render(vars(context))
|
||||||
|
|
||||||
|
|
||||||
class HysteriaServer:
|
class HysteriaServer:
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config = self._load_config()
|
self.config = self._load_config()
|
||||||
@ -353,16 +366,22 @@ class HysteriaServer:
|
|||||||
self.singbox_generator.set_template_path(self.config.singbox_template_path)
|
self.singbox_generator.set_template_path(self.config.singbox_template_path)
|
||||||
self.subscription_manager = SubscriptionManager(self.hysteria_cli, self.config)
|
self.subscription_manager = SubscriptionManager(self.hysteria_cli, self.config)
|
||||||
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._invalid_endpoint_middleware,
|
||||||
safe_subpath = self.validate_and_escape_subpath(self.config.subpath)
|
self._rate_limit_middleware,
|
||||||
self.app.add_routes([
|
self._noindex_middleware
|
||||||
web.get(f'/{safe_subpath}/sub/normal/{{username}}', self.handle)
|
|
||||||
])
|
])
|
||||||
|
|
||||||
self.app.router.add_route('*', f'/{safe_subpath}/{{tail:.*}}', self.handle_404)
|
safe_subpath = self.validate_and_escape_subpath(self.config.subpath)
|
||||||
self.app.router.add_route('*', '/{tail:.*}', self.handle_generic_404)
|
|
||||||
|
|
||||||
|
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}/robots.txt', self.robots_handler)
|
||||||
|
|
||||||
|
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')
|
||||||
@ -372,8 +391,8 @@ class HysteriaServer:
|
|||||||
subpath = os.getenv('SUBPATH', '').strip().strip("/")
|
subpath = os.getenv('SUBPATH', '').strip().strip("/")
|
||||||
|
|
||||||
if not self.is_valid_subpath(subpath):
|
if not self.is_valid_subpath(subpath):
|
||||||
raise ValueError(f"Invalid SUBPATH: '{subpath}'. Subpath must contain only alphanumeric characters, hyphens, and underscores.")
|
raise ValueError(
|
||||||
|
f"Invalid SUBPATH: '{subpath}'. Subpath must contain only alphanumeric characters, hyphens, and underscores.")
|
||||||
|
|
||||||
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'
|
||||||
@ -411,10 +430,25 @@ class HysteriaServer:
|
|||||||
@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 = request.headers.get('X-Forwarded-For', request.headers.get('X-Real-IP', request.remote))
|
||||||
if not self.rate_limiter.check_limit(client_ip):
|
if not self.rate_limiter.check_limit(client_ip): # type: ignore
|
||||||
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
|
||||||
|
async def _invalid_endpoint_middleware(self, request: web.Request, handler):
|
||||||
|
path = f'/{self.config.subpath}/'
|
||||||
|
if not request.path.startswith(path):
|
||||||
|
if request.transport is not None:
|
||||||
|
request.transport.close() # Drop the connection immediately
|
||||||
|
raise web.HTTPForbidden()
|
||||||
|
return await handler(request)
|
||||||
|
|
||||||
|
@middleware
|
||||||
|
async def _noindex_middleware(self, request: web.Request, handler):
|
||||||
|
response = await handler(request)
|
||||||
|
response.headers['X-Robots-Tag'] = 'noindex, nofollow, noarchive, nosnippet'
|
||||||
|
return response
|
||||||
|
|
||||||
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 = Utils.sanitize_input(request.match_info.get('username', ''), r'^[a-zA-Z0-9_-]+$')
|
||||||
@ -476,15 +510,18 @@ class HysteriaServer:
|
|||||||
ipv6_uri=ipv6_uri
|
ipv6_uri=ipv6_uri
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def robots_handler(self, request: web.Request) -> web.Response:
|
||||||
|
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(self, request: web.Request) -> web.Response:
|
||||||
"""Handles 404 Not Found errors *within* the subpath."""
|
"""Handles 404 Not Found errors *within* the subpath."""
|
||||||
print(f"404 Not Found (within subpath): {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:
|
# async def handle_generic_404(self, request: web.Request) -> web.Response:
|
||||||
"""Handles 404 Not Found errors *outside* the subpath."""
|
# """Handles 404 Not Found errors *outside* the subpath."""
|
||||||
print(f"404 Not Found (generic): {request.path}")
|
# print(f"404 Not Found (generic): {request.path}")
|
||||||
return web.Response(status=404, text="Not Found")
|
# return web.Response(status=404, text="Not Found")
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
||||||
@ -492,6 +529,7 @@ class HysteriaServer:
|
|||||||
ssl_context.set_ciphers('ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384')
|
ssl_context.set_ciphers('ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384')
|
||||||
web.run_app(self.app, port=self.config.port, ssl_context=ssl_context)
|
web.run_app(self.app, port=self.config.port, ssl_context=ssl_context)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
server = HysteriaServer()
|
server = HysteriaServer()
|
||||||
server.run()
|
server.run()
|
||||||
@ -1,7 +1,9 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Hysteria2 Subscription</title>
|
<title>Hysteria2 Subscription</title>
|
||||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||||
@ -76,9 +78,11 @@
|
|||||||
0% {
|
0% {
|
||||||
transform: translate(0, 0) rotate(0deg);
|
transform: translate(0, 0) rotate(0deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
50% {
|
50% {
|
||||||
transform: translate(100px, 100px) rotate(180deg);
|
transform: translate(100px, 100px) rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
100% {
|
100% {
|
||||||
transform: translate(0, 0) rotate(360deg);
|
transform: translate(0, 0) rotate(360deg);
|
||||||
}
|
}
|
||||||
@ -170,7 +174,8 @@
|
|||||||
.qr-section {
|
.qr-section {
|
||||||
background: var(--card-bg-light);
|
background: var(--card-bg-light);
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
overflow: hidden; /* Add this to contain the header */
|
overflow: hidden;
|
||||||
|
/* Add this to contain the header */
|
||||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
@ -289,7 +294,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
to { transform: rotate(360deg); }
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
@ -299,6 +306,7 @@
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="background-animation"></div>
|
<div class="background-animation"></div>
|
||||||
<div class="loading-indicator" id="loading-indicator">
|
<div class="loading-indicator" id="loading-indicator">
|
||||||
@ -330,7 +338,8 @@
|
|||||||
Used / Total
|
Used / Total
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="card-text" data-toggle="tooltip" data-placement="top" title="Total Data Usage: {{ usage_raw }}">
|
<p class="card-text" data-toggle="tooltip" data-placement="top"
|
||||||
|
title="Total Data Usage: {{ usage_raw }}">
|
||||||
{{ usage }}
|
{{ usage }}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@ -451,4 +460,5 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user