incy.go 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. package sub
  2. import (
  3. "encoding/base64"
  4. "regexp"
  5. "strings"
  6. "github.com/gin-gonic/gin"
  7. )
  8. // INCY clients identify themselves as INCY/<version>/<platform>.
  9. var incyUserAgentRegex = regexp.MustCompile(`(?i)\bincy\b`)
  10. var (
  11. incyRangeRegex = regexp.MustCompile(`^\d+(-\d+)?$`)
  12. incyHexRegex = regexp.MustCompile(`^#[0-9a-fA-F]{3,8}$`)
  13. // Incy documents fragmentation-packets as tlshello | 1-3 | 1 | all.
  14. incyPacketsRangeRegex = regexp.MustCompile(`^\d+-\d+$`)
  15. )
  16. // IncyConfig holds the Incy app-management headers the panel can push.
  17. // A "" field omits its header, so an untouched panel never overrides the app.
  18. type IncyConfig struct {
  19. AutoDetect bool
  20. ProfileDescription string
  21. SortOrder string
  22. SupportEmail string
  23. AnnounceUrl string
  24. PremiumUrl string
  25. BannerText string
  26. BannerButtonText string
  27. BannerButtonUrl string
  28. BannerBgColor string
  29. BannerButtonColor string
  30. HideUrl string
  31. HideCheck string
  32. NoLimitEnabled string
  33. PerAppProxyEnable string
  34. PerAppProxyMode string
  35. PerAppProxyList string
  36. FragmentationEnable string
  37. FragmentationLength string
  38. FragmentationInterval string
  39. FragmentationPackets string
  40. NoisesEnable string
  41. NoisesType string
  42. NoisesPacket string
  43. NoisesDelay string
  44. ServerAddressResolveEnable string
  45. ServerAddressResolveDnsDomain string
  46. ServerAddressResolveDnsIp string
  47. }
  48. // IsIncyClient checks if the client user-agent identifies as INCY.
  49. func IsIncyClient(userAgent string) bool {
  50. return incyUserAgentRegex.MatchString(userAgent)
  51. }
  52. // incyOnOff maps a switch setting to the documented `1`/`0` literal. An
  53. // unrecognised or unset value stays empty so the header is omitted.
  54. func incyOnOff(v string) string {
  55. switch strings.ToLower(strings.TrimSpace(v)) {
  56. case "1", "true", "yes", "on":
  57. return "1"
  58. case "0", "false", "no", "off":
  59. return "0"
  60. }
  61. return ""
  62. }
  63. // incyEnum passes through only a documented literal from a fixed value set.
  64. func incyEnum(v string, allowed ...string) string {
  65. value := strings.ToLower(strings.TrimSpace(v))
  66. for _, a := range allowed {
  67. if value == a {
  68. return value
  69. }
  70. }
  71. return ""
  72. }
  73. // incyMatch returns the trimmed value only when it matches a documented shape.
  74. func incyMatch(v string, re *regexp.Regexp) string {
  75. value := strings.TrimSpace(v)
  76. if re.MatchString(value) {
  77. return value
  78. }
  79. return ""
  80. }
  81. func incyASCII(v string) bool {
  82. for i := 0; i < len(v); i++ {
  83. if v[i] > 0x7e || v[i] < 0x20 {
  84. return false
  85. }
  86. }
  87. return true
  88. }
  89. // incyPackageList joins a comma- or line-separated app list as CSV, since a
  90. // header cannot carry the newlines the settings textarea accepts.
  91. func incyPackageList(v string) string {
  92. entries := strings.FieldsFunc(v, func(r rune) bool { return r == ',' || r == '\n' || r == '\r' })
  93. kept := entries[:0]
  94. for _, entry := range entries {
  95. if entry = strings.TrimSpace(entry); entry != "" {
  96. kept = append(kept, entry)
  97. }
  98. }
  99. return strings.Join(kept, ",")
  100. }
  101. // incyHeaderText base64-wraps non-ASCII text because the docs require
  102. // `base64:<...>` for anything outside the ASCII range (Cyrillic, CJK, emoji).
  103. func incyHeaderText(v string) string {
  104. text := sanitizeHeaderValue(v)
  105. if text == "" || incyASCII(text) {
  106. return text
  107. }
  108. return "base64:" + base64.StdEncoding.EncodeToString([]byte(text))
  109. }
  110. // ApplyIncyHeaders sets the Incy app-management headers from
  111. // https://docs.incy.cc/en/app-management/ (matched case-insensitively).
  112. func ApplyIncyHeaders(c *gin.Context, cfg IncyConfig, isIncy bool) {
  113. if c == nil || c.Writer == nil || !cfg.AutoDetect || !isIncy {
  114. return
  115. }
  116. h := c.Writer.Header()
  117. set := func(name, value string) {
  118. if value != "" {
  119. h.Set(name, value)
  120. }
  121. }
  122. set("Profile-Description", incyHeaderText(cfg.ProfileDescription))
  123. set("Sort-Order", incyEnum(cfg.SortOrder, "none", "ping", "name"))
  124. set("Support-Email", sanitizeHeaderValue(cfg.SupportEmail))
  125. set("Announce-Url", sanitizeHeaderValue(cfg.AnnounceUrl))
  126. set("Premium-Url", sanitizeHeaderValue(cfg.PremiumUrl))
  127. set("Banner-Text", incyHeaderText(cfg.BannerText))
  128. set("Banner-Button-Text", incyHeaderText(cfg.BannerButtonText))
  129. set("Banner-Button-Url", sanitizeHeaderValue(cfg.BannerButtonUrl))
  130. set("Banner-Bg-Color", incyMatch(cfg.BannerBgColor, incyHexRegex))
  131. set("Banner-Button-Color", incyMatch(cfg.BannerButtonColor, incyHexRegex))
  132. set("Hide-Url", incyOnOff(cfg.HideUrl))
  133. set("Hide-Check", incyOnOff(cfg.HideCheck))
  134. set("No-Limit-Enabled", incyOnOff(cfg.NoLimitEnabled))
  135. set("Per-App-Proxy-Enable", incyOnOff(cfg.PerAppProxyEnable))
  136. set("Per-App-Proxy-Mode", incyEnum(cfg.PerAppProxyMode, "bypass", "proxy"))
  137. set("Per-App-Proxy-List", incyPackageList(cfg.PerAppProxyList))
  138. set("Fragmentation-Enable", incyOnOff(cfg.FragmentationEnable))
  139. set("Fragmentation-Length", incyMatch(cfg.FragmentationLength, incyRangeRegex))
  140. set("Fragmentation-Interval", incyMatch(cfg.FragmentationInterval, incyRangeRegex))
  141. if packets := strings.ToLower(strings.TrimSpace(cfg.FragmentationPackets)); packets != "" {
  142. if packets == "tlshello" || packets == "all" || packets == "1" || incyPacketsRangeRegex.MatchString(packets) {
  143. set("Fragmentation-Packets", packets)
  144. }
  145. }
  146. set("Noises-Enable", incyOnOff(cfg.NoisesEnable))
  147. set("Noises-Type", incyEnum(cfg.NoisesType, "rand", "str", "hex"))
  148. set("Noises-Packet", sanitizeHeaderValue(cfg.NoisesPacket))
  149. set("Noises-Delay", incyMatch(cfg.NoisesDelay, incyRangeRegex))
  150. set("Server-Address-Resolve-Enable", incyOnOff(cfg.ServerAddressResolveEnable))
  151. set("Server-Address-Resolve-Dns-Domain", sanitizeHeaderValue(cfg.ServerAddressResolveDnsDomain))
  152. set("Server-Address-Resolve-Dns-Ip", sanitizeHeaderValue(cfg.ServerAddressResolveDnsIp))
  153. }