Merge pull request #180 from ReturnFI/beta
Privacy Boost, WARP Improvements & Caddy Proxy Integration
This commit is contained in:
21
changelog
21
changelog
@ -1,15 +1,18 @@
|
|||||||
# [1.10.1] - 2025-05-24
|
# [1.11.0] - 2025-05-28
|
||||||
|
|
||||||
## ✨ New Features
|
|
||||||
|
|
||||||
* 🔐 **feat:** Add option to reset Web Panel admin credentials via CLI
|
#### ⚠️ Attention Required
|
||||||
* 🧩 **feat:** Add "Reset Web Panel credentials" option to Bash menu
|
|
||||||
* 🌐 **feat:** Add API endpoint to fetch IP Limiter configuration
|
|
||||||
|
|
||||||
## 🖌️ UI Improvements
|
🚨 **Important:** Due to changes in the `normalsub` system (now using passwords in links and routing through Caddy),
|
||||||
|
you must **re-activate NormalSUB** from the settings panel after upgrading to **v1.11.0**.
|
||||||
|
|
||||||
* 🧼 **clean:** Removed shield icons from footer for a cleaner look
|
Failure to do so may result in broken subscription links.
|
||||||
|
|
||||||
## 🐛 Fixes
|
---
|
||||||
|
|
||||||
* 📊 **fix:** Align memory usage reporting with `free -m` in `server_info.py`
|
#### ✨ Features & Improvements
|
||||||
|
|
||||||
|
* 🔐 **normalsub:** Use **user password** instead of username in subscription path for improved privacy (harder to enumerate users)
|
||||||
|
* 🔁 **normalsub:** Now uses **Caddy** as a reverse proxy for better performance and flexibility
|
||||||
|
* 🧩 **WARP API:** Adapted backend to support **JSON-based status output**
|
||||||
|
* 🛠️ **Script Update:** WARP status script now outputs clean **JSON**, allowing easier parsing and integration
|
||||||
|
|||||||
@ -193,7 +193,7 @@ def show_uri(args: argparse.Namespace) -> None:
|
|||||||
if args.normalsub and is_service_active("hysteria-normal-sub.service"):
|
if args.normalsub and is_service_active("hysteria-normal-sub.service"):
|
||||||
domain, port, subpath = get_normalsub_domain_and_port()
|
domain, port, subpath = get_normalsub_domain_and_port()
|
||||||
if domain and port:
|
if domain and port:
|
||||||
print(f"\nNormal-SUB Sublink:\nhttps://{domain}:{port}/{subpath}/sub/normal/{args.username}#Hysteria2\n")
|
print(f"\nNormal-SUB Sublink:\nhttps://{domain}:{port}/{subpath}/sub/normal/{auth_password}#Hysteria2\n")
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""Main function to parse arguments and show URIs."""
|
"""Main function to parse arguments and show URIs."""
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import os
|
import os
|
||||||
import ssl
|
|
||||||
import json
|
import json
|
||||||
import subprocess
|
import subprocess
|
||||||
import re
|
import re
|
||||||
@ -23,12 +22,13 @@ load_dotenv()
|
|||||||
@dataclass
|
@dataclass
|
||||||
class AppConfig:
|
class AppConfig:
|
||||||
domain: str
|
domain: str
|
||||||
cert_file: str
|
external_port: int
|
||||||
key_file: str
|
aiohttp_listen_address: str
|
||||||
port: int
|
aiohttp_listen_port: int
|
||||||
sni_file: str
|
sni_file: str
|
||||||
singbox_template_path: str
|
singbox_template_path: str
|
||||||
hysteria_cli_path: str
|
hysteria_cli_path: str
|
||||||
|
users_json_path: str
|
||||||
rate_limit: int
|
rate_limit: int
|
||||||
rate_limit_window: int
|
rate_limit_window: int
|
||||||
sni: str
|
sni: str
|
||||||
@ -66,6 +66,7 @@ class UriComponents:
|
|||||||
@dataclass
|
@dataclass
|
||||||
class UserInfo:
|
class UserInfo:
|
||||||
username: str
|
username: str
|
||||||
|
password: str
|
||||||
upload_bytes: int
|
upload_bytes: int
|
||||||
download_bytes: int
|
download_bytes: int
|
||||||
max_download_bytes: int
|
max_download_bytes: int
|
||||||
@ -154,7 +155,6 @@ class Utils:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def is_valid_url(url: str) -> bool:
|
def is_valid_url(url: str) -> bool:
|
||||||
"""Checks if the given string is a valid URL."""
|
|
||||||
try:
|
try:
|
||||||
result = urlparse(url)
|
result = urlparse(url)
|
||||||
return all([result.scheme, result.netloc])
|
return all([result.scheme, result.netloc])
|
||||||
@ -163,8 +163,9 @@ class Utils:
|
|||||||
|
|
||||||
|
|
||||||
class HysteriaCLI:
|
class HysteriaCLI:
|
||||||
def __init__(self, cli_path: str):
|
def __init__(self, cli_path: str, users_json_path: str):
|
||||||
self.cli_path = cli_path
|
self.cli_path = cli_path
|
||||||
|
self.users_json_path = users_json_path
|
||||||
|
|
||||||
def _run_command(self, args: List[str]) -> str:
|
def _run_command(self, args: List[str]) -> str:
|
||||||
try:
|
try:
|
||||||
@ -173,7 +174,7 @@ class HysteriaCLI:
|
|||||||
stdout, stderr = process.communicate()
|
stdout, stderr = process.communicate()
|
||||||
if process.returncode != 0:
|
if process.returncode != 0:
|
||||||
if "User not found" in stderr:
|
if "User not found" in stderr:
|
||||||
return None # Indicate user not found
|
return None
|
||||||
else:
|
else:
|
||||||
print(f"Hysteria CLI error: {stderr}")
|
print(f"Hysteria CLI error: {stderr}")
|
||||||
raise subprocess.CalledProcessError(process.returncode, command, output=stdout, stderr=stderr)
|
raise subprocess.CalledProcessError(process.returncode, command, output=stdout, stderr=stderr)
|
||||||
@ -182,14 +183,57 @@ class HysteriaCLI:
|
|||||||
print(f"Hysteria CLI error: {e}")
|
print(f"Hysteria CLI error: {e}")
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def get_user_password(self, username: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
with open(self.users_json_path, 'r') as f:
|
||||||
|
users_data = json.load(f)
|
||||||
|
user_details = users_data.get(username)
|
||||||
|
if user_details and 'password' in user_details:
|
||||||
|
return user_details['password']
|
||||||
|
return None
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Error: Users file not found at {self.users_json_path}")
|
||||||
|
return None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"Error: Could not decode JSON from {self.users_json_path}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred while reading users file for password: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
def get_username_by_password(self, password_token: str) -> Optional[str]:
|
||||||
|
try:
|
||||||
|
with open(self.users_json_path, 'r') as f:
|
||||||
|
users_data = json.load(f)
|
||||||
|
for username, details in users_data.items():
|
||||||
|
if details.get('password') == password_token:
|
||||||
|
return username
|
||||||
|
return None
|
||||||
|
except FileNotFoundError:
|
||||||
|
print(f"Error: Users file not found at {self.users_json_path}")
|
||||||
|
return None
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"Error: Could not decode JSON from {self.users_json_path}")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
print(f"An unexpected error occurred while reading users file: {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
def get_user_info(self, username: str) -> Optional[UserInfo]:
|
def get_user_info(self, username: str) -> Optional[UserInfo]:
|
||||||
raw_info_str = self._run_command(['get-user', '-u', username])
|
raw_info_str = self._run_command(['get-user', '-u', username])
|
||||||
if raw_info_str is None:
|
if raw_info_str is None:
|
||||||
return None # User not found
|
return None
|
||||||
|
|
||||||
|
user_password = self.get_user_password(username)
|
||||||
|
if user_password is None:
|
||||||
|
print(f"Warning: Password for user '{username}' could not be fetched from {self.users_json_path}. Cannot create UserInfo.")
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
try:
|
||||||
raw_info = json.loads(raw_info_str)
|
raw_info = json.loads(raw_info_str)
|
||||||
return UserInfo(
|
return UserInfo(
|
||||||
username=username,
|
username=username,
|
||||||
|
password=user_password,
|
||||||
upload_bytes=raw_info.get('upload_bytes', 0),
|
upload_bytes=raw_info.get('upload_bytes', 0),
|
||||||
download_bytes=raw_info.get('download_bytes', 0),
|
download_bytes=raw_info.get('download_bytes', 0),
|
||||||
max_download_bytes=raw_info.get('max_download_bytes', 0),
|
max_download_bytes=raw_info.get('max_download_bytes', 0),
|
||||||
@ -198,7 +242,7 @@ class HysteriaCLI:
|
|||||||
)
|
)
|
||||||
except json.JSONDecodeError as e:
|
except json.JSONDecodeError as e:
|
||||||
print(f"JSONDecodeError: {e}, Raw output: {raw_info_str}")
|
print(f"JSONDecodeError: {e}, Raw output: {raw_info_str}")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_user_uri(self, username: str, ip_version: Optional[str] = None) -> str:
|
def get_user_uri(self, username: str, ip_version: Optional[str] = None) -> str:
|
||||||
if ip_version:
|
if ip_version:
|
||||||
@ -378,7 +422,7 @@ class HysteriaServer:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.config = self._load_config()
|
self.config = self._load_config()
|
||||||
self.rate_limiter = RateLimiter(self.config.rate_limit, self.config.rate_limit_window)
|
self.rate_limiter = RateLimiter(self.config.rate_limit, self.config.rate_limit_window)
|
||||||
self.hysteria_cli = HysteriaCLI(self.config.hysteria_cli_path)
|
self.hysteria_cli = HysteriaCLI(self.config.hysteria_cli_path, self.config.users_json_path)
|
||||||
self.singbox_generator = SingboxConfigGenerator(self.hysteria_cli, self.config.sni)
|
self.singbox_generator = SingboxConfigGenerator(self.hysteria_cli, self.config.sni)
|
||||||
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)
|
||||||
@ -392,36 +436,39 @@ class HysteriaServer:
|
|||||||
safe_subpath = self.validate_and_escape_subpath(self.config.subpath)
|
safe_subpath = self.validate_and_escape_subpath(self.config.subpath)
|
||||||
|
|
||||||
base_path = f'/{safe_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}/sub/normal/{{password_token}}', self.handle)
|
||||||
self.app.router.add_get(f'{base_path}/robots.txt', self.robots_handler)
|
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)
|
||||||
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')
|
||||||
cert_file = os.getenv('HYSTERIA_CERTFILE')
|
external_port = int(os.getenv('HYSTERIA_PORT', '443'))
|
||||||
key_file = os.getenv('HYSTERIA_KEYFILE')
|
aiohttp_listen_address = os.getenv('AIOHTTP_LISTEN_ADDRESS', '127.0.0.1')
|
||||||
port = int(os.getenv('HYSTERIA_PORT', '3326'))
|
aiohttp_listen_port = int(os.getenv('AIOHTTP_LISTEN_PORT', '33261'))
|
||||||
|
|
||||||
subpath = os.getenv('SUBPATH', '').strip().strip("/")
|
subpath = os.getenv('SUBPATH', '').strip().strip("/")
|
||||||
|
if not subpath or not self.is_valid_subpath(subpath):
|
||||||
if not self.is_valid_subpath(subpath):
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Invalid SUBPATH: '{subpath}'. Subpath must contain only alphanumeric characters, hyphens, and underscores.")
|
f"Invalid or empty SUBPATH: '{subpath}'. Subpath must be non-empty and contain only alphanumeric characters.")
|
||||||
|
|
||||||
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'
|
||||||
|
users_json_path = os.getenv('HYSTERIA_USERS_JSON_PATH', '/etc/hysteria/users.json')
|
||||||
rate_limit = 100
|
rate_limit = 100
|
||||||
rate_limit_window = 60
|
rate_limit_window = 60
|
||||||
template_dir = os.path.dirname(__file__)
|
template_dir = os.path.dirname(__file__)
|
||||||
|
|
||||||
sni = self._load_sni_from_env(sni_file)
|
sni = self._load_sni_from_env(sni_file)
|
||||||
return AppConfig(domain=domain, cert_file=cert_file, key_file=key_file, port=port, sni_file=sni_file,
|
return AppConfig(domain=domain, external_port=external_port,
|
||||||
singbox_template_path=singbox_template_path, hysteria_cli_path=hysteria_cli_path,
|
aiohttp_listen_address=aiohttp_listen_address,
|
||||||
rate_limit=rate_limit, rate_limit_window=rate_limit_window, sni=sni, template_dir=template_dir,
|
aiohttp_listen_port=aiohttp_listen_port,
|
||||||
|
sni_file=sni_file,
|
||||||
|
singbox_template_path=singbox_template_path,
|
||||||
|
hysteria_cli_path=hysteria_cli_path,
|
||||||
|
users_json_path=users_json_path,
|
||||||
|
rate_limit=rate_limit, rate_limit_window=rate_limit_window,
|
||||||
|
sni=sni, template_dir=template_dir,
|
||||||
subpath=subpath)
|
subpath=subpath)
|
||||||
|
|
||||||
def _load_sni_from_env(self, sni_file: str) -> str:
|
def _load_sni_from_env(self, sni_file: str) -> str:
|
||||||
@ -435,28 +482,29 @@ class HysteriaServer:
|
|||||||
return "bts.com"
|
return "bts.com"
|
||||||
|
|
||||||
def is_valid_subpath(self, subpath: str) -> bool:
|
def is_valid_subpath(self, subpath: str) -> bool:
|
||||||
"""Validates the subpath using a regex."""
|
return bool(re.match(r"^[a-zA-Z0-9]+$", subpath))
|
||||||
return bool(re.match(r"^[a-zA-Z0-9_-]+$", subpath))
|
|
||||||
|
|
||||||
def validate_and_escape_subpath(self, subpath: str) -> str:
|
def validate_and_escape_subpath(self, subpath: str) -> str:
|
||||||
"""Validates the subpath and returns the escaped version."""
|
|
||||||
if not self.is_valid_subpath(subpath):
|
if not self.is_valid_subpath(subpath):
|
||||||
raise ValueError(f"Invalid subpath: {subpath}")
|
raise ValueError(f"Invalid subpath: {subpath}")
|
||||||
return re.escape(subpath)
|
return re.escape(subpath)
|
||||||
|
|
||||||
@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_hdr = request.headers.get('X-Forwarded-For', request.headers.get('X-Real-IP'))
|
||||||
if not self.rate_limiter.check_limit(client_ip): # type: ignore
|
client_ip = client_ip_hdr.split(',')[0].strip() if client_ip_hdr else request.remote
|
||||||
|
|
||||||
|
if client_ip and not self.rate_limiter.check_limit(client_ip):
|
||||||
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
|
@middleware
|
||||||
async def _invalid_endpoint_middleware(self, request: web.Request, handler):
|
async def _invalid_endpoint_middleware(self, request: web.Request, handler):
|
||||||
path = f'/{self.config.subpath}/'
|
expected_prefix = f'/{self.config.subpath}/'
|
||||||
if not request.path.startswith(path):
|
if not request.path.startswith(expected_prefix):
|
||||||
|
print(f"Warning: Request {request.path} reached aiohttp outside expected subpath {expected_prefix}. Closing connection.")
|
||||||
if request.transport is not None:
|
if request.transport is not None:
|
||||||
request.transport.close()
|
request.transport.close()
|
||||||
raise web.HTTPForbidden()
|
raise web.HTTPForbidden()
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
|
||||||
@ -468,20 +516,27 @@ class HysteriaServer:
|
|||||||
|
|
||||||
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_-]+$')
|
password_token_raw = request.match_info.get('password_token', '')
|
||||||
if not username:
|
if not password_token_raw:
|
||||||
return web.Response(status=400, text="Error: Missing 'username' parameter.")
|
return web.Response(status=400, text="Error: Missing 'password_token' parameter.")
|
||||||
|
|
||||||
|
password_token = Utils.sanitize_input(password_token_raw, r'^[a-zA-Z0-9]+$')
|
||||||
|
|
||||||
|
username = self.hysteria_cli.get_username_by_password(password_token)
|
||||||
|
if username is None:
|
||||||
|
return web.Response(status=404, text="User not found for the provided token.")
|
||||||
|
|
||||||
user_agent = request.headers.get('User-Agent', '').lower()
|
user_agent = request.headers.get('User-Agent', '').lower()
|
||||||
user_info = self.hysteria_cli.get_user_info(username)
|
user_info = self.hysteria_cli.get_user_info(username)
|
||||||
if user_info is None:
|
if user_info is None:
|
||||||
return web.Response(status=404, text=f"User '{username}' not found.")
|
return web.Response(status=404, text=f"User '{username}' details not found.")
|
||||||
|
|
||||||
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']):
|
||||||
return await self._handle_html(request, username, user_info)
|
return await self._handle_html(request, username, user_info)
|
||||||
fragment = request.query.get('fragment', '')
|
fragment = request.query.get('fragment', '')
|
||||||
if not user_agent.startswith('hiddifynext') and ('singbox' in user_agent or 'sing' in user_agent):
|
if not user_agent.startswith('hiddifynext') and ('singbox' in user_agent or 'sing' in user_agent):
|
||||||
return await self._handle_singbox(username, fragment, user_info)
|
return await self._handle_singbox(username, fragment, user_info)
|
||||||
return await self._handle_normalsub(request, username, user_info)
|
return await self._handle_normalsub(request, username, user_info)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return web.Response(status=400, text=f"Error: {e}")
|
return web.Response(status=400, text=f"Error: {e}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -489,7 +544,7 @@ class HysteriaServer:
|
|||||||
return web.Response(status=500, text="Error: Internal server error")
|
return web.Response(status=500, text="Error: Internal server error")
|
||||||
|
|
||||||
async def _handle_html(self, request: web.Request, username: str, user_info: UserInfo) -> web.Response:
|
async def _handle_html(self, request: web.Request, username: str, user_info: UserInfo) -> web.Response:
|
||||||
context = await self._get_template_context(username, user_info)
|
context = await self._get_template_context(username, user_info)
|
||||||
return web.Response(text=self.template_renderer.render(context), content_type='text/html')
|
return web.Response(text=self.template_renderer.render(context), content_type='text/html')
|
||||||
|
|
||||||
async def _handle_singbox(self, username: str, fragment: str, user_info: UserInfo) -> web.Response:
|
async def _handle_singbox(self, username: str, fragment: str, user_info: UserInfo) -> web.Response:
|
||||||
@ -503,17 +558,19 @@ class HysteriaServer:
|
|||||||
async def _handle_normalsub(self, request: web.Request, username: str, user_info: UserInfo) -> web.Response:
|
async def _handle_normalsub(self, request: web.Request, username: str, user_info: UserInfo) -> web.Response:
|
||||||
user_agent = request.headers.get('User-Agent', '').lower()
|
user_agent = request.headers.get('User-Agent', '').lower()
|
||||||
subscription = self.subscription_manager.get_normal_subscription(username, user_agent)
|
subscription = self.subscription_manager.get_normal_subscription(username, user_agent)
|
||||||
if subscription == "User not found":
|
if subscription == "User not found": # Should be caught earlier by user_info check
|
||||||
return web.Response(status=404, text=f"User '{username}' not found.")
|
return web.Response(status=404, text=f"User '{username}' not found.")
|
||||||
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, user_info: UserInfo) -> TemplateContext:
|
async def _get_template_context(self, username: str, user_info: UserInfo) -> TemplateContext:
|
||||||
ipv4_uri, ipv6_uri = self.hysteria_cli.get_uris(username)
|
ipv4_uri, ipv6_uri = self.hysteria_cli.get_uris(username)
|
||||||
|
port_str = f":{self.config.external_port}" if self.config.external_port not in [80, 443, 0] else ""
|
||||||
|
base_url = f"https://{self.config.domain}{port_str}"
|
||||||
|
|
||||||
base_url = f"https://{self.config.domain}:{self.config.port}"
|
|
||||||
if not Utils.is_valid_url(base_url):
|
if not Utils.is_valid_url(base_url):
|
||||||
raise ValueError(f"Invalid base URL constructed: {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/{username}"
|
|
||||||
|
sub_link = f"{base_url}/{self.config.subpath}/sub/normal/{user_info.password}"
|
||||||
|
|
||||||
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)
|
||||||
@ -535,23 +592,20 @@ class HysteriaServer:
|
|||||||
async def robots_handler(self, request: web.Request) -> web.Response:
|
async def robots_handler(self, request: web.Request) -> web.Response:
|
||||||
return web.Response(text="User-agent: *\nDisallow: /", content_type="text/plain")
|
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_subpath(self, request: web.Request) -> web.Response:
|
||||||
"""Handles 404 Not Found errors *within* the subpath."""
|
print(f"404 Not Found (within subpath, unhandled by specific routes): {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:
|
|
||||||
# """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):
|
def run(self):
|
||||||
ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
print(f"Starting Hysteria Normalsub server on {self.config.aiohttp_listen_address}:{self.config.aiohttp_listen_port}")
|
||||||
ssl_context.load_cert_chain(certfile=self.config.cert_file, keyfile=self.config.key_file)
|
print(f"External access via Caddy should be at https://{self.config.domain}:{self.config.external_port}/{self.config.subpath}/sub/normal/<USER_PASSWORD>")
|
||||||
ssl_context.set_ciphers('ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384')
|
web.run_app(
|
||||||
web.run_app(self.app, port=self.config.port, ssl_context=ssl_context)
|
self.app,
|
||||||
|
host=self.config.aiohttp_listen_address,
|
||||||
|
port=self.config.aiohttp_listen_port
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
server = HysteriaServer()
|
server = HysteriaServer()
|
||||||
server.run()
|
server.run()
|
||||||
@ -2,30 +2,100 @@
|
|||||||
source /etc/hysteria/core/scripts/utils.sh
|
source /etc/hysteria/core/scripts/utils.sh
|
||||||
define_colors
|
define_colors
|
||||||
|
|
||||||
|
CADDY_CONFIG_FILE_NORMALSUB="/etc/hysteria/core/scripts/normalsub/Caddyfile.normalsub"
|
||||||
|
NORMALSUB_ENV_FILE="/etc/hysteria/core/scripts/normalsub/.env"
|
||||||
|
DEFAULT_AIOHTTP_LISTEN_ADDRESS="127.0.0.1"
|
||||||
|
DEFAULT_AIOHTTP_LISTEN_PORT="28261"
|
||||||
|
|
||||||
|
install_caddy_if_needed() {
|
||||||
|
if command -v caddy &> /dev/null; then
|
||||||
|
echo -e "${green}Caddy is already installed.${NC}"
|
||||||
|
if systemctl list-units --full -all | grep -q 'caddy.service'; then
|
||||||
|
if systemctl is-active --quiet caddy.service; then
|
||||||
|
echo -e "${yellow}Stopping and disabling default caddy.service...${NC}"
|
||||||
|
systemctl stop caddy > /dev/null 2>&1
|
||||||
|
systemctl disable caddy > /dev/null 2>&1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo -e "${yellow}Installing Caddy...${NC}"
|
||||||
|
sudo apt update -y > /dev/null 2>&1
|
||||||
|
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https curl > /dev/null 2>&1
|
||||||
|
curl -fsSL https://dl.cloudsmith.io/public/caddy/stable/gpg.key | sudo tee /etc/apt/trusted.gpg.d/caddy-stable.asc > /dev/null 2>&1
|
||||||
|
echo "deb [signed-by=/etc/apt/trusted.gpg.d/caddy-stable.asc] https://dl.cloudsmith.io/public/caddy/stable/deb/ubuntu $(lsb_release -cs) main" | sudo tee /etc/apt/sources.list.d/caddy-stable.list > /dev/null 2>&1
|
||||||
|
sudo apt update -y > /dev/null 2>&1
|
||||||
|
sudo apt install -y caddy
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo -e "${red}Error: Failed to install Caddy. ${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
systemctl stop caddy > /dev/null 2>&1
|
||||||
|
systemctl disable caddy > /dev/null 2>&1
|
||||||
|
echo -e "${green}Caddy installed successfully. ${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
update_env_file() {
|
update_env_file() {
|
||||||
local domain=$1
|
local domain=$1
|
||||||
local port=$2
|
local external_port=$2
|
||||||
local cert_dir="/etc/letsencrypt/live/$domain"
|
local aiohttp_listen_address=$3
|
||||||
|
local aiohttp_listen_port=$4
|
||||||
|
local subpath_val=$(pwgen -s 32 1)
|
||||||
|
|
||||||
cat <<EOL > /etc/hysteria/core/scripts/normalsub/.env
|
cat <<EOL > "$NORMALSUB_ENV_FILE"
|
||||||
HYSTERIA_DOMAIN=$domain
|
HYSTERIA_DOMAIN=$domain
|
||||||
HYSTERIA_PORT=$port
|
HYSTERIA_PORT=$external_port
|
||||||
HYSTERIA_CERTFILE=$cert_dir/fullchain.pem
|
AIOHTTP_LISTEN_ADDRESS=$aiohttp_listen_address
|
||||||
HYSTERIA_KEYFILE=$cert_dir/privkey.pem
|
AIOHTTP_LISTEN_PORT=$aiohttp_listen_port
|
||||||
SUBPATH=$(pwgen -s 32 1)
|
SUBPATH=$subpath_val
|
||||||
EOL
|
EOL
|
||||||
}
|
}
|
||||||
|
|
||||||
create_service_file() {
|
update_caddy_file_normalsub() {
|
||||||
|
local domain=$1
|
||||||
|
local external_port=$2
|
||||||
|
local subpath_val=$3
|
||||||
|
local aiohttp_address=$4
|
||||||
|
local aiohttp_port=$5
|
||||||
|
|
||||||
|
cat <<EOL > "$CADDY_CONFIG_FILE_NORMALSUB"
|
||||||
|
# Global configuration
|
||||||
|
{
|
||||||
|
admin off
|
||||||
|
auto_https disable_redirects
|
||||||
|
}
|
||||||
|
|
||||||
|
$domain:$external_port {
|
||||||
|
encode gzip zstd
|
||||||
|
|
||||||
|
route /$subpath_val/* {
|
||||||
|
reverse_proxy $aiohttp_address:$aiohttp_port {
|
||||||
|
header_up X-Real-IP {remote_host}
|
||||||
|
header_up X-Forwarded-For {remote_host}
|
||||||
|
header_up X-Forwarded-Port {server_port}
|
||||||
|
header_up X-Forwarded-Proto {scheme}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@blocked {
|
||||||
|
not path /$subpath_val/*
|
||||||
|
}
|
||||||
|
abort @blocked
|
||||||
|
}
|
||||||
|
EOL
|
||||||
|
}
|
||||||
|
|
||||||
|
create_normalsub_python_service_file() {
|
||||||
cat <<EOL > /etc/systemd/system/hysteria-normal-sub.service
|
cat <<EOL > /etc/systemd/system/hysteria-normal-sub.service
|
||||||
[Unit]
|
[Unit]
|
||||||
Description=normalsub Python Service
|
Description=Hysteria Normalsub Python Service
|
||||||
After=network.target
|
After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
ExecStart=/bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/hysteria2_venv/bin/python /etc/hysteria/core/scripts/normalsub/normalsub.py'
|
ExecStart=/bin/bash -c 'source /etc/hysteria/hysteria2_venv/bin/activate && /etc/hysteria/hysteria2_venv/bin/python /etc/hysteria/core/scripts/normalsub/normalsub.py'
|
||||||
WorkingDirectory=/etc/hysteria/core/scripts/normalsub
|
WorkingDirectory=/etc/hysteria/core/scripts/normalsub
|
||||||
EnvironmentFile=/etc/hysteria/core/scripts/normalsub/.env
|
EnvironmentFile=$NORMALSUB_ENV_FILE
|
||||||
Restart=always
|
Restart=always
|
||||||
User=root
|
User=root
|
||||||
Group=root
|
Group=root
|
||||||
@ -35,107 +105,120 @@ WantedBy=multi-user.target
|
|||||||
EOL
|
EOL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
create_caddy_normalsub_service_file() {
|
||||||
|
cat <<EOL > /etc/systemd/system/hysteria-caddy-normalsub.service
|
||||||
|
[Unit]
|
||||||
|
Description=Caddy for Hysteria Normalsub
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/etc/hysteria/core/scripts/normalsub
|
||||||
|
ExecStart=/usr/bin/caddy run --environ --config $CADDY_CONFIG_FILE_NORMALSUB
|
||||||
|
ExecReload=/usr/bin/caddy reload --config $CADDY_CONFIG_FILE_NORMALSUB --force
|
||||||
|
TimeoutStopSec=5s
|
||||||
|
LimitNOFILE=1048576
|
||||||
|
PrivateTmp=true
|
||||||
|
User=root
|
||||||
|
Group=root
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
}
|
||||||
|
|
||||||
start_service() {
|
start_service() {
|
||||||
local domain=$1
|
local domain=$1
|
||||||
local port=$2
|
local external_port=$2
|
||||||
|
|
||||||
if systemctl is-active --quiet hysteria-normal-sub.service; then
|
install_caddy_if_needed
|
||||||
echo "The hysteria-normal-sub.service is already running."
|
|
||||||
return
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Checking SSL certificates for $domain..."
|
local aiohttp_listen_address="$DEFAULT_AIOHTTP_LISTEN_ADDRESS"
|
||||||
if certbot certificates | grep -q "$domain"; then
|
local aiohttp_listen_port="$DEFAULT_AIOHTTP_LISTEN_PORT"
|
||||||
echo -e "${yellow}Certificate for $domain already exists. Renewing...${NC}"
|
|
||||||
certbot renew --cert-name "$domain"
|
update_env_file "$domain" "$external_port" "$aiohttp_listen_address" "$aiohttp_listen_port"
|
||||||
if [ $? -ne 0 ]; then
|
source "$NORMALSUB_ENV_FILE" # To get SUBPATH for Caddyfile
|
||||||
echo -e "${red}Error: Failed to renew SSL certificate. ${NC}"
|
|
||||||
exit 1
|
update_caddy_file_normalsub "$HYSTERIA_DOMAIN" "$HYSTERIA_PORT" "$SUBPATH" "$AIOHTTP_LISTEN_ADDRESS" "$AIOHTTP_LISTEN_PORT"
|
||||||
fi
|
|
||||||
echo -e "${green}Certificate renewed successfully. ${NC}"
|
create_normalsub_python_service_file
|
||||||
else
|
create_caddy_normalsub_service_file
|
||||||
echo -e "${yellow}Requesting new certificate for $domain...${NC}"
|
|
||||||
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d "$domain"
|
|
||||||
if [ $? -ne 0 ]; then
|
|
||||||
echo -e "${red}Error: Failed to generate SSL certificate. ${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
echo -e "${green}Certificate generated successfully. ${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
update_env_file "$domain" "$port"
|
|
||||||
create_service_file
|
|
||||||
chown -R hysteria:hysteria "/etc/letsencrypt/live/$domain"
|
|
||||||
chown -R hysteria:hysteria /etc/hysteria/core/scripts/normalsub
|
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
|
|
||||||
systemctl enable hysteria-normal-sub.service > /dev/null 2>&1
|
systemctl enable hysteria-normal-sub.service > /dev/null 2>&1
|
||||||
systemctl start hysteria-normal-sub.service > /dev/null 2>&1
|
systemctl start hysteria-normal-sub.service
|
||||||
systemctl daemon-reload > /dev/null 2>&1
|
|
||||||
|
systemctl enable hysteria-caddy-normalsub.service > /dev/null 2>&1
|
||||||
|
systemctl start hysteria-caddy-normalsub.service
|
||||||
|
|
||||||
if systemctl is-active --quiet hysteria-normal-sub.service; then
|
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. The service is now running on port $port. ${NC}"
|
echo -e "${green}Normalsub service setup completed.${NC}"
|
||||||
|
echo -e "${green}Access base URL: https://$HYSTERIA_DOMAIN:$HYSTERIA_PORT/$SUBPATH/sub/normal/{username}${NC}"
|
||||||
else
|
else
|
||||||
echo -e "${red}normalsub setup completed. The service failed to start. ${NC}"
|
echo -e "${red}Normalsub setup completed, but one or more services failed to start.${NC}"
|
||||||
|
systemctl status hysteria-normal-sub.service --no-pager
|
||||||
|
systemctl status hysteria-caddy-normalsub.service --no-pager
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
stop_service() {
|
stop_service() {
|
||||||
if [ -f /etc/hysteria/core/scripts/normalsub/.env ]; then
|
echo -e "${yellow}Stopping Hysteria Normalsub Python service...${NC}"
|
||||||
source /etc/hysteria/core/scripts/normalsub/.env
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -n "$HYSTERIA_DOMAIN" ]; then
|
|
||||||
echo -e "${yellow}Deleting SSL certificate for domain: $HYSTERIA_DOMAIN...${NC}"
|
|
||||||
certbot delete --cert-name "$HYSTERIA_DOMAIN" --non-interactive > /dev/null 2>&1
|
|
||||||
else
|
|
||||||
echo -e "${red}HYSTERIA_DOMAIN not found in .env. Skipping certificate deletion.${NC}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
systemctl stop hysteria-normal-sub.service > /dev/null 2>&1
|
systemctl stop hysteria-normal-sub.service > /dev/null 2>&1
|
||||||
systemctl disable hysteria-normal-sub.service > /dev/null 2>&1
|
systemctl disable hysteria-normal-sub.service > /dev/null 2>&1
|
||||||
|
echo -e "${yellow}Stopping Caddy service for Normalsub...${NC}"
|
||||||
|
systemctl stop hysteria-caddy-normalsub.service > /dev/null 2>&1
|
||||||
|
systemctl disable hysteria-caddy-normalsub.service > /dev/null 2>&1
|
||||||
|
|
||||||
systemctl daemon-reload > /dev/null 2>&1
|
systemctl daemon-reload > /dev/null 2>&1
|
||||||
|
|
||||||
rm -f /etc/hysteria/core/scripts/normalsub/.env
|
rm -f "$NORMALSUB_ENV_FILE"
|
||||||
|
rm -f "$CADDY_CONFIG_FILE_NORMALSUB"
|
||||||
|
rm -f /etc/systemd/system/hysteria-normal-sub.service
|
||||||
|
rm -f /etc/systemd/system/hysteria-caddy-normalsub.service
|
||||||
|
systemctl daemon-reload > /dev/null 2>&1
|
||||||
|
|
||||||
echo -e "${yellow}normalsub service stopped and disabled. .env file removed.${NC}"
|
echo -e "${green}Normalsub services stopped and disabled. Configuration files removed.${NC}"
|
||||||
}
|
}
|
||||||
|
|
||||||
edit_subpath() {
|
edit_subpath() {
|
||||||
local new_path="$1"
|
local new_path="$1"
|
||||||
local env_file="/etc/hysteria/core/scripts/normalsub/.env"
|
|
||||||
|
if [ ! -f "$NORMALSUB_ENV_FILE" ]; then
|
||||||
|
echo -e "${red}Error: .env file ($NORMALSUB_ENV_FILE) not found. Please run the start command first.${NC}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ ! "$new_path" =~ ^[a-zA-Z0-9]+$ ]]; then
|
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}"
|
echo -e "${red}Error: New subpath must contain only alphanumeric characters (a-z, A-Z, 0-9) and cannot be empty.${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ ! -f "$env_file" ]; then
|
source "$NORMALSUB_ENV_FILE"
|
||||||
echo -e "${red}Error: .env file ($env_file) not found. Please run the start command first.${NC}"
|
local old_subpath="$SUBPATH"
|
||||||
exit 1
|
|
||||||
fi
|
sed -i "s|^SUBPATH=.*|SUBPATH=$new_path|" "$NORMALSUB_ENV_FILE"
|
||||||
|
echo -e "${green}SUBPATH updated to $new_path in $NORMALSUB_ENV_FILE.${NC}"
|
||||||
|
|
||||||
if grep -q "^SUBPATH=" "$env_file"; then
|
update_caddy_file_normalsub "$HYSTERIA_DOMAIN" "$HYSTERIA_PORT" "$new_path" "$AIOHTTP_LISTEN_ADDRESS" "$AIOHTTP_LISTEN_PORT"
|
||||||
sed -i "s|^SUBPATH=.*|SUBPATH=$new_path|" "$env_file"
|
echo -e "${green}Caddyfile for Normalsub updated.${NC}"
|
||||||
else
|
|
||||||
echo "SUBPATH=$new_path" >> "$env_file"
|
|
||||||
fi
|
|
||||||
echo -e "${green}SUBPATH updated to $new_path in $env_file.${NC}"
|
|
||||||
|
|
||||||
echo -e "${yellow}Restarting hysteria-normal-sub service...${NC}"
|
echo -e "${yellow}Restarting hysteria-normal-sub service...${NC}"
|
||||||
systemctl daemon-reload
|
|
||||||
systemctl restart hysteria-normal-sub.service
|
systemctl restart hysteria-normal-sub.service
|
||||||
|
echo -e "${yellow}Reloading hysteria-caddy-normalsub service...${NC}"
|
||||||
|
systemctl restart hysteria-caddy-normalsub.service
|
||||||
|
|
||||||
if systemctl is-active --quiet hysteria-normal-sub.service; then
|
if systemctl is-active --quiet hysteria-normal-sub.service && systemctl is-active --quiet hysteria-caddy-normalsub.service; then
|
||||||
echo -e "${green}hysteria-normal-sub service restarted successfully.${NC}"
|
echo -e "${green}Services restarted/reloaded successfully.${NC}"
|
||||||
|
echo -e "${green}New access base URL: https://$HYSTERIA_DOMAIN:$HYSTERIA_PORT/$new_path/sub/normal/{username}${NC}"
|
||||||
else
|
else
|
||||||
echo -e "${red}Error: hysteria-normal-sub service failed to restart. Please check logs.${NC}"
|
echo -e "${red}Error: One or more services failed to restart/reload. Please check logs.${NC}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
case "$1" in
|
case "$1" in
|
||||||
start)
|
start)
|
||||||
if [ -z "$2" ] || [ -z "$3" ]; then
|
if [ -z "$2" ] || [ -z "$3" ]; then
|
||||||
echo -e "${red}Usage: $0 start <DOMAIN> <PORT> ${NC}"
|
echo -e "${red}Usage: $0 start <EXTERNAL_DOMAIN> <EXTERNAL_PORT>${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
start_service "$2" "$3"
|
start_service "$2" "$3"
|
||||||
@ -145,13 +228,13 @@ case "$1" in
|
|||||||
;;
|
;;
|
||||||
edit_subpath)
|
edit_subpath)
|
||||||
if [ -z "$2" ]; then
|
if [ -z "$2" ]; then
|
||||||
echo -e "${red}Usage: $0 edit_subpath <NEW_SUBPATH> ${NC}"
|
echo -e "${red}Usage: $0 edit_subpath <NEW_SUBPATH>${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
edit_subpath "$2"
|
edit_subpath "$2"
|
||||||
;;
|
;;
|
||||||
*)
|
*)
|
||||||
echo -e "${red}Usage: $0 {start <DOMAIN> <PORT> | stop | edit_subpath <NEW_SUBPATH>} ${NC}"
|
echo -e "${red}Usage: $0 {start <EXTERNAL_DOMAIN> <EXTERNAL_PORT> | stop | edit_subpath <NEW_SUBPATH>}${NC}"
|
||||||
exit 1
|
exit 1
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
@ -7,6 +7,7 @@ declare -a services=(
|
|||||||
"hysteria-caddy.service"
|
"hysteria-caddy.service"
|
||||||
"hysteria-telegram-bot.service"
|
"hysteria-telegram-bot.service"
|
||||||
"hysteria-normal-sub.service"
|
"hysteria-normal-sub.service"
|
||||||
|
"hysteria-caddy-normalsub.service"
|
||||||
# "hysteria-singbox.service"
|
# "hysteria-singbox.service"
|
||||||
"hysteria-ip-limit.service"
|
"hysteria-ip-limit.service"
|
||||||
"wg-quick@wgcf.service"
|
"wg-quick@wgcf.service"
|
||||||
|
|||||||
@ -10,65 +10,44 @@ if str(core_scripts_dir) not in sys.path:
|
|||||||
|
|
||||||
from paths import *
|
from paths import *
|
||||||
|
|
||||||
colors = {
|
|
||||||
"cyan": "\033[96m",
|
|
||||||
"green": "\033[92m",
|
|
||||||
"red": "\033[91m",
|
|
||||||
"purple": "\033[95m",
|
|
||||||
"end": "\033[0m"
|
|
||||||
}
|
|
||||||
|
|
||||||
def echo_status(label, is_active):
|
|
||||||
status = f"{colors['green']}Active{colors['end']}" if is_active else f"{colors['red']}Inactive{colors['end']}"
|
|
||||||
print(f"{colors['cyan']}{label}:{colors['end']} {status}")
|
|
||||||
|
|
||||||
def check_warp_configuration():
|
def check_warp_configuration():
|
||||||
|
status_data = {}
|
||||||
|
|
||||||
if not Path(CONFIG_FILE).exists():
|
if not Path(CONFIG_FILE).exists():
|
||||||
print(f"{colors['red']}Error: Config file not found at {CONFIG_FILE}{colors['end']}")
|
status_data["error"] = f"Config file not found at {CONFIG_FILE}"
|
||||||
|
print(json.dumps(status_data, indent=4))
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(CONFIG_FILE, "r") as f:
|
||||||
|
config = json.load(f)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
status_data["error"] = f"Invalid JSON in config file: {CONFIG_FILE}"
|
||||||
|
print(json.dumps(status_data, indent=4))
|
||||||
return
|
return
|
||||||
|
|
||||||
with open(CONFIG_FILE, "r") as f:
|
|
||||||
config = json.load(f)
|
|
||||||
|
|
||||||
acl_inline = config.get("acl", {}).get("inline", [])
|
acl_inline = config.get("acl", {}).get("inline", [])
|
||||||
|
|
||||||
def contains_warp(rule_prefixes):
|
def contains_warp(rule_prefixes):
|
||||||
return any(rule.startswith(prefix) for rule in acl_inline for prefix in rule_prefixes)
|
return any(rule.startswith(prefix) for rule in acl_inline for prefix in rule_prefixes)
|
||||||
|
|
||||||
print("--------------------------------")
|
status_data["all_traffic_via_warp"] = contains_warp(["warps(all)"])
|
||||||
print(f"{colors['purple']}Current WARP Configuration:{colors['end']}")
|
status_data["popular_sites_via_warp"] = contains_warp([
|
||||||
|
"warps(geosite:google)",
|
||||||
echo_status(
|
"warps(geoip:google)",
|
||||||
"All traffic",
|
"warps(geosite:netflix)",
|
||||||
contains_warp(["warps(all)"])
|
"warps(geosite:spotify)",
|
||||||
)
|
"warps(geosite:openai)",
|
||||||
|
"warps(geoip:openai)"
|
||||||
echo_status(
|
])
|
||||||
"Popular sites (Google, Netflix, etc.)",
|
status_data["domestic_sites_via_warp"] = contains_warp([
|
||||||
contains_warp([
|
"warps(geosite:ir)",
|
||||||
"warps(geosite:google)",
|
"warps(geoip:ir)"
|
||||||
"warps(geoip:google)",
|
])
|
||||||
"warps(geosite:netflix)",
|
status_data["block_adult_content"] = "reject(geosite:nsfw)" in acl_inline
|
||||||
"warps(geosite:spotify)",
|
|
||||||
"warps(geosite:openai)",
|
print(json.dumps(status_data, indent=4))
|
||||||
"warps(geoip:openai)"
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
echo_status(
|
|
||||||
"Domestic sites (geosite:ir, geoip:ir)",
|
|
||||||
contains_warp([
|
|
||||||
"warps(geosite:ir)",
|
|
||||||
"warps(geoip:ir)"
|
|
||||||
])
|
|
||||||
)
|
|
||||||
|
|
||||||
echo_status(
|
|
||||||
"Block adult content",
|
|
||||||
"reject(geosite:nsfw)" in acl_inline
|
|
||||||
)
|
|
||||||
|
|
||||||
print("--------------------------------")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
check_warp_configuration()
|
check_warp_configuration()
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import re
|
import json
|
||||||
from fastapi import APIRouter, HTTPException
|
from fastapi import APIRouter, HTTPException
|
||||||
from ..schema.response import DetailResponse
|
from ..schema.response import DetailResponse
|
||||||
from ..schema.config.warp import ConfigureInputBody, StatusResponse
|
from ..schema.config.warp import ConfigureInputBody, StatusResponse
|
||||||
@ -69,74 +69,21 @@ async def configure(body: ConfigureInputBody):
|
|||||||
|
|
||||||
@router.get('/status', response_model=StatusResponse, summary='Get WARP Status')
|
@router.get('/status', response_model=StatusResponse, summary='Get WARP Status')
|
||||||
async def status():
|
async def status():
|
||||||
"""
|
|
||||||
Retrieves the current status of WARP.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
StatusResponse: A response model containing the current WARP status details.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
HTTPException: If the WARP status is not available (404) or if there is an error processing the request (400).
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
if res := cli_api.warp_status():
|
status_json_str = cli_api.warp_status()
|
||||||
return __parse_status(res)
|
if not status_json_str:
|
||||||
raise HTTPException(status_code=404, detail='WARP status not available.')
|
raise HTTPException(status_code=404, detail='WARP status not available.')
|
||||||
|
|
||||||
|
status_data = json.loads(status_json_str)
|
||||||
|
|
||||||
|
if "error" in status_data:
|
||||||
|
raise HTTPException(status_code=500, detail=f'Error getting WARP status: {status_data["error"]}')
|
||||||
|
|
||||||
|
return StatusResponse(**status_data)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
raise HTTPException(status_code=500, detail='Error decoding WARP status JSON.')
|
||||||
|
except HTTPException as e:
|
||||||
|
raise e
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
|
raise HTTPException(status_code=400, detail=f'Error: {str(e)}')
|
||||||
|
|
||||||
|
|
||||||
def __parse_status(status: str) -> StatusResponse:
|
|
||||||
"""
|
|
||||||
Parses the output of the WARP status command to extract the current configuration settings.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
status: The output of the WARP status command as a string.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
StatusResponse: A response model containing the current WARP status details.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If the WARP status is invalid or incomplete.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Example output(status) from cli_api.warp_status():
|
|
||||||
# --------------------------------
|
|
||||||
# Current WARP Configuration:
|
|
||||||
# All traffic: Inactive
|
|
||||||
# Popular sites (Google, Netflix, etc.): Inactive
|
|
||||||
# Domestic sites (geosite:ir, geoip:ir): Inactive
|
|
||||||
# Block adult content: Inactive
|
|
||||||
# --------------------------------
|
|
||||||
data = {}
|
|
||||||
|
|
||||||
# Remove ANSI escape sequences(colors) (e.g., \x1b[1;35m)
|
|
||||||
clean_status = re.sub(r'\x1b\[[0-9;]*m', '', status)
|
|
||||||
|
|
||||||
for line in clean_status.split('\n'):
|
|
||||||
if ':' not in line:
|
|
||||||
continue
|
|
||||||
if 'Current WARP Configuration:' in line:
|
|
||||||
continue
|
|
||||||
key, _, value = line.partition(':')
|
|
||||||
key = key.strip().lower()
|
|
||||||
value = value.strip()
|
|
||||||
|
|
||||||
if not key or not value:
|
|
||||||
continue
|
|
||||||
|
|
||||||
if 'all traffic' in key:
|
|
||||||
data['all_traffic'] = value == 'active'
|
|
||||||
elif 'popular sites' in key:
|
|
||||||
data['popular_sites'] = value == 'active'
|
|
||||||
elif 'domestic sites' in key:
|
|
||||||
data['domestic_sites'] = value == 'active'
|
|
||||||
elif 'block adult content' in key:
|
|
||||||
data['block_adult_sites'] = value == 'active'
|
|
||||||
|
|
||||||
if not data:
|
|
||||||
raise ValueError('Invalid WARP status')
|
|
||||||
try:
|
|
||||||
return StatusResponse(**data)
|
|
||||||
except Exception as e:
|
|
||||||
raise ValueError(f'Invalid or incomplete WARP status: {e}')
|
|
||||||
@ -10,7 +10,7 @@ class ConfigureInputBody(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class StatusResponse(BaseModel):
|
class StatusResponse(BaseModel):
|
||||||
all_traffic: bool
|
all_traffic_via_warp: bool
|
||||||
popular_sites: bool
|
popular_sites_via_warp: bool
|
||||||
domestic_sites: bool
|
domestic_sites_via_warp: bool
|
||||||
block_adult_sites: bool
|
block_adult_content: bool
|
||||||
61
menu.sh
61
menu.sh
@ -383,7 +383,33 @@ warp_configure_handler() {
|
|||||||
local service_name="wg-quick@wgcf.service"
|
local service_name="wg-quick@wgcf.service"
|
||||||
|
|
||||||
if systemctl is-active --quiet "$service_name"; then
|
if systemctl is-active --quiet "$service_name"; then
|
||||||
python3 $CLI_PATH warp-status
|
echo -e "${cyan}=== WARP Status ===${NC}"
|
||||||
|
|
||||||
|
status_json=$(python3 $CLI_PATH warp-status)
|
||||||
|
|
||||||
|
all_traffic=$(echo "$status_json" | grep -o '"all_traffic_via_warp": *[^,}]*' | cut -d':' -f2 | tr -d ' "')
|
||||||
|
popular_sites=$(echo "$status_json" | grep -o '"popular_sites_via_warp": *[^,}]*' | cut -d':' -f2 | tr -d ' "')
|
||||||
|
domestic_sites=$(echo "$status_json" | grep -o '"domestic_sites_via_warp": *[^,}]*' | cut -d':' -f2 | tr -d ' "')
|
||||||
|
block_adult=$(echo "$status_json" | grep -o '"block_adult_content": *[^,}]*' | cut -d':' -f2 | tr -d ' "')
|
||||||
|
|
||||||
|
display_status() {
|
||||||
|
local label="$1"
|
||||||
|
local status="$2"
|
||||||
|
if [ "$status" = "true" ]; then
|
||||||
|
echo -e " ${green}✓${NC} $label: ${green}Enabled${NC}"
|
||||||
|
else
|
||||||
|
echo -e " ${red}✗${NC} $label: ${red}Disabled${NC}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
display_status "All Traffic via WARP" "$all_traffic"
|
||||||
|
display_status "Popular Sites via WARP" "$popular_sites"
|
||||||
|
display_status "Domestic Sites via WARP" "$domestic_sites"
|
||||||
|
display_status "Block Adult Content" "$block_adult"
|
||||||
|
|
||||||
|
echo -e "${cyan}==================${NC}"
|
||||||
|
echo
|
||||||
|
|
||||||
echo "Configure WARP Options:"
|
echo "Configure WARP Options:"
|
||||||
echo "1. Use WARP for all traffic"
|
echo "1. Use WARP for all traffic"
|
||||||
echo "2. Use WARP for popular sites"
|
echo "2. Use WARP for popular sites"
|
||||||
@ -401,25 +427,38 @@ warp_configure_handler() {
|
|||||||
3) python3 $CLI_PATH configure-warp --domestic-sites ;;
|
3) python3 $CLI_PATH configure-warp --domestic-sites ;;
|
||||||
4) python3 $CLI_PATH configure-warp --block-adult-sites ;;
|
4) python3 $CLI_PATH configure-warp --block-adult-sites ;;
|
||||||
5)
|
5)
|
||||||
ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me)
|
ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me)
|
||||||
cd /etc/warp/ && wgcf status
|
cd /etc/warp/ && wgcf status
|
||||||
echo
|
echo
|
||||||
echo -e "${yellow}Warp IP :${NC} ${cyan}$ip ${NC}" ;;
|
echo -e "${yellow}Warp IP:${NC} ${cyan}$ip${NC}"
|
||||||
|
;;
|
||||||
6)
|
6)
|
||||||
old_ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me)
|
old_ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me)
|
||||||
echo "Current IP address: $old_ip"
|
echo -e "${yellow}Current IP:${NC} ${cyan}$old_ip${NC}"
|
||||||
echo "Restarting $service_name..."
|
echo "Restarting $service_name..."
|
||||||
systemctl restart "$service_name"
|
systemctl restart "$service_name"
|
||||||
sleep 5
|
|
||||||
|
echo -n "Waiting for service to restart"
|
||||||
|
for i in {1..5}; do
|
||||||
|
echo -n "."
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
echo
|
||||||
|
|
||||||
new_ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me)
|
new_ip=$(curl -s --interface wgcf --connect-timeout 0.5 http://v4.ident.me)
|
||||||
echo "New IP address: $new_ip"
|
echo -e "${yellow}New IP:${NC} ${green}$new_ip${NC}"
|
||||||
|
|
||||||
|
if [ "$old_ip" != "$new_ip" ]; then
|
||||||
|
echo -e "${green}✓ IP address changed successfully${NC}"
|
||||||
|
else
|
||||||
|
echo -e "${yellow}⚠ IP address remained the same${NC}"
|
||||||
|
fi
|
||||||
;;
|
;;
|
||||||
0) echo "WARP configuration canceled." ;;
|
0) echo "WARP configuration canceled." ;;
|
||||||
*) echo "Invalid option. Please try again." ;;
|
*) echo -e "${red}Invalid option. Please try again.${NC}" ;;
|
||||||
esac
|
esac
|
||||||
else
|
else
|
||||||
echo "$service_name is not active. Please start the service before configuring WARP."
|
echo -e "${red}$service_name is not active. Please start the service before configuring WARP.${NC}"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -33,8 +33,9 @@ FILES=(
|
|||||||
"$HYSTERIA_INSTALL_DIR/config.json"
|
"$HYSTERIA_INSTALL_DIR/config.json"
|
||||||
"$HYSTERIA_INSTALL_DIR/.configs.env"
|
"$HYSTERIA_INSTALL_DIR/.configs.env"
|
||||||
"$HYSTERIA_INSTALL_DIR/core/scripts/telegrambot/.env"
|
"$HYSTERIA_INSTALL_DIR/core/scripts/telegrambot/.env"
|
||||||
"$HYSTERIA_INSTALL_DIR/core/scripts/singbox/.env"
|
# "$HYSTERIA_INSTALL_DIR/core/scripts/singbox/.env"
|
||||||
"$HYSTERIA_INSTALL_DIR/core/scripts/normalsub/.env"
|
"$HYSTERIA_INSTALL_DIR/core/scripts/normalsub/.env"
|
||||||
|
"$HYSTERIA_INSTALL_DIR/core/scripts/normalsub/Caddyfile.normalsub"
|
||||||
"$HYSTERIA_INSTALL_DIR/core/scripts/webpanel/.env"
|
"$HYSTERIA_INSTALL_DIR/core/scripts/webpanel/.env"
|
||||||
"$HYSTERIA_INSTALL_DIR/core/scripts/webpanel/Caddyfile"
|
"$HYSTERIA_INSTALL_DIR/core/scripts/webpanel/Caddyfile"
|
||||||
)
|
)
|
||||||
@ -80,7 +81,7 @@ info "Setting ownership and permissions..."
|
|||||||
chown hysteria:hysteria "$HYSTERIA_INSTALL_DIR/ca.key" "$HYSTERIA_INSTALL_DIR/ca.crt"
|
chown hysteria:hysteria "$HYSTERIA_INSTALL_DIR/ca.key" "$HYSTERIA_INSTALL_DIR/ca.crt"
|
||||||
chmod 640 "$HYSTERIA_INSTALL_DIR/ca.key" "$HYSTERIA_INSTALL_DIR/ca.crt"
|
chmod 640 "$HYSTERIA_INSTALL_DIR/ca.key" "$HYSTERIA_INSTALL_DIR/ca.crt"
|
||||||
|
|
||||||
chown -R hysteria:hysteria "$HYSTERIA_INSTALL_DIR/core/scripts/singbox"
|
# chown -R hysteria:hysteria "$HYSTERIA_INSTALL_DIR/core/scripts/singbox"
|
||||||
chown -R hysteria:hysteria "$HYSTERIA_INSTALL_DIR/core/scripts/telegrambot"
|
chown -R hysteria:hysteria "$HYSTERIA_INSTALL_DIR/core/scripts/telegrambot"
|
||||||
|
|
||||||
chmod +x "$HYSTERIA_INSTALL_DIR/core/scripts/hysteria2/user.sh"
|
chmod +x "$HYSTERIA_INSTALL_DIR/core/scripts/hysteria2/user.sh"
|
||||||
@ -118,6 +119,7 @@ SERVICES=(
|
|||||||
hysteria-scheduler.service
|
hysteria-scheduler.service
|
||||||
hysteria-telegram-bot.service
|
hysteria-telegram-bot.service
|
||||||
hysteria-normal-sub.service
|
hysteria-normal-sub.service
|
||||||
|
hysteria-caddy-normalsub.service
|
||||||
hysteria-webpanel.service
|
hysteria-webpanel.service
|
||||||
hysteria-ip-limit.service
|
hysteria-ip-limit.service
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user