mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-09-28 12:46:41 +08:00
6ab718f813
Happ ships its local SOCKS5 (127.0.0.1:10808) and HTTP inbounds with authorization disabled by default. Any app on the same device can then connect to that proxy, bypassing Android's per-app VPN routing, and learn the VPN server address - the leak publicly described in March-April 2026 for Happ, v2rayNG and other VLESS clients. Happ fixed its Xray API exposure, but the unauthenticated local proxy remained. Happ exposes a standard subscription header for this (no Provider ID required): socks-auth-mode / http-auth-mode = auto|manual|from-json| disable. A new subscription setting, subHappLocalProxyAuth (default "auto"), sends both headers to Happ clients. Like every other Happ header it is emitted only when Happ auto-detect is enabled and the User-Agent is Happ, so panels that never opted into the Happ integration see no change. An empty value sends nothing and keeps the client's own setting. Verified on Happ Android 4.4.1 (Xray 26.7.28): a subscription carrying socks-auth-mode manual + a test user/password switched the client's Inbounds screen to Manual with those credentials on "refresh subscription", and "auto" switched it to Auto with generated credentials. Co-authored-by: Kirill Rudenko <rudenko@npp-energy.ru> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Sanaei <ho3ein.sanaei@gmail.com>
152 lines
4.4 KiB
Go
152 lines
4.4 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
|
|
LocalProxyAuth string
|
|
}
|
|
|
|
// IsHappClient checks if the client user-agent identifies as Happ.
|
|
func IsHappClient(userAgent string) bool {
|
|
return happUserAgentRegex.MatchString(userAgent)
|
|
}
|
|
|
|
func sanitizeHeaderValue(v string) string {
|
|
return strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(v), "\r", ""), "\n", "")
|
|
}
|
|
|
|
// 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", strings.TrimSpace(cfg.ProviderId))
|
|
}
|
|
if cfg.NewUrl != "" {
|
|
c.Writer.Header().Set("New-Url", strings.TrimSpace(cfg.NewUrl))
|
|
}
|
|
if cfg.FallbackUrl != "" {
|
|
c.Writer.Header().Set("Fallback-Url", strings.TrimSpace(cfg.FallbackUrl))
|
|
}
|
|
if text := sanitizeHeaderValue(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 := sanitizeHeaderValue(cfg.SubInfoButtonText); btnText != "" {
|
|
c.Writer.Header().Set("Sub-Info-Button-Text", btnText)
|
|
}
|
|
if btnLink := sanitizeHeaderValue(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 := sanitizeHeaderValue(cfg.ColorProfile); profile != "" {
|
|
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)
|
|
}
|
|
}
|
|
// Happ's local SOCKS/HTTP inbounds default to no auth, so any app on the device can use them
|
|
// to reach the tunnel and learn the server address; these are standard headers (no ProviderID).
|
|
if mode := strings.TrimSpace(cfg.LocalProxyAuth); mode != "" {
|
|
c.Writer.Header().Set("Socks-Auth-Mode", mode)
|
|
c.Writer.Header().Set("Http-Auth-Mode", mode)
|
|
}
|
|
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)
|
|
}
|
|
}
|
|
}
|