import sys sys.path.insert(0, "/etc/hysteria/core/scripts") from fastapi import FastAPI, HTTPException, status, Query from fastapi.responses import JSONResponse from pydantic import BaseModel, Field from typing import Optional, List, Dict, Any from datetime import datetime from . import cli_api import asyncio app = FastAPI( title="Blitz Panel API", description="REST API for Hysteria2 Panel Management", version="1.0.0", ) # ============================================================================ # Pydantic Models # ============================================================================ class UserCreate(BaseModel): username: str = Field(..., min_length=1, max_length=50) traffic_limit: int = Field(..., ge=0, description="Traffic limit in GB") expiration_days: int = Field(..., ge=0) password: Optional[str] = None creation_date: Optional[str] = None unlimited: bool = False note: Optional[str] = None class BulkUserCreate(BaseModel): traffic_gb: float = Field(..., ge=0) expiration_days: int = Field(..., ge=0) count: int = Field(..., ge=1, le=1000) prefix: str = Field(..., min_length=1) start_number: int = Field(1, ge=1) unlimited: bool = False class UserEdit(BaseModel): new_username: Optional[str] = None new_password: Optional[str] = None new_traffic_limit: Optional[int] = Field(None, ge=0) new_expiration_days: Optional[int] = Field(None, ge=0) renew_password: bool = False renew_creation_date: bool = False blocked: Optional[bool] = None unlimited_ip: Optional[bool] = None note: Optional[str] = None class PortChange(BaseModel): port: int = Field(..., ge=1, le=65535) class SNIChange(BaseModel): sni: str = Field(..., min_length=1) class IPConfig(BaseModel): ipv4: Optional[str] = None ipv6: Optional[str] = None class Hysteria2Install(BaseModel): port: int = Field(..., ge=1, le=65535) sni: str = Field(..., min_length=1) class NodeCreate(BaseModel): name: str ip: str sni: Optional[str] = None pinSHA256: Optional[str] = None port: Optional[int] = Field(None, ge=1, le=65535) obfs: Optional[str] = None insecure: Optional[bool] = False class TelegramBotConfig(BaseModel): token: str admin_ids: str backup_interval: Optional[int] = Field(None, ge=1) class WebPanelConfig(BaseModel): domain: str port: int = Field(..., ge=1, le=65535) admin_username: str admin_password: str expiration_minutes: int = Field(30, ge=1) debug: bool = False decoy_path: str = "" class NormalSubConfig(BaseModel): domain: str port: int = Field(..., ge=1, le=65535) class WARPConfig(BaseModel): all_state: Optional[str] = Field(None, pattern="^(on|off)$") popular_sites_state: Optional[str] = Field(None, pattern="^(on|off)$") domestic_sites_state: Optional[str] = Field(None, pattern="^(on|off)$") block_adult_sites_state: Optional[str] = Field(None, pattern="^(on|off)$") class IPLimiterConfig(BaseModel): block_duration: Optional[int] = Field(None, ge=1) max_ips: Optional[int] = Field(None, ge=1) class ExtraProxyConfig(BaseModel): name: str uri: str # ============================================================================ # Error Handler # ============================================================================ def handle_api_error(func): """Decorator to handle CLI API errors""" if asyncio.iscoroutinefunction(func): async def async_wrapper(*args, **kwargs): try: return await func(*args, **kwargs) except cli_api.InvalidInputError as e: raise HTTPException(status_code=400, detail=str(e)) except cli_api.ScriptNotFoundError as e: raise HTTPException(status_code=500, detail=f"Script error: {str(e)}") except cli_api.CommandExecutionError as e: raise HTTPException( status_code=500, detail=f"Execution error: {str(e)}" ) except cli_api.HysteriaError as e: raise HTTPException(status_code=500, detail=str(e)) except Exception as e: raise HTTPException( status_code=500, detail=f"Unexpected error: {str(e)}" ) return async_wrapper else: def sync_wrapper(*args, **kwargs): try: return func(*args, **kwargs) except cli_api.InvalidInputError as e: raise HTTPException(status_code=400, detail=str(e)) except cli_api.ScriptNotFoundError as e: raise HTTPException(status_code=500, detail=f"Script error: {str(e)}") except cli_api.CommandExecutionError as e: raise HTTPException( status_code=500, detail=f"Execution error: {str(e)}" ) except cli_api.HysteriaError as e: raise HTTPException(status_code=500, detail=str(e)) except Exception as e: raise HTTPException( status_code=500, detail=f"Unexpected error: {str(e)}" ) return sync_wrapper # ============================================================================ # Health & Info # ============================================================================ @app.get("/health") async def health_check(): """Health check endpoint""" return {"status": "healthy", "timestamp": datetime.now().isoformat()} @app.get("/api/v1/info/server") async def get_server_info(): """Get server information""" info = cli_api.server_info() return {"data": info} @app.get("/api/v1/info/version") async def get_version(): """Get panel version""" version = cli_api.show_version() return {"version": version} @app.get("/api/v1/info/services") async def get_services_status(): """Get status of all services""" status_data = cli_api.get_services_status() return {"services": status_data} # ============================================================================ # User Management # ============================================================================ @app.get("/api/v1/users") async def list_users(): """List all users""" users = cli_api.list_users() return {"users": users or {}} @app.get("/api/v1/users/{username}") async def get_user(username: str): """Get user details""" user = cli_api.get_user(username) if not user: raise HTTPException(status_code=404, detail=f"User '{username}' not found") return {"user": user} @app.post("/api/v1/users", status_code=status.HTTP_201_CREATED) async def create_user(user: UserCreate): """Create a new user""" cli_api.add_user( username=user.username, traffic_limit=user.traffic_limit, expiration_days=user.expiration_days, password=user.password, creation_date=user.creation_date, unlimited=user.unlimited, note=user.note, ) return {"message": f"User '{user.username}' created successfully"} @app.post("/api/v1/users/bulk", status_code=status.HTTP_201_CREATED) async def create_bulk_users(bulk: BulkUserCreate): """Create multiple users at once""" cli_api.bulk_user_add( traffic_gb=bulk.traffic_gb, expiration_days=bulk.expiration_days, count=bulk.count, prefix=bulk.prefix, start_number=bulk.start_number, unlimited=bulk.unlimited, ) return {"message": f"Created {bulk.count} users with prefix '{bulk.prefix}'"} @app.put("/api/v1/users/{username}") async def edit_user(username: str, user: UserEdit): """Edit user details""" cli_api.edit_user( username=username, new_username=user.new_username, new_password=user.new_password, new_traffic_limit=user.new_traffic_limit, new_expiration_days=user.new_expiration_days, renew_password=user.renew_password, renew_creation_date=user.renew_creation_date, blocked=user.blocked, unlimited_ip=user.unlimited_ip, note=user.note, ) return {"message": f"User '{username}' updated successfully"} @app.post("/api/v1/users/{username}/reset") async def reset_user(username: str): """Reset user traffic and dates""" cli_api.reset_user(username) return {"message": f"User '{username}' reset successfully"} @app.delete("/api/v1/users/{username}") async def delete_user(username: str): """Delete a user""" cli_api.remove_users([username]) return {"message": f"User '{username}' deleted successfully"} @app.delete("/api/v1/users") async def delete_multiple_users(usernames: List[str] = Query(...)): """Delete multiple users""" cli_api.remove_users(usernames) return {"message": f"Deleted {len(usernames)} users"} @app.post("/api/v1/users/{username}/kick") async def kick_user(username: str): """Kick user (disconnect)""" cli_api.kick_users_by_name([username]) return {"message": f"User '{username}' kicked successfully"} @app.get("/api/v1/users/{username}/uri") async def get_user_uri( username: str, qrcode: bool = False, ipv: int = 4, all: bool = False, singbox: bool = False, normalsub: bool = False, ): """Get user connection URI""" uri = cli_api.show_user_uri(username, qrcode, ipv, all, singbox, normalsub) return {"uri": uri} @app.get("/api/v1/users/uri/batch") async def get_batch_user_uri(usernames: List[str] = Query(...)): """Get URIs for multiple users""" uris = cli_api.show_user_uri_json(usernames) return {"uris": uris} # ============================================================================ # Hysteria2 Core Management # ============================================================================ @app.post("/api/v1/hysteria2/install") async def install_hysteria2(config: Hysteria2Install): """Install Hysteria2""" cli_api.install_hysteria2(port=config.port, sni=config.sni) return {"message": "Hysteria2 installed successfully"} @app.post("/api/v1/hysteria2/uninstall") async def uninstall_hysteria2(): """Uninstall Hysteria2""" cli_api.uninstall_hysteria2() return {"message": "Hysteria2 uninstalled successfully"} @app.post("/api/v1/hysteria2/update") async def update_hysteria2(): """Update Hysteria2 core""" cli_api.update_hysteria2() return {"message": "Hysteria2 updated successfully"} @app.post("/api/v1/hysteria2/restart") async def restart_hysteria2(): """Restart Hysteria2 service""" cli_api.restart_hysteria2() return {"message": "Hysteria2 restarted successfully"} @app.get("/api/v1/hysteria2/config/port") async def get_port(): """Get current Hysteria2 port""" port = cli_api.get_hysteria2_port() return {"port": port} @app.put("/api/v1/hysteria2/config/port") async def change_port(config: PortChange): """Change Hysteria2 port""" cli_api.change_hysteria2_port(config.port) return {"message": f"Port changed to {config.port}"} @app.get("/api/v1/hysteria2/config/sni") async def get_sni(): """Get current SNI""" sni = cli_api.get_hysteria2_sni() return {"sni": sni} @app.put("/api/v1/hysteria2/config/sni") async def change_sni(config: SNIChange): """Change SNI""" cli_api.change_hysteria2_sni(config.sni) return {"message": f"SNI changed to {config.sni}"} @app.post("/api/v1/hysteria2/obfs/enable") async def enable_obfs(): """Enable obfuscation""" cli_api.enable_hysteria2_obfs() return {"message": "Obfuscation enabled"} @app.post("/api/v1/hysteria2/obfs/disable") async def disable_obfs(): """Disable obfuscation""" cli_api.disable_hysteria2_obfs() return {"message": "Obfuscation disabled"} @app.get("/api/v1/hysteria2/obfs/status") async def check_obfs(): """Check obfuscation status""" status_msg = cli_api.check_hysteria2_obfs() return {"status": status_msg} @app.post("/api/v1/hysteria2/masquerade/enable") async def enable_masquerade(): """Enable masquerade""" result = cli_api.enable_hysteria2_masquerade() return {"message": result} @app.post("/api/v1/hysteria2/masquerade/disable") async def disable_masquerade(): """Disable masquerade""" result = cli_api.disable_hysteria2_masquerade() return {"message": result} @app.get("/api/v1/hysteria2/masquerade/status") async def get_masquerade_status(): """Get masquerade status""" status_msg = cli_api.get_hysteria2_masquerade_status() return {"status": status_msg} # ============================================================================ # Traffic & Statistics # ============================================================================ @app.get("/api/v1/traffic/status") async def get_traffic_status(): """Get traffic status for all users""" data = cli_api.traffic_status(no_gui=True, display_output=False) return {"traffic": data} # ============================================================================ # IP Configuration # ============================================================================ @app.get("/api/v1/config/ip") async def get_ip_addresses(): """Get configured IP addresses""" ipv4, ipv6 = cli_api.get_ip_address() return {"ipv4": ipv4, "ipv6": ipv6} @app.post("/api/v1/config/ip") async def add_ip_addresses(): """Auto-detect and add IP addresses""" cli_api.add_ip_address() return {"message": "IP addresses added"} @app.put("/api/v1/config/ip") async def edit_ip_addresses(config: IPConfig): """Edit IP addresses""" cli_api.edit_ip_address(ipv4=config.ipv4, ipv6=config.ipv6) return {"message": "IP addresses updated"} # ============================================================================ # Advanced Features # ============================================================================ @app.post("/api/v1/advanced/tcp-brutal/install") async def install_tcp_brutal(): """Install TCP Brutal""" cli_api.install_tcp_brutal() return {"message": "TCP Brutal installed"} @app.post("/api/v1/advanced/warp/install") async def install_warp(): """Install WARP""" cli_api.install_warp() return {"message": "WARP installed"} @app.post("/api/v1/advanced/warp/uninstall") async def uninstall_warp(): """Uninstall WARP""" cli_api.uninstall_warp() return {"message": "WARP uninstalled"} @app.put("/api/v1/advanced/warp/configure") async def configure_warp(config: WARPConfig): """Configure WARP settings""" cli_api.configure_warp( all_state=config.all_state, popular_sites_state=config.popular_sites_state, domestic_sites_state=config.domestic_sites_state, block_adult_sites_state=config.block_adult_sites_state, ) return {"message": "WARP configured"} @app.get("/api/v1/advanced/warp/status") async def get_warp_status(): """Get WARP status""" status_data = cli_api.warp_status() return {"status": status_data} # ============================================================================ # Telegram Bot # ============================================================================ @app.post("/api/v1/services/telegram/start") async def start_telegram(config: TelegramBotConfig): """Start Telegram bot""" cli_api.start_telegram_bot( token=config.token, adminid=config.admin_ids, backup_interval=config.backup_interval, ) return {"message": "Telegram bot started"} @app.post("/api/v1/services/telegram/stop") async def stop_telegram(): """Stop Telegram bot""" cli_api.stop_telegram_bot() return {"message": "Telegram bot stopped"} # ============================================================================ # WebPanel # ============================================================================ @app.post("/api/v1/services/webpanel/start") async def start_webpanel(config: WebPanelConfig): """Start WebPanel""" cli_api.start_webpanel( domain=config.domain, port=config.port, admin_username=config.admin_username, admin_password=config.admin_password, expiration_minutes=config.expiration_minutes, debug=config.debug, decoy_path=config.decoy_path, ) return {"message": "WebPanel started"} @app.post("/api/v1/services/webpanel/stop") async def stop_webpanel(): """Stop WebPanel""" cli_api.stop_webpanel() return {"message": "WebPanel stopped"} @app.get("/api/v1/services/webpanel/url") async def get_webpanel_url(): """Get WebPanel URL""" url = cli_api.get_webpanel_url() return {"url": url} # ============================================================================ # Normal-Sub # ============================================================================ @app.post("/api/v1/services/normalsub/start") async def start_normalsub(config: NormalSubConfig): """Start Normal-Sub service""" cli_api.start_normalsub(domain=config.domain, port=config.port) return {"message": "Normal-Sub started"} @app.post("/api/v1/services/normalsub/stop") async def stop_normalsub(): """Stop Normal-Sub service""" cli_api.stop_normalsub() return {"message": "Normal-Sub stopped"} # ============================================================================ # IP Limiter # ============================================================================ @app.post("/api/v1/services/ip-limiter/start") async def start_ip_limiter(): """Start IP limiter service""" cli_api.start_ip_limiter() return {"message": "IP limiter started"} @app.post("/api/v1/services/ip-limiter/stop") async def stop_ip_limiter(): """Stop IP limiter service""" cli_api.stop_ip_limiter() return {"message": "IP limiter stopped"} @app.put("/api/v1/services/ip-limiter/config") async def config_ip_limiter(config: IPLimiterConfig): """Configure IP limiter""" cli_api.config_ip_limiter( block_duration=config.block_duration, max_ips=config.max_ips ) return {"message": "IP limiter configured"} @app.get("/api/v1/services/ip-limiter/config") async def get_ip_limiter_config(): """Get IP limiter configuration""" config = cli_api.get_ip_limiter_config() return config # ============================================================================ # Nodes Management # ============================================================================ @app.post("/api/v1/nodes") async def add_node(node: NodeCreate): """Add external node""" result = cli_api.add_node( name=node.name, ip=node.ip, sni=node.sni, pinSHA256=node.pinSHA256, port=node.port, obfs=node.obfs, insecure=node.insecure, ) return {"message": result} @app.delete("/api/v1/nodes/{name}") async def delete_node(name: str): """Delete node""" result = cli_api.delete_node(name) return {"message": result} @app.get("/api/v1/nodes") async def list_nodes(): """List all nodes""" result = cli_api.list_nodes() return {"nodes": result} # ============================================================================ # Geo Updates # ============================================================================ @app.post("/api/v1/geo/update/{country}") async def update_geo(country: str): """Update geo files for country (iran, china, russia)""" cli_api.update_geo(country) return {"message": f"Geo files updated for {country}"} # ============================================================================ # Backup & Restore # ============================================================================ @app.post("/api/v1/backup") async def create_backup(): """Create backup""" cli_api.backup_hysteria2() return {"message": "Backup created successfully"} @app.post("/api/v1/restore") async def restore_backup(backup_file_path: str): """Restore from backup""" cli_api.restore_hysteria2(backup_file_path) return {"message": "Restored successfully"} if __name__ == "__main__": import uvicorn uvicorn.run(app, host="0.0.0.0", port=8000)