diff --git a/changelog b/changelog index 4145713..2ed5cf2 100644 --- a/changelog +++ b/changelog @@ -1,23 +1,15 @@ -# [1.10.0] - 2025-05-18 +# [1.10.1] - 2025-05-24 ## โจ New Features -### โ๏ธ NormalSub Configuration Enhancements +* ๐ **feat:** Add option to reset Web Panel admin credentials via CLI +* ๐งฉ **feat:** Add "Reset Web Panel credentials" option to Bash menu +* ๐ **feat:** Add API endpoint to fetch IP Limiter configuration -* ๐ ๏ธ **feat:** Add NormalSub subpath editing via Settings UI - - New 'Configure' tab in the Settings panel (visible only if NormalSub is active) - - Real-time client-side validation and live subpath editing +## ๐๏ธ UI Improvements -* ๐ **feat:** Add API endpoints - - `GET /api/v1/config/normalsub/subpath`: Fetch current subpath - - `PUT /api/v1/config/normalsub/edit_subpath`: Update the subpath securely +* ๐งผ **clean:** Removed shield icons from footer for a cleaner look -* ๐ฅ๏ธ **feat:** Add CLI command support - - `edit_subpath` option added to CLI and `normal-sub` command - - Automatically restarts the service after applying changes +## ๐ Fixes -* ๐ง **feat:** Add backend CLI + shell logic to update `.env` subpath for NormalSub - -## ๐ Documentation - -* ๐ **docs:** Add sponsorship section with referral link to README +* ๐ **fix:** Align memory usage reporting with `free -m` in `server_info.py` diff --git a/core/cli.py b/core/cli.py index 6672e5b..ab0c9f7 100644 --- a/core/cli.py +++ b/core/cli.py @@ -523,6 +523,28 @@ def get_web_panel_api_token(): except Exception as e: click.echo(f'{e}', err=True) +@cli.command('reset-webpanel-creds') +@click.option('--new-username', '-u', required=False, help='New admin username for WebPanel', type=str) +@click.option('--new-password', '-p', required=False, help='New admin password for WebPanel', type=str) +def reset_webpanel_creds(new_username: str | None, new_password: str | None): + """Resets the WebPanel admin username and/or password.""" + try: + if not new_username and not new_password: + raise click.UsageError('Error: You must provide either --new-username or --new-password, or both.') + + cli_api.reset_webpanel_credentials(new_username, new_password) + + message_parts = [] + if new_username: + message_parts.append(f"username to '{new_username}'") + if new_password: + message_parts.append("password") + + click.echo(f'WebPanel admin {" and ".join(message_parts)} updated successfully.') + click.echo('WebPanel service has been restarted.') + except Exception as e: + click.echo(f'{e}', err=True) + @cli.command('get-webpanel-services-status') def get_web_panel_services_status(): diff --git a/core/cli_api.py b/core/cli_api.py index ad7a8e2..834da6c 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -588,6 +588,18 @@ def get_webpanel_api_token() -> str | None: '''Gets the API token of WebPanel.''' return run_cmd(['bash', Command.SHELL_WEBPANEL.value, 'api-token']) +def reset_webpanel_credentials(new_username: str | None = None, new_password: str | None = None): + '''Resets the WebPanel admin username and/or password.''' + if not new_username and not new_password: + raise InvalidInputError('Error: At least new username or new password must be provided.') + + cmd_args = ['bash', Command.SHELL_WEBPANEL.value, 'resetcreds'] + if new_username: + cmd_args.extend(['-u', new_username]) + if new_password: + cmd_args.extend(['-p', new_password]) + + run_cmd(cmd_args) def get_services_status() -> dict[str, bool] | None: '''Gets the status of all project services.''' @@ -630,4 +642,22 @@ def config_ip_limiter(block_duration: int = None, max_ips: int = None): cmd_args.append('') run_cmd(cmd_args) + +def get_ip_limiter_config() -> dict[str, int | None]: + '''Retrieves the current IP Limiter configuration from .configs.env.''' + try: + if not os.path.exists(CONFIG_ENV_FILE): + return {"block_duration": None, "max_ips": None} + + env_vars = dotenv_values(CONFIG_ENV_FILE) + block_duration_str = env_vars.get('BLOCK_DURATION') + max_ips_str = env_vars.get('MAX_IPS') + + block_duration = int(block_duration_str) if block_duration_str and block_duration_str.isdigit() else None + max_ips = int(max_ips_str) if max_ips_str and max_ips_str.isdigit() else None + + return {"block_duration": block_duration, "max_ips": max_ips} + except Exception as e: + print(f"Error reading IP Limiter config from .configs.env: {e}") + return {"block_duration": None, "max_ips": None} # endregion diff --git a/core/scripts/hysteria2/server_info.py b/core/scripts/hysteria2/server_info.py index 329dbc4..742e81b 100644 --- a/core/scripts/hysteria2/server_info.py +++ b/core/scripts/hysteria2/server_info.py @@ -52,14 +52,39 @@ def get_cpu_usage(interval: float = 0.1) -> float: def get_memory_usage() -> tuple[int, int]: - with open("/proc/meminfo") as f: - lines = f.readlines() + mem_info = {} + try: + with open("/proc/meminfo", "r") as f: + for line in f: + parts = line.split() + if len(parts) >= 2: + key = parts[0].rstrip(':') + if parts[1].isdigit(): + mem_info[key] = int(parts[1]) + except FileNotFoundError: + print("Error: /proc/meminfo not found.", file=sys.stderr) + return 0, 0 + except Exception as e: + print(f"Error reading /proc/meminfo: {e}", file=sys.stderr) + return 0, 0 - mem_total = int(next(line for line in lines if "MemTotal" in line).split()[1]) // 1024 - mem_available = int(next(line for line in lines if "MemAvailable" in line).split()[1]) // 1024 - mem_used = mem_total - mem_available + mem_total_kb = mem_info.get("MemTotal", 0) + mem_free_kb = mem_info.get("MemFree", 0) + buffers_kb = mem_info.get("Buffers", 0) + cached_kb = mem_info.get("Cached", 0) + sreclaimable_kb = mem_info.get("SReclaimable", 0) - return mem_total, mem_used + used_kb = mem_total_kb - mem_free_kb - buffers_kb - cached_kb - sreclaimable_kb + + if used_kb < 0: + used_kb = mem_total_kb - mem_info.get("MemAvailable", mem_total_kb) + used_kb = max(0, used_kb) + + + total_mb = mem_total_kb // 1024 + used_mb = used_kb // 1024 + + return total_mb, used_mb diff --git a/core/scripts/webpanel/routers/api/v1/config/hysteria.py b/core/scripts/webpanel/routers/api/v1/config/hysteria.py index 20535ea..2f08f59 100644 --- a/core/scripts/webpanel/routers/api/v1/config/hysteria.py +++ b/core/scripts/webpanel/routers/api/v1/config/hysteria.py @@ -1,6 +1,6 @@ from fastapi import APIRouter, BackgroundTasks, HTTPException, UploadFile, File from ..schema.config.hysteria import ConfigFile, GetPortResponse, GetSniResponse -from ..schema.response import DetailResponse, IPLimitConfig, SetupDecoyRequest, DecoyStatusResponse +from ..schema.response import DetailResponse, IPLimitConfig, SetupDecoyRequest, DecoyStatusResponse, IPLimitConfigResponse from fastapi.responses import FileResponse import shutil import zipfile @@ -335,6 +335,14 @@ async def config_ip_limit_api(config: IPLimitConfig): except Exception as e: raise HTTPException(status_code=400, detail=f'Error configuring IP Limiter: {str(e)}') +@router.get('/ip-limit/config', response_model=IPLimitConfigResponse, summary='Get IP Limiter Configuration') +async def get_ip_limit_config_api(): + """Retrieves the current IP Limiter configuration.""" + try: + config = cli_api.get_ip_limiter_config() + return IPLimitConfigResponse(**config) + except Exception as e: + raise HTTPException(status_code=500, detail=f'Error retrieving IP Limiter configuration: {str(e)}') def run_setup_decoy_background(domain: str, decoy_path: str): """Function to run decoy setup in the background.""" diff --git a/core/scripts/webpanel/routers/api/v1/schema/response.py b/core/scripts/webpanel/routers/api/v1/schema/response.py index 22ebbd2..acacc20 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/response.py +++ b/core/scripts/webpanel/routers/api/v1/schema/response.py @@ -6,8 +6,12 @@ class DetailResponse(BaseModel): detail: str class IPLimitConfig(BaseModel): - block_duration: Optional[int] = None - max_ips: Optional[int] = None + block_duration: Optional[int] = Field(None, example=60) + max_ips: Optional[int] = Field(None, example=1) + +class IPLimitConfigResponse(BaseModel): + block_duration: Optional[int] = Field(None, description="Current block duration in seconds for IP Limiter") + max_ips: Optional[int] = Field(None, description="Current maximum IPs per user for IP Limiter") class SetupDecoyRequest(BaseModel): domain: str = Field(..., description="Domain name associated with the web panel") @@ -15,4 +19,4 @@ class SetupDecoyRequest(BaseModel): class DecoyStatusResponse(BaseModel): active: bool = Field(..., description="Whether the decoy site is currently configured and active") - path: Optional[str] = Field(None, description="The configured path for the decoy site, if active") + path: Optional[str] = Field(None, description="The configured path for the decoy site, if active") \ No newline at end of file diff --git a/core/scripts/webpanel/templates/base.html b/core/scripts/webpanel/templates/base.html index 234afb6..98ee5cf 100644 --- a/core/scripts/webpanel/templates/base.html +++ b/core/scripts/webpanel/templates/base.html @@ -102,7 +102,7 @@
Guides & Tutorials
@@ -121,22 +121,20 @@