From d1385ac8c41f45ed3c1009103e1125fe6f2d8098 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:33:28 +0330 Subject: [PATCH 1/9] feat: add script to manage extra subscription links Introduces a new script `extra_config.py` to manage additional proxy configurations (vmess, vless, ss, trojan) for subscription links. --- core/scripts/hysteria2/extra_config.py | 104 +++++++++++++++++++++++++ core/scripts/paths.py | 1 + 2 files changed, 105 insertions(+) create mode 100644 core/scripts/hysteria2/extra_config.py diff --git a/core/scripts/hysteria2/extra_config.py b/core/scripts/hysteria2/extra_config.py new file mode 100644 index 0000000..aac6d58 --- /dev/null +++ b/core/scripts/hysteria2/extra_config.py @@ -0,0 +1,104 @@ +import json +import argparse +import os +import sys +from init_paths import * +from paths import * +VALID_PROTOCOLS = ("vmess://", "vless://", "ss://", "trojan://") + +def read_configs(): + if not os.path.exists(EXTRA_CONFIG_PATH): + return [] + try: + with open(EXTRA_CONFIG_PATH, 'r') as f: + content = f.read() + if not content: + return [] + return json.loads(content) + except (json.JSONDecodeError, IOError): + return [] + +def write_configs(configs): + try: + os.makedirs(os.path.dirname(EXTRA_CONFIG_PATH), exist_ok=True) + with open(EXTRA_CONFIG_PATH, 'w') as f: + json.dump(configs, f, indent=4) + except IOError as e: + print(f"Error writing to {EXTRA_CONFIG_PATH}: {e}", file=sys.stderr) + sys.exit(1) + +def add_config(name, uri): + if not any(uri.startswith(protocol) for protocol in VALID_PROTOCOLS): + print(f"Error: Invalid URI. Must start with one of {', '.join(VALID_PROTOCOLS)}", file=sys.stderr) + sys.exit(1) + + configs = read_configs() + + if any(c['name'] == name for c in configs): + print(f"Error: A configuration with the name '{name}' already exists.", file=sys.stderr) + sys.exit(1) + + configs.append({"name": name, "uri": uri}) + write_configs(configs) + print(f"Successfully added configuration '{name}'.") + +def delete_config(name): + configs = read_configs() + + initial_length = len(configs) + configs = [c for c in configs if c['name'] != name] + + if len(configs) == initial_length: + print(f"Error: No configuration found with the name '{name}'.", file=sys.stderr) + sys.exit(1) + + write_configs(configs) + print(f"Successfully deleted configuration '{name}'.") + +def list_configs(): + configs = read_configs() + print(json.dumps(configs, indent=4)) + +def get_config(name): + configs = read_configs() + config = next((c for c in configs if c['name'] == name), None) + + if config: + print(json.dumps(config, indent=4)) + else: + print(f"Error: No configuration found with the name '{name}'.", file=sys.stderr) + sys.exit(1) + +def main(): + parser = argparse.ArgumentParser(description="Manage extra proxy configurations for subscription links.") + subparsers = parser.add_subparsers(dest="command", required=True) + + parser_add = subparsers.add_parser("add", help="Add a new proxy configuration.") + parser_add.add_argument("--name", type=str, required=True, help="A unique name for the configuration.") + parser_add.add_argument("--uri", type=str, required=True, help="The proxy URI (vmess, vless, ss, trojan).") + + parser_delete = subparsers.add_parser("delete", help="Delete a proxy configuration.") + parser_delete.add_argument("--name", type=str, required=True, help="The name of the configuration to delete.") + + subparsers.add_parser("list", help="List all extra proxy configurations.") + + parser_get = subparsers.add_parser("get", help="Get a specific proxy configuration by name.") + parser_get.add_argument("--name", type=str, required=True, help="The name of the configuration to retrieve.") + + args = parser.parse_args() + + if os.geteuid() != 0: + print("This script must be run as root.", file=sys.stderr) + sys.exit(1) + + if args.command == "add": + add_config(args.name, args.uri) + elif args.command == "delete": + delete_config(args.name) + elif args.command == "list": + list_configs() + elif args.command == "get": + get_config(args.name) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/core/scripts/paths.py b/core/scripts/paths.py index d59c6ec..5dbab5f 100644 --- a/core/scripts/paths.py +++ b/core/scripts/paths.py @@ -8,6 +8,7 @@ TRAFFIC_FILE = BASE_DIR / "traffic_data.json" CONFIG_FILE = BASE_DIR / "config.json" CONFIG_ENV = BASE_DIR / ".configs.env" NODES_JSON_PATH = BASE_DIR / "nodes.json" +EXTRA_CONFIG_PATH = BASE_DIR / "extra.json" TELEGRAM_ENV = BASE_DIR / "core/scripts/telegrambot/.env" SINGBOX_ENV = BASE_DIR / "core/scripts/singbox/.env" NORMALSUB_ENV = BASE_DIR / "core/scripts/normalsub/.env" From 491b384468f96042b3303721c1c870546844d175 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:35:51 +0330 Subject: [PATCH 2/9] feat: add cli interface for extra subscription configs --- core/cli.py | 50 +++++++++++++++++++++++++++++++++++++++++++++++++ core/cli_api.py | 20 ++++++++++++++++++++ 2 files changed, 70 insertions(+) diff --git a/core/cli.py b/core/cli.py index a6b416e..ef9a075 100644 --- a/core/cli.py +++ b/core/cli.py @@ -368,6 +368,56 @@ def masquerade(remove: bool, enable: str): except Exception as e: click.echo(f'{e}', err=True) +@cli.group('extra-config') +def extra_config(): + """Manage extra proxy configurations for subscription links.""" + pass + + +@extra_config.command('add') +@click.option('--name', required=True, help='A unique name for the configuration.') +@click.option('--uri', required=True, help='The proxy URI (vmess, vless, ss, trojan).') +def add_extra_config(name: str, uri: str): + """Add a new extra proxy configuration.""" + try: + output = cli_api.add_extra_config(name, uri) + click.echo(output) + except Exception as e: + click.echo(f'{e}', err=True) + + +@extra_config.command('delete') +@click.option('--name', required=True, help='The name of the configuration to delete.') +def delete_extra_config(name: str): + """Delete an extra proxy configuration.""" + try: + output = cli_api.delete_extra_config(name) + click.echo(output) + except Exception as e: + click.echo(f'{e}', err=True) + + +@extra_config.command('list') +def list_extra_configs(): + """List all extra proxy configurations.""" + try: + output = cli_api.list_extra_configs() + click.echo(output) + except Exception as e: + click.echo(f'{e}', err=True) + + +@extra_config.command('get') +@click.option('--name', required=True, help='The name of the configuration to retrieve.') +def get_extra_config(name: str): + """Get a specific extra proxy configuration.""" + try: + res = cli_api.get_extra_config(name) + if res: + pretty_print(res) + except Exception as e: + click.echo(f'{e}', err=True) + # endregion # region Advanced Menu diff --git a/core/cli_api.py b/core/cli_api.py index 3098060..c235dce 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -36,6 +36,7 @@ class Command(Enum): NODE_MANAGER = os.path.join(SCRIPT_DIR, 'hysteria2', 'node.py') MANAGE_OBFS = os.path.join(SCRIPT_DIR, 'hysteria2', 'manage_obfs.py') MASQUERADE_SCRIPT = os.path.join(SCRIPT_DIR, 'hysteria2', 'masquerade.py') + EXTRA_CONFIG_SCRIPT = os.path.join(SCRIPT_DIR, 'hysteria2', 'extra_config.py') TRAFFIC_STATUS = 'traffic.py' # won't be called directly (it's a python module) UPDATE_GEO = os.path.join(SCRIPT_DIR, 'hysteria2', 'update_geo.py') LIST_USERS = os.path.join(SCRIPT_DIR, 'hysteria2', 'list_users.sh') @@ -476,6 +477,25 @@ def update_geo(country: str): except Exception as e: raise HysteriaError(f'An unexpected error occurred: {e}') +def add_extra_config(name: str, uri: str) -> str: + """Adds an extra proxy configuration.""" + return run_cmd(['python3', Command.EXTRA_CONFIG_SCRIPT.value, 'add', '--name', name, '--uri', uri]) + + +def delete_extra_config(name: str) -> str: + """Deletes an extra proxy configuration.""" + return run_cmd(['python3', Command.EXTRA_CONFIG_SCRIPT.value, 'delete', '--name', name]) + + +def list_extra_configs() -> str: + """Lists all extra proxy configurations.""" + return run_cmd(['python3', Command.EXTRA_CONFIG_SCRIPT.value, 'list']) + + +def get_extra_config(name: str) -> dict[str, Any] | None: + """Gets a specific extra proxy configuration.""" + if res := run_cmd(['python3', Command.EXTRA_CONFIG_SCRIPT.value, 'get', '--name', name]): + return json.loads(res) # endregion From 68dde4f8638837d116cfbde6df4524441273a908 Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:39:20 +0330 Subject: [PATCH 3/9] feat: add webpanel API for extra subscription configs --- .../routers/api/v1/config/__init__.py | 2 + .../routers/api/v1/config/extra_config.py | 59 +++++++++++++++++++ .../routers/api/v1/schema/config/__init__.py | 1 + .../api/v1/schema/config/extra_config.py | 24 ++++++++ 4 files changed, 86 insertions(+) create mode 100644 core/scripts/webpanel/routers/api/v1/config/extra_config.py create mode 100644 core/scripts/webpanel/routers/api/v1/schema/config/extra_config.py diff --git a/core/scripts/webpanel/routers/api/v1/config/__init__.py b/core/scripts/webpanel/routers/api/v1/config/__init__.py index 02812a3..ee15be7 100644 --- a/core/scripts/webpanel/routers/api/v1/config/__init__.py +++ b/core/scripts/webpanel/routers/api/v1/config/__init__.py @@ -6,6 +6,7 @@ from . import normalsub from . import singbox from . import ip from . import misc +from . import extra_config router = APIRouter() @@ -16,4 +17,5 @@ router.include_router(telegram.router, prefix='/telegram') router.include_router(normalsub.router, prefix='/normalsub') router.include_router(singbox.router, prefix='/singbox') router.include_router(ip.router, prefix='/ip') +router.include_router(extra_config.router, prefix='/extra-config', tags=['Config - Extra Config']) router.include_router(misc.router) diff --git a/core/scripts/webpanel/routers/api/v1/config/extra_config.py b/core/scripts/webpanel/routers/api/v1/config/extra_config.py new file mode 100644 index 0000000..50605bc --- /dev/null +++ b/core/scripts/webpanel/routers/api/v1/config/extra_config.py @@ -0,0 +1,59 @@ +from fastapi import APIRouter, HTTPException +from ..schema.response import DetailResponse +import json +from ..schema.config.extra_config import ( + AddExtraConfigBody, + DeleteExtraConfigBody, + ExtraConfigListResponse, +) +import cli_api + +router = APIRouter() + +@router.get('/list', response_model=ExtraConfigListResponse, summary='Get All Extra Configs') +async def get_all_extra_configs(): + """ + Retrieves the list of all configured extra proxy configurations. + + Returns: + A list of extra config objects, each containing a name and a URI. + """ + try: + configs_str = cli_api.list_extra_configs() + if not configs_str: + return [] + return json.loads(configs_str) + except json.JSONDecodeError as e: + raise HTTPException(status_code=500, detail=f"Failed to parse extra configs list: {e}") + except Exception as e: + raise HTTPException(status_code=500, detail=f"Failed to retrieve extra configs: {str(e)}") + + +@router.post('/add', response_model=DetailResponse, summary='Add Extra Config') +async def add_extra_config(body: AddExtraConfigBody): + """ + Adds a new extra proxy configuration. + + Args: + body: Request body containing the name and URI of the config. + """ + try: + cli_api.add_extra_config(body.name, body.uri) + return DetailResponse(detail=f"Extra config '{body.name}' added successfully.") + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) + + +@router.post('/delete', response_model=DetailResponse, summary='Delete Extra Config') +async def delete_extra_config(body: DeleteExtraConfigBody): + """ + Deletes an extra proxy configuration by its name. + + Args: + body: Request body containing the name of the config to delete. + """ + try: + cli_api.delete_extra_config(body.name) + return DetailResponse(detail=f"Extra config '{body.name}' deleted successfully.") + except Exception as e: + raise HTTPException(status_code=400, detail=str(e)) \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/schema/config/__init__.py b/core/scripts/webpanel/routers/api/v1/schema/config/__init__.py index cd54453..0aac258 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/config/__init__.py +++ b/core/scripts/webpanel/routers/api/v1/schema/config/__init__.py @@ -3,3 +3,4 @@ from . import normalsub from . import singbox from . import telegram from . import warp +from . import extra_config diff --git a/core/scripts/webpanel/routers/api/v1/schema/config/extra_config.py b/core/scripts/webpanel/routers/api/v1/schema/config/extra_config.py new file mode 100644 index 0000000..fda8190 --- /dev/null +++ b/core/scripts/webpanel/routers/api/v1/schema/config/extra_config.py @@ -0,0 +1,24 @@ +from pydantic import BaseModel, field_validator, Field + +VALID_PROTOCOLS = ("vmess://", "vless://", "ss://", "trojan://") + +class ExtraConfigBase(BaseModel): + name: str = Field(..., min_length=1, description="A unique name for the configuration.") + uri: str = Field(..., description="The proxy URI.") + + @field_validator('uri') + def validate_uri_protocol(cls, v): + if not any(v.startswith(protocol) for protocol in VALID_PROTOCOLS): + raise ValueError(f"Invalid URI. Must start with one of {', '.join(VALID_PROTOCOLS)}") + return v + +class AddExtraConfigBody(ExtraConfigBase): + pass + +class DeleteExtraConfigBody(BaseModel): + name: str + +class ExtraConfigResponse(ExtraConfigBase): + pass + +ExtraConfigListResponse = list[ExtraConfigResponse] \ No newline at end of file From 7587d5a4a882e8f7a0a1831a967eba749d61ac6b Mon Sep 17 00:00:00 2001 From: Whispering Wind <151555003+ReturnFI@users.noreply.github.com> Date: Sun, 17 Aug 2025 15:40:23 +0330 Subject: [PATCH 4/9] feat: add extra configs management UI to settings page --- core/scripts/webpanel/templates/settings.html | 189 +++++++++++++++++- 1 file changed, 182 insertions(+), 7 deletions(-) diff --git a/core/scripts/webpanel/templates/settings.html b/core/scripts/webpanel/templates/settings.html index da42d18..caf9ca7 100644 --- a/core/scripts/webpanel/templates/settings.html +++ b/core/scripts/webpanel/templates/settings.html @@ -51,6 +51,11 @@ aria-controls='change_ip' aria-selected='false'> IP Management +
Add external proxy links (Vmess, Vless, SS, Trojan) to be included in all users' subscription links.
+| Name | +URI | +Action | +
|---|