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()
|
||||
|
||||
|
||||
@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()
|
||||
@ -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>
|
||||
Reference in New Issue
Block a user