diff --git a/changelog b/changelog index d9d79fc..03155de 100644 --- a/changelog +++ b/changelog @@ -1,18 +1,13 @@ -# [1.11.0] - 2025-05-28 +# [1.12.0] - 2025-06-03 +#### ⨠Features & Enhancements -#### â ī¸ Attention Required +* đ¨ **UI/UX:** Some **enhancements to the Settings page** with cleaner design and improved interactivity +* đ **New Tabs:** Added **WARP** and **OBFS** management tabs to the settings panel for easier control +* đ **OBFS:** -đ¨ **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**. - -Failure to do so may result in broken subscription links. - ---- - -#### ⨠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 + * Added **OBFS status API endpoint** + * Introduced `--check` flag to the `manage_obfs` CLI tool + * Enabled **status checking** functionality to monitor OBFS state +* đ§ **normalsub:** Improved `edit_subpath` with **better error handling** and **efficient Caddy reload** +* đĻ **Singbox:** Enhanced **Singbox template configuration** for better maintainability and compatibility diff --git a/core/cli.py b/core/cli.py index ab0c9f7..320ca19 100644 --- a/core/cli.py +++ b/core/cli.py @@ -250,19 +250,25 @@ def server_info(): @cli.command('manage_obfs') @click.option('--remove', '-r', is_flag=True, help="Remove 'obfs' from config.json.") @click.option('--generate', '-g', is_flag=True, help="Generate new 'obfs' in config.json.") -def manage_obfs(remove: bool, generate: bool): +@click.option('--check', '-c', is_flag=True, help="Check 'obfs' status in config.json.") +def manage_obfs(remove: bool, generate: bool, check: bool): try: - if not remove and not generate: - raise click.UsageError('Error: You must use either --remove or --generate') - if remove and generate: - raise click.UsageError('Error: You cannot use both --remove and --generate at the same time') + options_selected = sum([remove, generate, check]) + if options_selected == 0: + raise click.UsageError('Error: You must use either --remove, --generate, or --check.') + if options_selected > 1: + raise click.UsageError('Error: You can only use one of --remove, --generate, or --check at a time.') if generate: cli_api.enable_hysteria2_obfs() - click.echo('Obfs enabled successfully.') + click.echo('OBFS enabled successfully.') elif remove: cli_api.disable_hysteria2_obfs() - click.echo('Obfs disabled successfully.') + click.echo('OBFS disabled successfully.') + elif check: + status_output = cli_api.check_hysteria2_obfs() + click.echo(status_output) + except Exception as e: click.echo(f'{e}', err=True) @@ -361,16 +367,29 @@ def uninstall_warp(): @cli.command('configure-warp') -@click.option('--all', '-a', is_flag=True, help='Use WARP for all connections') -@click.option('--popular-sites', '-p', is_flag=True, help='Use WARP for popular sites like Google, OpenAI, etc') -@click.option('--domestic-sites', '-d', is_flag=True, help='Use WARP for Iran domestic sites') -@click.option('--block-adult-sites', '-x', is_flag=True, help='Block adult content (porn)') -def configure_warp(all: bool, popular_sites: bool, domestic_sites: bool, block_adult_sites: bool): +@click.option('--set-all', 'set_all_traffic', type=click.Choice(['on', 'off']), help='Set WARP for all connections (on/off)', required=False) +@click.option('--set-popular-sites', type=click.Choice(['on', 'off']), help='Set WARP for popular sites (on/off)', required=False) +@click.option('--set-domestic-sites', type=click.Choice(['on', 'off']), help='Set behavior for domestic sites (on=WARP, off=REJECT)', required=False) +@click.option('--set-block-adult-sites', type=click.Choice(['on', 'off']), help='Set block adult content (on/off)', required=False) +def configure_warp_cmd(set_all_traffic: str | None, + set_popular_sites: str | None, + set_domestic_sites: str | None, + set_block_adult_sites: str | None): + if not any([set_all_traffic, set_popular_sites, set_domestic_sites, set_block_adult_sites]): + click.echo("Error: At least one configuration option must be provided to configure-warp.", err=True) + click.echo("Use --help for more information.") + return + try: - cli_api.configure_warp(all, popular_sites, domestic_sites, block_adult_sites) - click.echo('WARP configured successfully.') + cli_api.configure_warp( + all_state=set_all_traffic, + popular_sites_state=set_popular_sites, + domestic_sites_state=set_domestic_sites, + block_adult_sites_state=set_block_adult_sites + ) + click.echo('WARP configuration update process initiated.') except Exception as e: - click.echo(f'{e}', err=True) + click.echo(f'Error configuring WARP: {e}', err=True) @cli.command('warp-status') def warp_status(): diff --git a/core/cli_api.py b/core/cli_api.py index 834da6c..28c998b 100644 --- a/core/cli_api.py +++ b/core/cli_api.py @@ -218,6 +218,10 @@ def disable_hysteria2_obfs(): '''Removes 'obfs' from Hysteria2 configuration.''' run_cmd(['python3', Command.MANAGE_OBFS.value, '--remove']) +def check_hysteria2_obfs(): + '''Removes 'obfs' from Hysteria2 configuration.''' + result = subprocess.run(["python3", Command.MANAGE_OBFS.value, "--check"], check=True, capture_output=True, text=True) + return result.stdout.strip() def enable_hysteria2_masquerade(domain: str): '''Enables masquerade for Hysteria2.''' @@ -458,21 +462,28 @@ def uninstall_warp(): run_cmd(['python3', Command.UNINSTALL_WARP.value]) -def configure_warp(all: bool, popular_sites: bool, domestic_sites: bool, block_adult_sites: bool): +def configure_warp(all_state: str | None = None, + popular_sites_state: str | None = None, + domestic_sites_state: str | None = None, + block_adult_sites_state: str | None = None): ''' - Configures WARP with various options. + Configures WARP with various options. States are 'on' or 'off'. ''' cmd_args = [ 'python3', Command.CONFIGURE_WARP.value ] - if all: - cmd_args.append('--all') - if popular_sites: - cmd_args.append('--popular-sites') - if domestic_sites: - cmd_args.append('--domestic-sites') - if block_adult_sites: - cmd_args.append('--block-adult') + if all_state: + cmd_args.extend(['--set-all', all_state]) + if popular_sites_state: + cmd_args.extend(['--set-popular-sites', popular_sites_state]) + if domestic_sites_state: + cmd_args.extend(['--set-domestic-sites', domestic_sites_state]) + if block_adult_sites_state: + cmd_args.extend(['--set-block-adult', block_adult_sites_state]) + + if len(cmd_args) == 2: + print("No WARP configuration options provided to cli_api.configure_warp.") + return run_cmd(cmd_args) diff --git a/core/scripts/hysteria2/manage_obfs.py b/core/scripts/hysteria2/manage_obfs.py index 3368649..3a11f52 100644 --- a/core/scripts/hysteria2/manage_obfs.py +++ b/core/scripts/hysteria2/manage_obfs.py @@ -9,16 +9,14 @@ from init_paths import * from paths import * def restart_hysteria(): - """Restart the Hysteria2 service using the CLI script.""" try: subprocess.run(["python3", CLI_PATH, "restart-hysteria2"], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) except Exception as e: - print(f"â ī¸ Failed to restart Hysteria2: {e}") + print(f"Failed to restart Hysteria2: {e}") def remove_obfs(): - """Remove the 'obfs' section from the config.""" try: with open(CONFIG_FILE, 'r') as f: config = json.load(f) @@ -27,9 +25,9 @@ def remove_obfs(): del config['obfs'] with open(CONFIG_FILE, 'w') as f: json.dump(config, f, indent=2) - print("â Successfully removed 'obfs' from config.json.") + print("Successfully removed 'obfs' from config.json.") else: - print("âšī¸ 'obfs' section not found in config.json.") + print("'obfs' section not found in config.json.") restart_hysteria() @@ -39,13 +37,12 @@ def remove_obfs(): print(f"â Error removing 'obfs': {e}") def generate_obfs(): - """Generate and add an 'obfs' section with a random password.""" try: with open(CONFIG_FILE, 'r') as f: config = json.load(f) if 'obfs' in config: - print("âšī¸ 'obfs' section already exists. Replacing it.") + print("'obfs' section already exists. Replacing it.") del config['obfs'] password = ''.join(secrets.choice(string.ascii_letters + string.digits) for _ in range(32)) @@ -60,30 +57,48 @@ def generate_obfs(): with open(CONFIG_FILE, 'w') as f: json.dump(config, f, indent=2) - print(f"â Successfully added 'obfs' to config.json with password: {password}") + print(f"Successfully added 'obfs' to config.json with password: {password}") restart_hysteria() except FileNotFoundError: - print(f"â Config file not found: {CONFIG_FILE}") + print(f"Config file not found: {CONFIG_FILE}") except Exception as e: - print(f"â Error generating 'obfs': {e}") + print(f"Error generating 'obfs': {e}") + +def check_obfs(): + try: + with open(CONFIG_FILE, 'r') as f: + config = json.load(f) + + if 'obfs' in config: + print("OBFS is active.") + else: + print("OBFS is not active.") + + except FileNotFoundError: + print(f"Config file not found: {CONFIG_FILE}") + except Exception as e: + print(f"Error checking 'obfs' status: {e}") def main(): if len(sys.argv) != 2: - print("Usage: python3 obfs_manager.py --remove|-r | --generate|-g") + print("Usage: python3 obfs_manager.py --remove|-r | --generate|-g | --check|-c") sys.exit(1) option = sys.argv[1] if option in ("--remove", "-r"): - print("Removing 'obfs' from config.json...") + # print("Removing 'obfs' from config.json...") remove_obfs() elif option in ("--generate", "-g"): - print("Generating 'obfs' in config.json...") + # print("Generating 'obfs' in config.json...") generate_obfs() + elif option in ("--check", "-c"): + # print("Checking 'obfs' status in config.json...") + check_obfs() else: - print("Invalid option. Use --remove|-r or --generate|-g") + print("Invalid option. Use --remove|-r, --generate|-g, or --check|-c") sys.exit(1) if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/core/scripts/normalsub/singbox.json b/core/scripts/normalsub/singbox.json index 84fb19d..c22f140 100644 --- a/core/scripts/normalsub/singbox.json +++ b/core/scripts/normalsub/singbox.json @@ -1,172 +1,160 @@ { - "log": { - "level": "info", - "timestamp": true - }, "dns": { - "servers": [ - { - "tag": "proxyDns", - "address": "tls://8.8.8.8", - "detour": "Proxy" - }, - { - "tag": "localDns", - "address": "https://223.5.5.5/dns-query", - "detour": "direct" - } - ], + "final": "local-dns", "rules": [ { - "outbound": "any", - "server": "localDns" + "action": "route", + "clash_mode": "Global", + "server": "proxy-dns", + "source_ip_cidr": [ + "172.19.0.0/30", + "fdfe:dcba:9876::1/126" + ] }, { - "rule_set": "geosite-ir", - "server": "proxyDns" - }, - { - "clash_mode": "direct", - "server": "localDns" - }, - { - "clash_mode": "global", - "server": "proxyDns" + "action": "route", + "server": "proxy-dns", + "source_ip_cidr": [ + "172.19.0.0/30", + "fdfe:dcba:9876::1/126" + ] } ], - "final": "localDns", - "strategy": "ipv4_only" + "servers": [ + { + "type": "https", + "server": "1.1.1.1", + "address_resolver": "local-dns", + "detour": "proxy", + "tag": "proxy-dns" + }, + { + "address": "local", + "detour": "direct", + "tag": "local-dns" + } + ], + "strategy": "prefer_ipv4" }, "inbounds": [ { - "tag": "tun-in", - "type": "tun", "address": [ - "172.19.0.0/30" + "172.19.0.1/30", + "fdfe:dcba:9876::1/126" ], - "mtu": 9000, "auto_route": true, - "strict_route": true, - "stack": "system", + "endpoint_independent_nat": false, + "mtu": 9000, "platform": { "http_proxy": { "enabled": true, "server": "127.0.0.1", "server_port": 2080 } - } + }, + "stack": "system", + "strict_route": false, + "type": "tun" }, { - "tag": "mixed-in", - "type": "mixed", "listen": "127.0.0.1", - "listen_port": 2080 + "listen_port": 2080, + "type": "mixed", + "users": [] } ], + "log": { + "level": "warn", + "timestamp": true + }, "outbounds": [ { - "tag": "Proxy", - "type": "selector", "outbounds": [ "auto", "direct" - ] - }, - { - "tag": "Global", - "type": "selector", - "outbounds": [ - "direct" - ] - }, - { - "tag": "auto", - "type": "urltest", - "outbounds": [ - "Proxy" ], - "url": "http://www.gstatic.com/generate_204", + "tag": "proxy", + "type": "selector" + }, + { "interval": "10m", - "tolerance": 50 + "outbounds": [], + "tag": "auto", + "tolerance": 50, + "type": "urltest", + "url": "http://www.gstatic.com/generate_204" }, { - "type": "direct", - "tag": "direct" - }, - { - "type": "direct", - "tag": "local" + "tag": "direct", + "type": "direct" } ], "route": { "auto_detect_interface": true, - "final": "Proxy", + "final": "proxy", + "rule_set": [ + { + "download_detour": "direct", + "format": "binary", + "tag": "geosite-ads", + "type": "remote", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-category-ads-all.srs" + }, + { + "download_detour": "direct", + "format": "binary", + "tag": "geoip-private", + "type": "remote", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-private.srs" + }, + { + "download_detour": "direct", + "format": "binary", + "tag": "geosite-ir", + "type": "remote", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs" + }, + { + "download_detour": "direct", + "format": "binary", + "tag": "geoip-ir", + "type": "remote", + "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs" + } + ], "rules": [ { - "inbound": [ - "tun-in", - "mixed-in" - ], "action": "sniff" }, { - "type": "logical", - "mode": "or", - "rules": [ - { - "port": 53 - }, - { - "protocol": "dns" - } - ], - "action": "hijack-dns" - }, - { - "rule_set": "geosite-category-ads-all", - "action": "reject" - }, - { - "rule_set": "geosite-category-ads-all", - "outbound": "Proxy" - }, - { - "ip_is_private": true, + "action": "route", + "clash_mode": "Direct", "outbound": "direct" }, { "action": "route", - "rule_set": "geosite-ir", - "outbound": "direct" + "clash_mode": "Global", + "outbound": "proxy" + }, + { + "action": "hijack-dns", + "protocol": "dns" }, { "action": "route", - "rule_set": "geoip-ir", - "outbound": "direct" - } - ], - "rule_set": [ - { - "tag": "geosite-category-ads-all", - "type": "remote", - "format": "binary", - "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-category-ads-all.srs", - "download_detour": "direct" + "outbound": "direct", + "rule_set": [ + "geosite-ir", + "geoip-ir", + "geosite-private" + ] }, { - "type": "remote", - "tag": "geoip-ir", - "format": "binary", - "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geoip-ir.srs", - "update_interval": "120h0m0s" - }, - { - "type": "remote", - "tag": "geosite-ir", - "format": "binary", - "url": "https://raw.githubusercontent.com/Chocolate4U/Iran-sing-box-rules/rule-set/geosite-ir.srs", - "update_interval": "120h0m0s" + "action": "reject", + "rule_set": [ + "geoip-ads" + ] } ] } -} +} \ No newline at end of file diff --git a/core/scripts/warp/configure.py b/core/scripts/warp/configure.py index 6f448aa..0b557fd 100644 --- a/core/scripts/warp/configure.py +++ b/core/scripts/warp/configure.py @@ -4,6 +4,7 @@ import json import sys import subprocess from pathlib import Path +import argparse core_scripts_dir = Path(__file__).resolve().parents[1] if str(core_scripts_dir) not in sys.path: @@ -11,149 +12,184 @@ if str(core_scripts_dir) not in sys.path: from paths import * -def warp_configure_handler(all_traffic=False, popular_sites=False, domestic_sites=False, block_adult_sites=False): - """ - Configure WARP routing rules based on provided parameters - - Args: - all_traffic (bool): Toggle WARP for all traffic - popular_sites (bool): Toggle WARP for popular sites (Google, Netflix, etc.) - domestic_sites (bool): Toggle between WARP and Reject for domestic sites - block_adult_sites (bool): Toggle blocking of adult content - """ - with open(CONFIG_FILE, 'r') as f: - config = json.load(f) - +def warp_configure_handler( + set_all_traffic_state: str | None = None, + set_popular_sites_state: str | None = None, + set_domestic_sites_state: str | None = None, + set_block_adult_sites_state: str | None = None +): + try: + with open(CONFIG_FILE, 'r') as f: + config = json.load(f) + except FileNotFoundError: + print(f"Error: Configuration file {CONFIG_FILE} not found.") + sys.exit(1) + except json.JSONDecodeError: + print(f"Error: Could not decode JSON from {CONFIG_FILE}.") + sys.exit(1) + modified = False - - if all_traffic: - warp_all_active = any(rule == "warps(all)" for rule in config.get('acl', {}).get('inline', [])) - - if warp_all_active: - config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule != "warps(all)"] - print("Traffic configuration changed to Direct.") - modified = True - else: - if 'acl' not in config: - config['acl'] = {} - if 'inline' not in config['acl']: - config['acl']['inline'] = [] - config['acl']['inline'].append("warps(all)") - print("Traffic configuration changed to WARP.") - modified = True - - if popular_sites: + + if 'acl' not in config: + config['acl'] = {} + if 'inline' not in config['acl']: + config['acl']['inline'] = [] + + if set_all_traffic_state is not None: + warp_all_rule = "warps(all)" + warp_all_active = warp_all_rule in config['acl']['inline'] + if set_all_traffic_state == "on": + if not warp_all_active: + config['acl']['inline'].append(warp_all_rule) + print("All traffic rule: Enabled.") + modified = True + else: + print("All traffic rule: Already enabled.") + elif set_all_traffic_state == "off": + if warp_all_active: + config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule != warp_all_rule] + print("All traffic rule: Disabled.") + modified = True + else: + print("All traffic rule: Already disabled.") + + if set_popular_sites_state is not None: popular_rules = [ - "warps(geoip:google)", - "warps(geosite:google)", - "warps(geosite:netflix)", - "warps(geosite:spotify)", - "warps(geosite:openai)", - "warps(geoip:openai)" + "warps(geoip:google)", "warps(geosite:google)", "warps(geosite:netflix)", + "warps(geosite:spotify)", "warps(geosite:openai)", "warps(geoip:openai)" ] - - rule_exists = any(rule in config.get('acl', {}).get('inline', []) for rule in popular_rules) - - if rule_exists: - config['acl']['inline'] = [rule for rule in config['acl']['inline'] - if rule not in popular_rules] - print("WARP configuration for Google, OpenAI, etc. removed.") - modified = True - else: - if 'acl' not in config: - config['acl'] = {} - if 'inline' not in config['acl']: - config['acl']['inline'] = [] - config['acl']['inline'].extend(popular_rules) - print("WARP configured for Google, OpenAI, etc.") - modified = True - - if domestic_sites: - ir_site_rule = "warps(geosite:ir)" - ir_ip_rule = "warps(geoip:ir)" - reject_site_rule = "reject(geosite:ir)" - reject_ip_rule = "reject(geoip:ir)" - - using_warp = (ir_site_rule in config.get('acl', {}).get('inline', []) and - ir_ip_rule in config.get('acl', {}).get('inline', [])) - using_reject = (reject_site_rule in config.get('acl', {}).get('inline', []) and - reject_ip_rule in config.get('acl', {}).get('inline', [])) - - if 'acl' not in config: - config['acl'] = {} - if 'inline' not in config['acl']: - config['acl']['inline'] = [] - - if using_warp: - config['acl']['inline'] = [reject_site_rule if rule == ir_site_rule else - reject_ip_rule if rule == ir_ip_rule else - rule for rule in config['acl']['inline']] - print("Configuration changed to Reject for geosite:ir and geoip:ir.") - modified = True - elif using_reject: - config['acl']['inline'] = [ir_site_rule if rule == reject_site_rule else - ir_ip_rule if rule == reject_ip_rule else - rule for rule in config['acl']['inline']] - print("Configuration changed to Use WARP for geosite:ir and geoip:ir.") - modified = True - else: - config['acl']['inline'].extend([reject_site_rule, reject_ip_rule]) - print("Added Reject configuration for geosite:ir and geoip:ir.") - modified = True - - if block_adult_sites: + if set_popular_sites_state == "on": + added_any = False + for rule in popular_rules: + if rule not in config['acl']['inline']: + config['acl']['inline'].append(rule) + added_any = True + if added_any: + print("Popular sites rule: Enabled/Updated.") + modified = True + else: + + all_present = all(rule in config['acl']['inline'] for rule in popular_rules) + if all_present: + print("Popular sites rule: Already enabled.") + else: + print("Popular sites rule: Enabled/Updated.") + modified = True + elif set_popular_sites_state == "off": + removed_any = False + initial_len = len(config['acl']['inline']) + config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule not in popular_rules] + if len(config['acl']['inline']) < initial_len: + removed_any = True + if removed_any: + print("Popular sites rule: Disabled.") + modified = True + else: + print("Popular sites rule: Already disabled.") + + if set_domestic_sites_state is not None: + ir_site_warp_rule = "warps(geosite:ir)" + ir_ip_warp_rule = "warps(geoip:ir)" + ir_site_reject_rule = "reject(geosite:ir)" + ir_ip_reject_rule = "reject(geoip:ir)" + + if set_domestic_sites_state == "on": + changed_to_warp = False + if ir_site_reject_rule in config['acl']['inline'] or ir_ip_reject_rule in config['acl']['inline']: + config['acl']['inline'] = [r for r in config['acl']['inline'] if r not in [ir_site_reject_rule, ir_ip_reject_rule]] + changed_to_warp = True + if ir_site_warp_rule not in config['acl']['inline']: + config['acl']['inline'].append(ir_site_warp_rule) + changed_to_warp = True + if ir_ip_warp_rule not in config['acl']['inline']: + config['acl']['inline'].append(ir_ip_warp_rule) + changed_to_warp = True + if changed_to_warp: + print("Domestic sites: Configured to use WARP.") + modified = True + else: + print("Domestic sites: Already configured to use WARP.") + elif set_domestic_sites_state == "off": + changed_to_reject = False + if ir_site_warp_rule in config['acl']['inline'] or ir_ip_warp_rule in config['acl']['inline']: + config['acl']['inline'] = [r for r in config['acl']['inline'] if r not in [ir_site_warp_rule, ir_ip_warp_rule]] + changed_to_reject = True + if ir_site_reject_rule not in config['acl']['inline']: + config['acl']['inline'].append(ir_site_reject_rule) + changed_to_reject = True + if ir_ip_reject_rule not in config['acl']['inline']: + config['acl']['inline'].append(ir_ip_reject_rule) + changed_to_reject = True + if changed_to_reject: + print("Domestic sites: Configured to REJECT.") + modified = True + else: + print("Domestic sites: Already configured to REJECT.") + + if set_block_adult_sites_state is not None: nsfw_rule = "reject(geosite:nsfw)" + is_blocking_nsfw = nsfw_rule in config['acl']['inline'] - blocked = nsfw_rule in config.get('acl', {}).get('inline', []) + if 'resolver' not in config: config['resolver'] = {} + if 'tls' not in config['resolver']: config['resolver']['tls'] = {} + + desired_resolver = "" + if set_block_adult_sites_state == "on": + desired_resolver = "1.1.1.3:853" + if not is_blocking_nsfw: + config['acl']['inline'].append(nsfw_rule) + print("Adult content blocking: Enabled.") + modified = True + else: + print("Adult content blocking: Already enabled.") + elif set_block_adult_sites_state == "off": + desired_resolver = "1.1.1.1:853" + if is_blocking_nsfw: + config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule != nsfw_rule] + print("Adult content blocking: Disabled.") + modified = True + else: + print("Adult content blocking: Already disabled.") - if blocked: - config['acl']['inline'] = [rule for rule in config['acl']['inline'] - if rule != nsfw_rule] - if 'resolver' not in config: - config['resolver'] = {} - if 'tls' not in config['resolver']: - config['resolver']['tls'] = {} - config['resolver']['tls']['addr'] = "1.1.1.1:853" - print("Adult content blocking removed and resolver updated.") + if config['resolver']['tls'].get('addr') != desired_resolver: + config['resolver']['tls']['addr'] = desired_resolver + print(f"Resolver: Updated to {desired_resolver}.") modified = True - else: - if 'acl' not in config: - config['acl'] = {} - if 'inline' not in config['acl']: - config['acl']['inline'] = [] - config['acl']['inline'].append(nsfw_rule) - if 'resolver' not in config: - config['resolver'] = {} - if 'tls' not in config['resolver']: - config['resolver']['tls'] = {} - config['resolver']['tls']['addr'] = "1.1.1.3:853" - print("Adult content blocked and resolver updated.") - modified = True - + + if 'acl' in config and 'inline' in config['acl']: + config['acl']['inline'] = [rule for rule in config['acl']['inline'] if rule] + + if modified: with open(CONFIG_FILE, 'w') as f: json.dump(config, f, indent=2) + print("Configuration updated. Attempting to restart hysteria2 service...") try: subprocess.run(["python3", CLI_PATH, "restart-hysteria2"], - stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True) - except subprocess.CalledProcessError: - print("Warning: Failed to restart hysteria2") + stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, timeout=10) + print("Hysteria2 service restarted successfully.") + except subprocess.CalledProcessError as e: + print(f"Warning: Failed to restart hysteria2. STDERR: {e.stderr.decode().strip()}") + except subprocess.TimeoutExpired: + print("Warning: Timeout expired while trying to restart hysteria2 service.") if __name__ == "__main__": - import argparse - - parser = argparse.ArgumentParser(description="Configure WARP settings") - parser.add_argument("--all", action="store_true", help="Toggle WARP for all traffic") - parser.add_argument("--popular-sites", action="store_true", help="Toggle WARP for popular sites") - parser.add_argument("--domestic-sites", action="store_true", help="Toggle between WARP and Reject for domestic sites") - parser.add_argument("--block-adult", action="store_true", help="Toggle blocking of adult content") + parser = argparse.ArgumentParser(description="Configure WARP settings. At least one option must be provided.") + parser.add_argument("--set-all", choices=['on', 'off'], help="Set WARP for all traffic (on/off)") + parser.add_argument("--set-popular-sites", choices=['on', 'off'], help="Set WARP for popular sites (on/off)") + parser.add_argument("--set-domestic-sites", choices=['on', 'off'], help="Set behavior for domestic sites (on=WARP, off=REJECT)") + parser.add_argument("--set-block-adult", choices=['on', 'off'], help="Set blocking of adult content (on/off)") args = parser.parse_args() + if not any(vars(args).values()): + parser.print_help() + sys.exit(1) + warp_configure_handler( - all_traffic=args.all, - popular_sites=args.popular_sites, - domestic_sites=args.domestic_sites, - block_adult_sites=args.block_adult + set_all_traffic_state=args.set_all, + set_popular_sites_state=args.set_popular_sites, + set_domestic_sites_state=args.set_domestic_sites, + set_block_adult_sites_state=args.set_block_adult ) \ No newline at end of file diff --git a/core/scripts/webpanel/routers/api/v1/config/hysteria.py b/core/scripts/webpanel/routers/api/v1/config/hysteria.py index 2f08f59..7c676df 100644 --- a/core/scripts/webpanel/routers/api/v1/config/hysteria.py +++ b/core/scripts/webpanel/routers/api/v1/config/hysteria.py @@ -1,5 +1,5 @@ from fastapi import APIRouter, BackgroundTasks, HTTPException, UploadFile, File -from ..schema.config.hysteria import ConfigFile, GetPortResponse, GetSniResponse +from ..schema.config.hysteria import ConfigFile, GetPortResponse, GetSniResponse, GetObfsResponse from ..schema.response import DetailResponse, IPLimitConfig, SetupDecoyRequest, DecoyStatusResponse, IPLimitConfigResponse from fastapi.responses import FileResponse import shutil @@ -223,6 +223,23 @@ async def disable_obfs(): raise HTTPException(status_code=400, detail=f'Error: {str(e)}') +@router.get('/check-obfs', response_model=GetObfsResponse, summary='Check Hysteria2 OBFS Status') +async def check_obfs(): + """ + Checks the current status of Hysteria2 OBFS. + + Returns: + A GetObfsResponse containing the Hysteria2 OBFS status message (e.g., 'OBFS is active.'). + + Raises: + HTTPException: if an error occurs while checking the Hysteria2 OBFS status. + """ + try: + obfs_status_message = cli_api.check_hysteria2_obfs() + return GetObfsResponse(obfs=obfs_status_message) + except Exception as e: + raise HTTPException(status_code=400, detail=f'Error checking OBFS status: {str(e)}') + @router.get('/enable-masquerade/{domain}', response_model=DetailResponse, summary='Enable Hysteria2 masquerade') async def enable_masquerade(domain: str): """ diff --git a/core/scripts/webpanel/routers/api/v1/config/warp.py b/core/scripts/webpanel/routers/api/v1/config/warp.py index b1e1a21..4968c65 100644 --- a/core/scripts/webpanel/routers/api/v1/config/warp.py +++ b/core/scripts/webpanel/routers/api/v1/config/warp.py @@ -8,7 +8,7 @@ import cli_api router = APIRouter() -@router.post('/install', response_model=DetailResponse, summary='Install WARP') +@router.post('/install', response_model=DetailResponse, summary='Install WARP', name="install_warp") async def install(): """ Installs WARP. @@ -27,7 +27,7 @@ async def install(): raise HTTPException(status_code=400, detail=f'Error: {str(e)}') -@router.delete('/uninstall', response_model=DetailResponse, summary='Uninstall WARP') +@router.delete('/uninstall', response_model=DetailResponse, summary='Uninstall WARP', name="uninstall_warp") async def uninstall(): """ Uninstalls WARP. @@ -45,7 +45,7 @@ async def uninstall(): raise HTTPException(status_code=400, detail=f'Error: {str(e)}') -@router.post('/configure', response_model=DetailResponse, summary='Configure WARP') +@router.post('/configure', response_model=DetailResponse, summary='Configure WARP', name="configure_warp") async def configure(body: ConfigureInputBody): """ Configures WARP with the given options. @@ -60,14 +60,23 @@ async def configure(body: ConfigureInputBody): HTTPException: If an error occurs during configuration, an HTTP 400 error is raised with the error details. """ try: - cli_api.configure_warp(body.all, body.popular_sites, body.domestic_sites, - body.block_adult_sites) + all_st = 'on' if body.all else 'off' + pop_sites_st = 'on' if body.popular_sites else 'off' + dom_sites_st = 'on' if body.domestic_sites else 'off' + block_adult_st = 'on' if body.block_adult_sites else 'off' + + cli_api.configure_warp( + all_state=all_st, + popular_sites_state=pop_sites_st, + domestic_sites_state=dom_sites_st, + block_adult_sites_state=block_adult_st + ) return DetailResponse(detail='WARP configured successfully.') except Exception as e: - raise HTTPException(status_code=400, detail=f'Error: {str(e)}') + raise HTTPException(status_code=404, detail=f'Error configuring WARP: {str(e)}') -@router.get('/status', response_model=StatusResponse, summary='Get WARP Status') +@router.get('/status', response_model=StatusResponse, summary='Get WARP Status', name="status_warp") async def status(): try: status_json_str = cli_api.warp_status() diff --git a/core/scripts/webpanel/routers/api/v1/schema/config/hysteria.py b/core/scripts/webpanel/routers/api/v1/schema/config/hysteria.py index 82e51a4..444db27 100644 --- a/core/scripts/webpanel/routers/api/v1/schema/config/hysteria.py +++ b/core/scripts/webpanel/routers/api/v1/schema/config/hysteria.py @@ -18,3 +18,6 @@ class GetPortResponse(BaseModel): class GetSniResponse(BaseModel): sni: str + +class GetObfsResponse(BaseModel): + obfs: str \ No newline at end of file diff --git a/core/scripts/webpanel/templates/settings.html b/core/scripts/webpanel/templates/settings.html index c4c5c58..9a532fd 100644 --- a/core/scripts/webpanel/templates/settings.html +++ b/core/scripts/webpanel/templates/settings.html @@ -40,6 +40,11 @@ Change SNI + +