Skip to content
This repository was archived by the owner on Jul 8, 2026. It is now read-only.

Commit ad8eec5

Browse files
authored
Merge pull request #250 from ReturnFI/beta
Extra subscription configs & blocked-user check
2 parents bec447b + 7a624ef commit ad8eec5

11 files changed

Lines changed: 530 additions & 53 deletions

File tree

changelog

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,22 @@
1-
# [1.14.0] - 2025-08-13
1+
# [1.15.0] - 2025-08-17
22

33
#### ✨ New Features
44

5-
* 🌐 **Per-User Unlimited IP Option**:
5+
* 🚫 **Blocked User Check**
66

7-
* Added `unlimited_user` flag to exempt specific users from concurrent IP limits
8-
* Works in both CLI and web panel
9-
* Integrated into `limit.sh` for enforcement bypass
10-
* 🖥️ **Web Panel Enhancements**:
7+
* Subscription endpoint now validates blocked users and prevents access
8+
* 🌐 **External Configs in Subscriptions**
119

12-
* Unlimited IP control added to **Users** page
13-
* IP limit UI now conditionally shown based on service status
14-
* 🛠️ **CLI & Scripts**:
10+
* NormalSub links now include external proxy configs
11+
* ⚙️ **Settings Page**
1512

16-
* Add unlimited IP option to user creation and editing
17-
* Menu script now supports unlimited IP configuration
13+
* Added management UI for extra subscription configs
14+
* 🖥️ **Webpanel API**
1815

19-
#### 🔄 Improvements
16+
* New API endpoints for managing extra configs
17+
* 🛠️ **CLI & Scripts**
2018

21-
* 📜 **API**:
19+
* CLI support for extra subscription configs
20+
* New `extra_config.py` script to manage additional proxy configs:
2221

23-
* Updated user schemas to match CLI output and new unlimited IP feature
24-
* 🐛 **Fixes**:
25-
26-
* Corrected type hints for optional parameters in `config_ip_limiter`
27-
28-
#### 📦 Dependency Updates
29-
30-
* ⬆️ `anyio` → 4.10.0
31-
* ⬆️ `certbot` → 4.2.0
32-
* ⬆️ `charset-normalizer` → 3.4.3
22+
* `vmess`, `vless`, `ss`, `trojan`

core/cli.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,56 @@ def masquerade(remove: bool, enable: str):
368368
except Exception as e:
369369
click.echo(f'{e}', err=True)
370370

371+
@cli.group('extra-config')
372+
def extra_config():
373+
"""Manage extra proxy configurations for subscription links."""
374+
pass
375+
376+
377+
@extra_config.command('add')
378+
@click.option('--name', required=True, help='A unique name for the configuration.')
379+
@click.option('--uri', required=True, help='The proxy URI (vmess, vless, ss, trojan).')
380+
def add_extra_config(name: str, uri: str):
381+
"""Add a new extra proxy configuration."""
382+
try:
383+
output = cli_api.add_extra_config(name, uri)
384+
click.echo(output)
385+
except Exception as e:
386+
click.echo(f'{e}', err=True)
387+
388+
389+
@extra_config.command('delete')
390+
@click.option('--name', required=True, help='The name of the configuration to delete.')
391+
def delete_extra_config(name: str):
392+
"""Delete an extra proxy configuration."""
393+
try:
394+
output = cli_api.delete_extra_config(name)
395+
click.echo(output)
396+
except Exception as e:
397+
click.echo(f'{e}', err=True)
398+
399+
400+
@extra_config.command('list')
401+
def list_extra_configs():
402+
"""List all extra proxy configurations."""
403+
try:
404+
output = cli_api.list_extra_configs()
405+
click.echo(output)
406+
except Exception as e:
407+
click.echo(f'{e}', err=True)
408+
409+
410+
@extra_config.command('get')
411+
@click.option('--name', required=True, help='The name of the configuration to retrieve.')
412+
def get_extra_config(name: str):
413+
"""Get a specific extra proxy configuration."""
414+
try:
415+
res = cli_api.get_extra_config(name)
416+
if res:
417+
pretty_print(res)
418+
except Exception as e:
419+
click.echo(f'{e}', err=True)
420+
371421
# endregion
372422

373423
# region Advanced Menu

core/cli_api.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ class Command(Enum):
3636
NODE_MANAGER = os.path.join(SCRIPT_DIR, 'hysteria2', 'node.py')
3737
MANAGE_OBFS = os.path.join(SCRIPT_DIR, 'hysteria2', 'manage_obfs.py')
3838
MASQUERADE_SCRIPT = os.path.join(SCRIPT_DIR, 'hysteria2', 'masquerade.py')
39+
EXTRA_CONFIG_SCRIPT = os.path.join(SCRIPT_DIR, 'hysteria2', 'extra_config.py')
3940
TRAFFIC_STATUS = 'traffic.py' # won't be called directly (it's a python module)
4041
UPDATE_GEO = os.path.join(SCRIPT_DIR, 'hysteria2', 'update_geo.py')
4142
LIST_USERS = os.path.join(SCRIPT_DIR, 'hysteria2', 'list_users.sh')
@@ -476,6 +477,25 @@ def update_geo(country: str):
476477
except Exception as e:
477478
raise HysteriaError(f'An unexpected error occurred: {e}')
478479

