Files
3x-ui/frontend/src/pages/settings/happRoutingEditor.ts
T
NgaiYeanCoi 5e15120cec feat(happ): add routing editor, optional ad blocking, and LAN bypass preset (#6545)
* feat(happ): make ad blocking optional in routing presets

Add an independent AdBlock toggle for Iran, China, and global presets, applied only when generating routing rules. Update the China preset to Bypass-CN and cover preset behavior and localized controls.

* feat: add visual routing editor with JSON support and localization updates

- Implemented a new modal for editing routing profiles with basic and advanced tabs.
- Added functionality to load, parse, and generate routing profiles in JSON format.
- Enhanced user experience with validation and error handling for JSON input.
- Updated translations for Russian, Turkish, Ukrainian, Vietnamese, Chinese (Simplified and Traditional) to include new routing editor terms.
- Created helper functions for managing routing profiles and generating deep links.
- Added unit tests for routing editor functionalities and JSON handling.

* feat: update routing editor to preserve null lists in profiles and enhance validation messages
2026-09-26 22:57:01 +02:00

91 lines
2.9 KiB
TypeScript

import { HappRoutingProfileSchema, type HappRoutingProfile } from '@/schemas/happRouting';
import { toBase64Utf8 } from './happPresets';
import { isRemoteRoutingSource } from './subscriptionShared';
export type HappRoutingMode = 'add' | 'onadd';
export type HappRoutingListKey =
| 'DirectSites'
| 'DirectIp'
| 'ProxySites'
| 'ProxyIp'
| 'BlockSites'
| 'BlockIp';
export type HappRoutingLoadResult =
| { success: true; profile: HappRoutingProfile; mode: HappRoutingMode; isNew: boolean }
| { success: false; error: 'off' | 'remote' | 'invalid' };
export function parseHappRoutingJson(input: string): HappRoutingProfile | null {
try {
const value: unknown = JSON.parse(input);
const result = HappRoutingProfileSchema.safeParse(value);
// Keep the validated input object; schema output can discard some unknown extension keys.
return result.success ? (value as HappRoutingProfile) : null;
} catch {
return null;
}
}
export function loadHappRouting(input: string): HappRoutingLoadResult {
const source = input.trim();
if (!source) {
return {
success: true,
profile: {
Name: 'Custom Rules',
GlobalProxy: 'true',
DirectSites: [],
DirectIp: [],
ProxySites: [],
ProxyIp: [],
BlockSites: [],
BlockIp: [],
DomainStrategy: 'IPIfNonMatch',
},
mode: 'onadd',
isNew: true,
};
}
if (source === 'happ://routing/off') return { success: false, error: 'off' };
if (isRemoteRoutingSource(source)) return { success: false, error: 'remote' };
if (source.startsWith('{')) {
const profile = parseHappRoutingJson(source);
return profile
? { success: true, profile, mode: 'onadd', isNew: false }
: { success: false, error: 'invalid' };
}
const match = /^happ:\/\/routing\/(onadd|add)\/([A-Za-z0-9+/_-]+={0,2})$/.exec(source);
if (!match) return { success: false, error: 'invalid' };
try {
const binary = atob(match[2].replace(/-/g, '+').replace(/_/g, '/'));
// Decode UTF-8 strictly so malformed bytes cannot silently change profile names or rules.
const json = new TextDecoder('utf-8', { fatal: true }).decode(
Uint8Array.from(binary, (character) => character.charCodeAt(0)),
);
const profile = parseHappRoutingJson(json);
return profile
? { success: true, profile, mode: match[1] as HappRoutingMode, isNew: false }
: { success: false, error: 'invalid' };
} catch {
return { success: false, error: 'invalid' };
}
}
export function buildHappRoutingDeeplink(
profile: HappRoutingProfile,
mode: HappRoutingMode = 'onadd',
): string {
return `happ://routing/${mode}/` + toBase64Utf8(JSON.stringify(profile));
}
export function parseHappRoutingList(input: string): string[] {
// Commas can belong to regexp rules, so the editor uses one rule per line.
return input
.split(/\r?\n/)
.map((entry) => entry.trim())
.filter(Boolean);
}