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:
Iam54r1n4
2025-03-31 18:36:36 +00:00
parent 924d24049f
commit 08d1679dc0
2 changed files with 90 additions and 42 deletions

View File

@ -19,6 +19,7 @@ from jinja2 import Environment, FileSystemLoader
load_dotenv()
@dataclass
class AppConfig:
domain: str
@ -34,6 +35,7 @@ class AppConfig:
template_dir: str
subpath: str
class RateLimiter:
def __init__(self, limit: int, window: int):
self.limit = limit
@ -51,6 +53,7 @@ class RateLimiter:
self.store[client_ip] = (requests + 1, current_time)
return True
@dataclass
class UriComponents:
username: Optional[str]
@ -59,6 +62,7 @@ class UriComponents:
port: Optional[int]
obfs_password: str
@dataclass
class UserInfo:
username: str
@ -100,6 +104,7 @@ class UserInfo:
download = Utils.human_readable_bytes(self.download_bytes)
return f"Upload: {upload}, Download: {download}, Total: {total}"
@dataclass
class TemplateContext:
username: str
@ -113,6 +118,7 @@ class TemplateContext:
ipv4_uri: Optional[str]
ipv6_uri: Optional[str]
class Utils:
@staticmethod
def sanitize_input(value: str, pattern: str) -> str:
@ -155,6 +161,7 @@ class Utils:
except ValueError:
return False
class HysteriaCLI:
def __init__(self, cli_path: str):
self.cli_path = cli_path
@ -190,6 +197,7 @@ class HysteriaCLI:
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)
class UriParser:
@staticmethod
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}")
return None
class SingboxConfigGenerator:
def __init__(self, hysteria_cli: HysteriaCLI, default_sni: str):
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]:
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 = []
if config_v4:
@ -304,6 +314,7 @@ class SingboxConfigGenerator:
combined_config['outbounds'].extend(modified_v4_outbounds + modified_v6_outbounds)
return combined_config
class SubscriptionManager:
def __init__(self, hysteria_cli: HysteriaCLI, config: AppConfig):
self.hysteria_cli = hysteria_cli
@ -335,6 +346,7 @@ class SubscriptionManager:
profile_lines = f"//profile-title: {username}-Hysteria2 🚀\n//profile-update-interval: 1\n"
return profile_lines + subscription_info + "\n".join(processed_uris)
class TemplateRenderer:
def __init__(self, template_dir: str, config: AppConfig):
self.env = Environment(loader=FileSystemLoader(template_dir), autoescape=True)
@ -344,6 +356,7 @@ class TemplateRenderer:
def render(self, context: TemplateContext) -> str:
return self.html_template.render(vars(context))
class HysteriaServer:
def __init__(self):
self.config = self._load_config()
@ -353,16 +366,22 @@ class HysteriaServer:
self.singbox_generator.set_template_path(self.config.singbox_template_path)
self.subscription_manager = SubscriptionManager(self.hysteria_cli, self.config)
self.template_renderer = TemplateRenderer(self.config.template_dir, self.config)
self.app = web.Application(middlewares=[self._rate_limit_middleware])
safe_subpath = self.validate_and_escape_subpath(self.config.subpath)
self.app.add_routes([
web.get(f'/{safe_subpath}/sub/normal/{{username}}', self.handle)
self.app = web.Application(middlewares=[
self._invalid_endpoint_middleware,
self._rate_limit_middleware,
self._noindex_middleware
])
self.app.router.add_route('*', f'/{safe_subpath}/{{tail:.*}}', self.handle_404)
self.app.router.add_route('*', '/{tail:.*}', self.handle_generic_404)
safe_subpath = self.validate_and_escape_subpath(self.config.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}/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:
domain = os.getenv('HYSTERIA_DOMAIN', 'localhost')
@ -372,8 +391,8 @@ class HysteriaServer:
subpath = os.getenv('SUBPATH', '').strip().strip("/")
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'
singbox_template_path = '/etc/hysteria/core/scripts/normalsub/singbox.json'
@ -411,10 +430,25 @@ class HysteriaServer:
@middleware
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))
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 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:
try:
username = Utils.sanitize_input(request.match_info.get('username', ''), r'^[a-zA-Z0-9_-]+$')
@ -476,15 +510,18 @@ class HysteriaServer:
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:
"""Handles 404 Not Found errors *within* the subpath."""
print(f"404 Not Found (within subpath): {request.path}")
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")
# 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):
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')
web.run_app(self.app, port=self.config.port, ssl_context=ssl_context)
if __name__ == '__main__':
server = HysteriaServer()
server.run()

View File

@ -1,7 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="robots" content="noindex, nofollow, noarchive, nosnippet">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hysteria2 Subscription</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
@ -76,9 +78,11 @@
0% {
transform: translate(0, 0) rotate(0deg);
}
50% {
transform: translate(100px, 100px) rotate(180deg);
}
100% {
transform: translate(0, 0) rotate(360deg);
}
@ -170,7 +174,8 @@
.qr-section {
background: var(--card-bg-light);
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);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.1);
@ -289,7 +294,9 @@
}
@keyframes spin {
to { transform: rotate(360deg); }
to {
transform: rotate(360deg);
}
}
@media (max-width: 768px) {
@ -299,6 +306,7 @@
}
</style>
</head>
<body>
<div class="background-animation"></div>
<div class="loading-indicator" id="loading-indicator">
@ -330,7 +338,8 @@
Used / Total
</div>
<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 }}
</p>
</div>
@ -451,4 +460,5 @@
});
</script>
</body>
</html>