mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-17 07:37:15 +00:00
feat(sub): add Happ client integration, routing presets, and app management (#6434)
* feat(sub): add Happ client integration, routing presets, and app management Implement comprehensive Happ proxy client integration according to official developer specifications. - Fix header emission on disabled routing and hidden settings to send explicit '0' headers rather than omitting, allowing Happ clients to reset cached settings. - Add support for 'happ://routing/off' deeplink in routing validation. - Preserve '?serverDescription=' query parameters in link fragments without escaping to support Happ server subtitles across VMess, VLESS, Trojan and SS. - Add Happ application management headers: ProviderID, New-Url, Fallback-Url, Sub-Info banners, Sub-Expire notifications, No-Limit mode, hardware ID enforcement, TUN modes/types, route exclusions, APNS exclusions, and per-app proxy settings. - Add curated routing presets (Iran Bypass, China Direct, AdBlock, Global) and interactive visual rule generator in frontend settings. - Synchronize all 13 translation locales with native Persian, Russian, and Chinese translations. * fix(sub): keep Happ header overrides behind the auto-detect opt-in The Routing-Enable/Hide-Settings off values were emitted on the User-Agent alone, so every panel that upgraded would push "Routing-Enable: 0" — documented by happ.su as disabling routing globally — to every Happ client without the operator enabling anything. They now ride subHappAutoDetect like every other Happ header. Two further mismatches against the vendor spec: - serverDescription was written as a key of the VMess base64 JSON object. happ.su documents it as a "#Title?serverDescription=<base64>" link parameter or a JSON "meta" entry, so the caption never reached Happ while every other VMess consumer received an unknown key. Dropped rather than moved: emitting the documented form is unsafe here because our own parser base64-decodes the whole VMess body (internal/util/link/outbound.go). - The TUN Mode dropdown stored the literal "default", forwarded as "Tun-Mode: default", where happ.su documents system|gvisor only. It now stores the unset value so no header is sent. TUN Type "default" is a documented value and is unchanged. Each fix carries a test that fails without it.
This commit is contained in:
@@ -102,6 +102,29 @@ var defaultValueMap = map[string]string{
|
||||
"subEnableRouting": "false",
|
||||
"subRoutingRules": "",
|
||||
"subHideSettings": "false",
|
||||
"subHappAutoDetect": "false",
|
||||
"subHappProviderId": "",
|
||||
"subHappNewUrl": "",
|
||||
"subHappFallbackUrl": "",
|
||||
"subHappSubInfoColor": "blue",
|
||||
"subHappSubInfoText": "",
|
||||
"subHappSubInfoButtonText": "",
|
||||
"subHappSubInfoButtonLink": "",
|
||||
"subHappSubExpire": "false",
|
||||
"subHappSubExpireButtonLink": "",
|
||||
"subHappNotificationExpire": "false",
|
||||
"subHappNoLimit": "false",
|
||||
"subHappAlwaysHwid": "false",
|
||||
"subHappTunMode": "",
|
||||
"subHappTunType": "",
|
||||
"subHappExcludeRoutes": "",
|
||||
"subHappExcludeApns": "false",
|
||||
"subHappColorProfile": "",
|
||||
"subHappPingType": "",
|
||||
"subHappAutoConnect": "false",
|
||||
"subHappAutoConnectType": "lowestdelay",
|
||||
"subHappPerAppMode": "off",
|
||||
"subHappPerAppList": "",
|
||||
"subIncyEnableRouting": "false",
|
||||
"subIncyRoutingRules": "",
|
||||
"subListen": "",
|
||||
@@ -820,6 +843,98 @@ func (s *SettingService) GetSubHideSettings() (bool, error) {
|
||||
return s.getBool("subHideSettings")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappAutoDetect() (bool, error) {
|
||||
return s.getBool("subHappAutoDetect")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappProviderId() (string, error) {
|
||||
return s.getString("subHappProviderId")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappNewUrl() (string, error) {
|
||||
return s.getString("subHappNewUrl")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappFallbackUrl() (string, error) {
|
||||
return s.getString("subHappFallbackUrl")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappSubInfoColor() (string, error) {
|
||||
return s.getString("subHappSubInfoColor")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappSubInfoText() (string, error) {
|
||||
return s.getString("subHappSubInfoText")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappSubInfoButtonText() (string, error) {
|
||||
return s.getString("subHappSubInfoButtonText")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappSubInfoButtonLink() (string, error) {
|
||||
return s.getString("subHappSubInfoButtonLink")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappSubExpire() (bool, error) {
|
||||
return s.getBool("subHappSubExpire")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappSubExpireButtonLink() (string, error) {
|
||||
return s.getString("subHappSubExpireButtonLink")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappNotificationExpire() (bool, error) {
|
||||
return s.getBool("subHappNotificationExpire")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappNoLimit() (bool, error) {
|
||||
return s.getBool("subHappNoLimit")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappAlwaysHwid() (bool, error) {
|
||||
return s.getBool("subHappAlwaysHwid")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappTunMode() (string, error) {
|
||||
return s.getString("subHappTunMode")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappTunType() (string, error) {
|
||||
return s.getString("subHappTunType")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappExcludeRoutes() (string, error) {
|
||||
return s.getString("subHappExcludeRoutes")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappExcludeApns() (bool, error) {
|
||||
return s.getBool("subHappExcludeApns")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappColorProfile() (string, error) {
|
||||
return s.getString("subHappColorProfile")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappPingType() (string, error) {
|
||||
return s.getString("subHappPingType")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappAutoConnect() (bool, error) {
|
||||
return s.getBool("subHappAutoConnect")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappAutoConnectType() (string, error) {
|
||||
return s.getString("subHappAutoConnectType")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappPerAppMode() (string, error) {
|
||||
return s.getString("subHappPerAppMode")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubHappPerAppList() (string, error) {
|
||||
return s.getString("subHappPerAppList")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubIncyEnableRouting() (bool, error) {
|
||||
return s.getBool("subIncyEnableRouting")
|
||||
}
|
||||
@@ -1363,6 +1478,16 @@ func validateSettingsURLs(allSetting *entity.AllSetting) error {
|
||||
// the scheme instead of forcing SanitizeHTTPURL's http(s)-only rule.
|
||||
allSetting.SubSupportUrl = common.EnsureURLScheme(allSetting.SubSupportUrl)
|
||||
allSetting.SubProfileUrl = common.EnsureURLScheme(allSetting.SubProfileUrl)
|
||||
for _, ptr := range []*string{
|
||||
&allSetting.SubHappNewUrl,
|
||||
&allSetting.SubHappFallbackUrl,
|
||||
&allSetting.SubHappSubInfoButtonLink,
|
||||
&allSetting.SubHappSubExpireButtonLink,
|
||||
} {
|
||||
if strings.TrimSpace(*ptr) != "" {
|
||||
*ptr = common.EnsureURLScheme(strings.TrimSpace(*ptr))
|
||||
}
|
||||
}
|
||||
for name, value := range map[string]*string{
|
||||
"Happ routing source": &allSetting.SubRoutingRules,
|
||||
"Clash/Mihomo routing source": &allSetting.SubClashRules,
|
||||
|
||||
Reference in New Issue
Block a user