480+
def add_extra_config(name: str, uri: str) -> str:
481+
"""Adds an extra proxy configuration."""
482+
return run_cmd(['python3', Command.EXTRA_CONFIG_SCRIPT.value, 'add', '--name', name, '--uri', uri])
483+
484+
485+
def delete_extra_config(name: str) -> str:
486+
"""Deletes an extra proxy configuration."""
487+
return run_cmd(['python3', Command.EXTRA_CONFIG_SCRIPT.value, 'delete', '--name', name])
488+
489+
490+
def list_extra_configs() -> str:
491+
"""Lists all extra proxy configurations."""
492+
return run_cmd(['python3', Command.EXTRA_CONFIG_SCRIPT.value, 'list'])
493+
494+
495+
def get_extra_config(name: str) -> dict[str, Any] | None:
496+
"""Gets a specific extra proxy configuration."""
497+
if res := run_cmd(['python3', Command.EXTRA_CONFIG_SCRIPT.value, 'get', '--name', name]):
498+
return json.loads(res)
479499

480500
# endregion
481501

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import json
2+
import argparse
3+
import os
4+
import sys
5+
from init_paths import *
6+
from paths import *
7+
VALID_PROTOCOLS = ("vmess://", "vless://", "ss://", "trojan://")
8+
9+
def read_configs():
10+
if not os.path.exists(EXTRA_CONFIG_PATH):
11+
return []
12+
try:
13+
with open(EXTRA_CONFIG_PATH, 'r') as f:
14+
content = f.read()
15+
if not content:
16+
return []
17+
return json.loads(content)
18+
except (json.JSONDecodeError, IOError):
19+
return []
20+
21+
def write_configs(configs):
22+
try:
23+
os.makedirs(os.path.dirname(EXTRA_CONFIG_PATH), exist_ok=True)
24+
with open(EXTRA_CONFIG_PATH, 'w') as f:
25+
json.dump(configs, f, indent=4)
26+
except IOError as e:
27+
print(f"Error writing to {EXTRA_CONFIG_PATH}: {e}", file=sys.stderr)
28+
sys.exit(1)
29+
30+
def add_config(name, uri):
31+
if not any(uri.startswith(protocol) for protocol in VALID_PROTOCOLS):
32+
print(f"Error: Invalid URI. Must start with one of {', '.join(VALID_PROTOCOLS)}", file=sys.stderr)
33+
sys.exit(1)
34+
35+
configs = read_configs()
36+
37+
if any(c['name'] == name for c in configs):
38+
print(f"Error: A configuration with the name '{name}' already exists.", file=sys.stderr)
39+
sys.exit(1)
40+
41+
configs.append({"name": name, "uri": uri})
42+
write_configs(configs)
43+
print(f"Successfully added configuration '{name}'.")
44+
45+
def delete_config(name):
46+
configs = read_configs()
47+
48+
initial_length = len(configs)
49+
configs = [c for c in configs if c['name'] != name]
50+
51+
if len(configs) == initial_length:
52+
print(f"Error: No configuration found with the name '{name}'.", file=sys.stderr)
53+
sys.exit(1)
54+
55+
write_configs(configs)
56+
print(f"Successfully deleted configuration '{name}'.")
57+
58+
def list_configs():
59+
configs = read_configs()
60+
print(json.dumps(configs, indent=4))
61+
62+
def get_config(name):
63+
configs = read_configs()
64+
config = next((c for c in configs if c['name'] == name), None)
65+
66+
if config:
67+
print(json.dumps(config, indent=4))
68+
else:
69+
print(f"Error: No configuration found with the name '{name}'.", file=sys.stderr)
70+
sys.exit(1)
71+
72+
def main():
73+
parser = argparse.ArgumentParser(description="Manage extra proxy configurations for subscription links.")
74+
subparsers = parser.add_subparsers(dest="command", required=True)
75+
76+
parser_add = subparsers.add_parser("add", help="Add a new proxy configuration.")
77+
parser_add.add_argument("--name", type=str, required=True, help="A unique name for the configuration.")
78+
parser_add.add_argument("--uri", type=str, required=True, help="The proxy URI (vmess, vless, ss, trojan).")
79+
80+
parser_delete = subparsers.add_parser("delete", help="Delete a proxy configuration.")
81+
parser_delete.add_argument("--name", type=str, required=True, help="The name of the configuration to delete.")
82+
83+
subparsers.add_parser("list", help="List all extra proxy configurations.")
84+
85+
parser_get = subparsers.add_parser("get", help="Get a specific proxy configuration by name.")
86+
parser_get.add_argument("--name", type=str, required=True, help="The name of the configuration to retrieve.")
87+
88+
args = parser.parse_args()
89+
90+
if os.geteuid() != 0:
91+
print("This script must be run as root.", file=sys.stderr)
92+
sys.exit(1)
93+
94+
if args.command == "add":
95+
add_config(args.name, args.uri)
96+
elif args.command == "delete":
97+
delete_config(args.name)
98+
elif args.command == "list":
99+
list_configs()
100+
elif args.command == "get":
101+
get_config(args.name)
102+
103+
if __name__ == "__main__":
104+
main()

0 commit comments

Comments
 (0)