feat: Add WARP management tab to settings page

Integrates WARP (install, uninstall, configure, status) functionality
into the web panel's settings page. Users can now manage WARP
directly from the UI.
This commit is contained in:
Whispering Wind
2025-06-02 13:29:48 +03:30
committed by GitHub
parent 5e61afe15c
commit 3a13108181
6 changed files with 406 additions and 176 deletions

View File

@ -367,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():

View File

@ -462,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)

View File

@ -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
)

View File

@ -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()

View File

@ -66,6 +66,10 @@
aria-controls='decoy' aria-selected='false'><i class="fas fa-mask"></i>
Decoy Site</a>
</li>
<li class='nav-item'>
<a class='nav-link' id='warp-tab-link' data-toggle='pill' href='#warp-content' role='tab'
aria-controls='warp-content' aria-selected='false'><i class="fas fa-cloud"></i> WARP</a>
</li>
</ul>
</div>
<div class='card-body' style="margin-left: 25px;">
@ -330,6 +334,63 @@
</div>
</div>
<!-- WARP Tab -->
<div class='tab-pane fade' id='warp-content' role='tabpanel' aria-labelledby='warp-tab-link'>
<div id="warp_initial_controls">
<div class='alert alert-info'>WARP service is not active.</div>
<button id="warp_start_btn" type='button' class='btn btn-success mt-3'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Install & Start WARP
</button>
</div>
<div id="warp_active_controls" style="display: none;">
<div class='alert alert-success mb-3'>WARP service is active.</div>
<div class="card card-secondary">
<div class="card-header">
<h3 class="card-title"><i class="fas fa-cogs"></i> WARP Configuration</h3>
</div>
<div class="card-body">
<form id="warp_config_form">
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="warp_all_traffic">
<label class="custom-control-label" for="warp_all_traffic">Route All Traffic through WARP</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="warp_popular_sites">
<label class="custom-control-label" for="warp_popular_sites">Route Popular Sites through WARP</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="warp_domestic_sites">
<label class="custom-control-label" for="warp_domestic_sites">Route Domestic Sites through WARP</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
<input type="checkbox" class="custom-control-input" id="warp_block_adult_sites">
<label class="custom-control-label" for="warp_block_adult_sites">Block Adult Sites (WARP Family DNS)</label>
</div>
</div>
<button id="warp_save_config_btn" type='button' class='btn btn-primary'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Save Configuration
</button>
</form>
</div>
</div>
<button id="warp_stop_btn" type='button' class='btn btn-danger mt-3'>
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true" style="display: none;"></span>
Stop & Uninstall WARP
</button>
</div>
</div>
</div>
</div>
<!-- /.card -->
@ -543,7 +604,8 @@
const servicesMap = {
"hysteria_telegram_bot": "#telegram_form",
"hysteria_normal_sub": "#normal_sub_service_form",
"hysteria_iplimit": "#ip-limit-service"
"hysteria_iplimit": "#ip-limit-service",
"hysteria_warp": "warp_service"
};
Object.keys(servicesMap).forEach(service => {
@ -599,7 +661,18 @@
$("#max_ips").val("");
$("#block_duration, #max_ips").removeClass('is-invalid');
}
} else {
} else if (service === "hysteria_warp") {
const isWarpServiceRunning = data[service];
if (isWarpServiceRunning) {
$("#warp_initial_controls").hide();
$("#warp_active_controls").show();
fetchWarpFullStatusAndConfig();
} else {
$("#warp_initial_controls").show();
$("#warp_active_controls").hide();
$("#warp_config_form")[0].reset();
}
} else {
const $formSelector = $(targetSelector);
if (isRunning) {
$formSelector.find(".form-group").hide();
@ -1013,6 +1086,84 @@
});
}
function fetchWarpFullStatusAndConfig() {
$.ajax({
url: "{{ url_for('status_warp') }}",
type: "GET",
success: function (data) {
$("#warp_all_traffic").prop('checked', data.all_traffic_via_warp || false);
$("#warp_popular_sites").prop('checked', data.popular_sites_via_warp || false);
$("#warp_domestic_sites").prop('checked', data.domestic_sites_via_warp || false);
$("#warp_block_adult_sites").prop('checked', data.block_adult_content || false);
$("#warp_initial_controls").hide();
$("#warp_active_controls").show();
},
error: function (xhr, status, error) {
let errorMsg = "Failed to fetch WARP configuration.";
if (xhr.responseJSON && xhr.responseJSON.detail) {
errorMsg = xhr.responseJSON.detail;
}
console.error("Error fetching WARP config:", errorMsg, xhr.responseText);
if (xhr.status === 404) {
$("#warp_initial_controls").show();
$("#warp_active_controls").hide();
$("#warp_config_form")[0].reset();
Swal.fire("Info", "WARP service might not be fully configured. Please try reinstalling if issues persist.", "info");
} else {
$("#warp_config_form")[0].reset();
Swal.fire("Warning", "Could not load current WARP configuration values. Please check manually or re-save.", "warning");
}
}
});
}
$("#warp_start_btn").on("click", function() {
confirmAction("install and start WARP", function () {
sendRequest(
"{{ url_for('install_warp') }}",
"POST",
null,
"WARP installation request sent. The page will reload.",
"#warp_start_btn",
true
);
});
});
$("#warp_stop_btn").on("click", function() {
confirmAction("stop and uninstall WARP", function () {
sendRequest(
"{{ url_for('uninstall_warp') }}",
"DELETE",
null,
"WARP uninstallation request sent. The page will reload.",
"#warp_stop_btn",
true
);
});
});
$("#warp_save_config_btn").on("click", function() {
const configData = {
all: $("#warp_all_traffic").is(":checked"),
popular_sites: $("#warp_popular_sites").is(":checked"),
domestic_sites: $("#warp_domestic_sites").is(":checked"),
block_adult_sites: $("#warp_block_adult_sites").is(":checked")
};
confirmAction("save WARP configuration", function () {
sendRequest(
"{{ url_for('configure_warp') }}",
"POST",
configData,
"WARP configuration saved successfully!",
"#warp_save_config_btn",
false,
fetchWarpFullStatusAndConfig
);
});
});
$("#telegram_start").on("click", startTelegram);