Skip to content

Commit 5976274

Browse files
committed
fix(subscription): implement host-specific xray template overrides and add corresponding tests
1 parent 395cd97 commit 5976274

File tree

3 files changed

+91
-17
lines changed

3 files changed

+91
-17
lines changed

app/subscription/share.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,16 @@ async def process_inbounds_and_tags(
353353
hosts = await filter_hosts(list((await host_manager.get_hosts()).values()), user.status)
354354
if randomize_order and len(hosts) > 1:
355355
random.shuffle(hosts)
356+
def _resolve_host_xray_template_content(inbound: SubscriptionInboundData) -> str | None:
357+
if xray_template_overrides is None:
358+
return None
359+
if not isinstance(inbound.subscription_templates, dict):
360+
return None
361+
template_id = inbound.subscription_templates.get("xray")
362+
if not isinstance(template_id, int):
363+
return None
364+
return xray_template_overrides.get(template_id)
365+
356366
for host_data in hosts:
357367
result = await process_host(host_data, format_variables, user.inbounds, proxy_settings)
358368
if not result:
@@ -379,14 +389,7 @@ async def process_inbounds_and_tags(
379389
inbound_copy.transport_config.download_settings = processed_download_settings
380390

381391
if isinstance(conf, XrayConfiguration):
382-
template_content = None
383-
if (
384-
xray_template_overrides is not None
385-
and isinstance(inbound_copy.subscription_templates, dict)
386-
and isinstance((template_id := inbound_copy.subscription_templates.get("xray")), int)
387-
):
388-
template_content = xray_template_overrides.get(template_id)
389-
392+
template_content = _resolve_host_xray_template_content(inbound_copy)
390393
conf.add(
391394
remark=remark,
392395
address=formatted_address,

dashboard/src/components/dialogs/host-modal.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -828,21 +828,18 @@ const HostModal: React.FC<HostModalProps> = ({ isDialogOpen, onOpenChange, onSub
828828
</PopoverContent>
829829
</Popover>
830830
</div>
831-
<Select
832-
dir={dir}
833-
value={field.value != null ? String(field.value) : '__default__'}
834-
onValueChange={value => field.onChange(value === '__default__' ? undefined : Number.parseInt(value, 10))}
835-
disabled={isLoadingXrayTemplates}
836-
>
831+
<Select
832+
dir={dir}
833+
value={field.value != null ? String(field.value) : ''}
834+
onValueChange={value => field.onChange(value ? Number.parseInt(value, 10) : undefined)}
835+
disabled={isLoadingXrayTemplates}
836+
>
837837
<FormControl>
838838
<SelectTrigger className="py-5">
839839
<SelectValue placeholder={isLoadingXrayTemplates ? t('loading', { defaultValue: 'Loading...' }) : t('hostsDialog.selectXrayTemplate')} />
840840
</SelectTrigger>
841841
</FormControl>
842842
<SelectContent dir={dir}>
843-
<SelectItem className="cursor-pointer px-4" value="__default__">
844-
{t('hostsDialog.inboundDefault')}
845-
</SelectItem>
846843
{isLoadingXrayTemplates ? (
847844
<SelectItem className="px-4" value="__loading_xray_templates__" disabled>
848845
<span className="flex items-center gap-2">

tests/api/test_user.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -509,6 +509,80 @@ def test_xray_subscription_uses_host_specific_template_override(access_token):
509509
delete_core(access_token, core["id"])
510510

511511

512+
def test_xray_subscription_template_override_isolated_per_host(access_token):
513+
core = create_core(access_token)
514+
inbound = get_inbounds(access_token)[0]
515+
override_template = create_client_template(
516+
access_token,
517+
name=unique_name("xray_host_isolated_template"),
518+
template_type="xray_subscription",
519+
content=json.dumps(
520+
{
521+
"log": {"loglevel": "warning"},
522+
"inbounds": [{"tag": "placeholder", "protocol": "vmess", "settings": {"clients": []}}],
523+
"outbounds": [{"tag": "template-marker", "protocol": "freedom", "settings": {}}],
524+
}
525+
),
526+
)
527+
528+
first_host_response = client.post(
529+
"/api/host",
530+
headers=auth_headers(access_token),
531+
json={
532+
"remark": "Host With Template {USERNAME}",
533+
"address": ["198.51.100.60"],
534+
"port": 443,
535+
"sni": ["host-template.example.com"],
536+
"inbound_tag": inbound,
537+
"priority": 1,
538+
"subscription_templates": {"xray": override_template["id"]},
539+
},
540+
)
541+
assert first_host_response.status_code == status.HTTP_201_CREATED
542+
first_host_id = first_host_response.json()["id"]
543+
544+
second_host_response = client.post(
545+
"/api/host",
546+
headers=auth_headers(access_token),
547+
json={
548+
"remark": "Host Without Template {USERNAME}",
549+
"address": ["198.51.100.61"],
550+
"port": 443,
551+
"sni": ["host-default.example.com"],
552+
"inbound_tag": inbound,
553+
"priority": 2,
554+
},
555+
)
556+
assert second_host_response.status_code == status.HTTP_201_CREATED
557+
second_host_id = second_host_response.json()["id"]
558+
559+
group = create_group(access_token, name=unique_name("xray_isolated_group"), inbound_tags=[inbound])
560+
user = create_user(access_token, group_ids=[group["id"]], payload={"username": unique_name("xray_isolated_user")})
561+
562+
try:
563+
response = client.get(f"{user['subscription_url']}/xray")
564+
assert response.status_code == status.HTTP_200_OK
565+
566+
configs = response.json()
567+
assert isinstance(configs, list)
568+
assert len(configs) == 2
569+
570+
marker_count = 0
571+
for config in configs:
572+
outbounds = config.get("outbounds", [])
573+
if any(outbound.get("tag") == "template-marker" for outbound in outbounds):
574+
marker_count += 1
575+
576+
assert marker_count == 1
577+
finally:
578+
delete_user(access_token, user["username"])
579+
delete_group(access_token, group["id"])
580+
client.delete(f"/api/host/{first_host_id}", headers=auth_headers(access_token))
581+
client.delete(f"/api/host/{second_host_id}", headers=auth_headers(access_token))
582+
delete_client_template(access_token, override_template["id"])
583+
delete_core(access_token, core["id"])
584+
585+
512586
def test_singbox_subscription_includes_wireguard_outbound(access_token):
513587
interface_private_key, interface_public_key = generate_wireguard_keypair()
514588
pre_shared_key, _ = generate_wireguard_keypair()

0 commit comments

Comments
 (0)