mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-11 12:47:13 +00:00
1456658028
* 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.
142 lines
3.9 KiB
Go
142 lines
3.9 KiB
Go
package sub
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
var happUserAgentRegex = regexp.MustCompile(`(?i)\bhapp\b`)
|
|
|
|
// HappConfig holds all Happ client customization parameters.
|
|
type HappConfig struct {
|
|
AutoDetect bool
|
|
ProviderId string
|
|
NewUrl string
|
|
FallbackUrl string
|
|
SubInfoColor string
|
|
SubInfoText string
|
|
SubInfoButtonText string
|
|
SubInfoButtonLink string
|
|
SubExpire bool
|
|
SubExpireButtonLink string
|
|
NotificationExpire bool
|
|
NoLimit bool
|
|
AlwaysHwid bool
|
|
TunMode string
|
|
TunType string
|
|
ExcludeRoutes string
|
|
ExcludeApns bool
|
|
ColorProfile string
|
|
PingType string
|
|
AutoConnect bool
|
|
AutoConnectType string
|
|
PerAppMode string
|
|
PerAppList string
|
|
}
|
|
|
|
// IsHappClient checks if the client user-agent identifies as Happ.
|
|
func IsHappClient(userAgent string) bool {
|
|
return happUserAgentRegex.MatchString(userAgent)
|
|
}
|
|
|
|
// ApplyHappHeaders sets standard and advanced Happ subscription headers.
|
|
func ApplyHappHeaders(c *gin.Context, cfg HappConfig, isHapp bool) {
|
|
if c == nil || c.Writer == nil || !cfg.AutoDetect || !isHapp {
|
|
return
|
|
}
|
|
if cfg.ProviderId != "" {
|
|
c.Writer.Header().Set("ProviderID", cfg.ProviderId)
|
|
}
|
|
if cfg.NewUrl != "" {
|
|
c.Writer.Header().Set("New-Url", cfg.NewUrl)
|
|
}
|
|
if cfg.FallbackUrl != "" {
|
|
c.Writer.Header().Set("Fallback-Url", cfg.FallbackUrl)
|
|
}
|
|
if text := strings.TrimSpace(cfg.SubInfoText); text != "" {
|
|
color := strings.TrimSpace(cfg.SubInfoColor)
|
|
switch strings.ToLower(color) {
|
|
case "primary", "info":
|
|
color = "blue"
|
|
case "success":
|
|
color = "green"
|
|
case "warning", "danger":
|
|
color = "red"
|
|
case "":
|
|
color = "blue"
|
|
}
|
|
c.Writer.Header().Set("Sub-Info-Color", color)
|
|
c.Writer.Header().Set("Sub-Info-Text", text)
|
|
if btnText := strings.TrimSpace(cfg.SubInfoButtonText); btnText != "" {
|
|
c.Writer.Header().Set("Sub-Info-Button-Text", btnText)
|
|
}
|
|
if btnLink := strings.TrimSpace(cfg.SubInfoButtonLink); btnLink != "" {
|
|
c.Writer.Header().Set("Sub-Info-Button-Link", btnLink)
|
|
}
|
|
}
|
|
if cfg.SubExpire {
|
|
c.Writer.Header().Set("Sub-Expire", "1")
|
|
if link := strings.TrimSpace(cfg.SubExpireButtonLink); link != "" {
|
|
c.Writer.Header().Set("Sub-Expire-Button-Link", link)
|
|
}
|
|
}
|
|
if cfg.NotificationExpire {
|
|
c.Writer.Header().Set("Notification-Subs-Expire", "1")
|
|
}
|
|
if cfg.NoLimit {
|
|
c.Writer.Header().Set("No-Limit-Enabled", "1")
|
|
}
|
|
if cfg.AlwaysHwid {
|
|
c.Writer.Header().Set("Subscription-Always-Hwid-Enable", "1")
|
|
}
|
|
if cfg.TunMode != "" {
|
|
c.Writer.Header().Set("Tun-Mode", cfg.TunMode)
|
|
}
|
|
if cfg.TunType != "" {
|
|
c.Writer.Header().Set("Tun-Type", cfg.TunType)
|
|
}
|
|
if routes := strings.TrimSpace(cfg.ExcludeRoutes); routes != "" {
|
|
c.Writer.Header().Set("Exclude-Routes", routes)
|
|
}
|
|
if cfg.ExcludeApns {
|
|
c.Writer.Header().Set("Exclude-Apns-Enable", "true")
|
|
}
|
|
if profile := strings.TrimSpace(cfg.ColorProfile); profile != "" {
|
|
profile = strings.ReplaceAll(strings.ReplaceAll(profile, "\r", ""), "\n", "")
|
|
c.Writer.Header().Set("Color-Profile", profile)
|
|
}
|
|
if ping := strings.TrimSpace(cfg.PingType); ping != "" {
|
|
if strings.EqualFold(ping, "http") {
|
|
ping = "proxy"
|
|
}
|
|
c.Writer.Header().Set("Ping-Type", ping)
|
|
}
|
|
if cfg.AutoConnect {
|
|
c.Writer.Header().Set("Subscription-Autoconnect", "1")
|
|
autoType := strings.TrimSpace(cfg.AutoConnectType)
|
|
switch strings.ToLower(autoType) {
|
|
case "fastest":
|
|
autoType = "lowestdelay"
|
|
case "last":
|
|
autoType = "lastused"
|
|
}
|
|
if autoType != "" {
|
|
c.Writer.Header().Set("Subscription-Autoconnect-Type", autoType)
|
|
}
|
|
}
|
|
if mode := strings.TrimSpace(cfg.PerAppMode); mode != "" && mode != "off" {
|
|
switch strings.ToLower(mode) {
|
|
case "include":
|
|
mode = "on"
|
|
case "exclude":
|
|
mode = "bypass"
|
|
}
|
|
c.Writer.Header().Set("Per-App-Proxy-Mode", mode)
|
|
if list := strings.TrimSpace(cfg.PerAppList); list != "" {
|
|
c.Writer.Header().Set("Per-App-Proxy-List", list)
|
|
}
|
|
}
|
|
}
|