json_routing.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366
  1. package sub
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "maps"
  7. "slices"
  8. "strings"
  9. "sync/atomic"
  10. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  11. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  12. )
  13. // jsonRoutingSpec is the canonical form of the generic Happ/INCY routing
  14. // payload (inline JSON, happ:// or incy:// deeplink, or remote URL).
  15. type jsonRoutingSpec struct {
  16. DomainStrategy string
  17. RemoteDNSDomain string
  18. RemoteDNSIP string
  19. DomesticDNSDomain string
  20. DomesticDNSIP string
  21. DnsHosts map[string]string
  22. RouteOrder []string // e.g. {"block","proxy","direct"}; default {"block","direct","proxy"}
  23. DirectSites []string
  24. DirectIp []string
  25. ProxySites []string
  26. ProxyIp []string
  27. BlockSites []string
  28. BlockIp []string
  29. }
  30. func (s jsonRoutingSpec) empty() bool {
  31. return s.DomainStrategy == "" && s.RemoteDNSDomain == "" && s.RemoteDNSIP == "" &&
  32. s.DomesticDNSDomain == "" && s.DomesticDNSIP == "" && len(s.DnsHosts) == 0 &&
  33. len(s.RouteOrder) == 0 && len(s.DirectSites) == 0 && len(s.DirectIp) == 0 &&
  34. len(s.ProxySites) == 0 && len(s.ProxyIp) == 0 && len(s.BlockSites) == 0 && len(s.BlockIp) == 0
  35. }
  36. // equal reports whether two specs carry the same routing payload, so a
  37. // rebuilt template only replaces the memoised one when the profile changed.
  38. func (s jsonRoutingSpec) equal(other jsonRoutingSpec) bool {
  39. return s.DomainStrategy == other.DomainStrategy &&
  40. s.RemoteDNSDomain == other.RemoteDNSDomain && s.RemoteDNSIP == other.RemoteDNSIP &&
  41. s.DomesticDNSDomain == other.DomesticDNSDomain && s.DomesticDNSIP == other.DomesticDNSIP &&
  42. maps.Equal(s.DnsHosts, other.DnsHosts) && slices.Equal(s.RouteOrder, other.RouteOrder) &&
  43. slices.Equal(s.DirectSites, other.DirectSites) && slices.Equal(s.DirectIp, other.DirectIp) &&
  44. slices.Equal(s.ProxySites, other.ProxySites) && slices.Equal(s.ProxyIp, other.ProxyIp) &&
  45. slices.Equal(s.BlockSites, other.BlockSites) && slices.Equal(s.BlockIp, other.BlockIp)
  46. }
  47. // Both happ forms appear in the wild; normalizeHappRouting accepts each.
  48. var jsonRoutingDeeplinkPrefixes = []string{
  49. "happ://routing/onadd/", "happ://routing/add/", "incy://routing/onadd/",
  50. }
  51. // bakedTemplate resolves once per emitted document, so an unusable profile
  52. // must not write one identical warning per document on every public fetch.
  53. var lastJsonRoutingWarning atomic.Value
  54. // resolveJsonRoutingSpec parses the routing payload, degrading to an empty
  55. // spec on error — a bad setting must never take the subscription server down.
  56. func resolveJsonRoutingSpec(raw string) jsonRoutingSpec {
  57. spec, remote, err := parseJsonRoutingSpec(raw)
  58. if err != nil {
  59. warning := "subJsonRoutingRules: " + err.Error()
  60. if remote {
  61. warning = "subJsonRoutingRules: remote source unavailable, emitting default routing"
  62. }
  63. if previous, _ := lastJsonRoutingWarning.Load().(string); previous != warning {
  64. lastJsonRoutingWarning.Store(warning)
  65. logger.Warning(warning)
  66. }
  67. return jsonRoutingSpec{}
  68. }
  69. lastJsonRoutingWarning.Store("")
  70. return spec
  71. }
  72. // jsonRoutingHeaderSource turns the JSON routing setting into a Routing header
  73. // value; blank means unusable, and the header then stays unset.
  74. func jsonRoutingHeaderSource(raw string) string {
  75. trimmed := strings.TrimSpace(raw)
  76. if trimmed == "" {
  77. return ""
  78. }
  79. if strings.HasPrefix(trimmed, "incy://") {
  80. _, rest, ok := cutAnyPrefix(trimmed, jsonRoutingDeeplinkPrefixes)
  81. if !ok {
  82. return ""
  83. }
  84. decoded, err := decodeRoutingBase64(rest)
  85. if err != nil {
  86. return ""
  87. }
  88. if _, err := validateAndCompactJSONObject(decoded); err != nil || len(trimmed) > remoteRoutingHappMaxValue {
  89. return ""
  90. }
  91. return trimmed
  92. }
  93. resolved, _, err := resolveRoutingSource(remoteRoutingJson, trimmed)
  94. if err != nil {
  95. return ""
  96. }
  97. content, err := normalizeHappRouting([]byte(resolved))
  98. if err != nil || len(content) > remoteRoutingHappMaxValue {
  99. return ""
  100. }
  101. return content
  102. }
  103. // parseJsonRoutingSpec resolves raw (inline JSON, happ:// or incy:// deeplink,
  104. // or https:// URL) into a spec; the caller degrades on error, never fails.
  105. func parseJsonRoutingSpec(raw string) (jsonRoutingSpec, bool, error) {
  106. trimmed := strings.TrimSpace(raw)
  107. if trimmed == "" {
  108. return jsonRoutingSpec{}, false, nil
  109. }
  110. if _, remote, err := common.ParseRemoteRoutingURL(trimmed); remote {
  111. if err != nil {
  112. return jsonRoutingSpec{}, true, err
  113. }
  114. resolved, remote, err := resolveRoutingSource(remoteRoutingJson, trimmed)
  115. if err != nil || !remote {
  116. return jsonRoutingSpec{}, true, err
  117. }
  118. trimmed = resolved
  119. }
  120. payload := []byte(trimmed)
  121. if _, rest, ok := cutAnyPrefix(trimmed, jsonRoutingDeeplinkPrefixes); ok {
  122. decoded, err := decodeRoutingBase64(rest)
  123. if err != nil {
  124. return jsonRoutingSpec{}, false, fmt.Errorf("invalid routing deeplink payload: %w", err)
  125. }
  126. payload = decoded
  127. } else if !strings.HasPrefix(trimmed, "{") {
  128. return jsonRoutingSpec{}, false, errors.New("routing payload must be a JSON object or a happ/incy deeplink")
  129. }
  130. var object map[string]any
  131. if err := json.Unmarshal(payload, &object); err != nil {
  132. return jsonRoutingSpec{}, false, fmt.Errorf("invalid routing payload JSON: %w", err)
  133. }
  134. if object == nil {
  135. return jsonRoutingSpec{}, false, errors.New("routing payload must be a JSON object")
  136. }
  137. spec, err := buildJsonRoutingSpec(object)
  138. if err != nil {
  139. return jsonRoutingSpec{}, false, err
  140. }
  141. return spec, false, nil
  142. }
  143. func cutAnyPrefix(s string, prefixes []string) (string, string, bool) {
  144. for _, prefix := range prefixes {
  145. if after, ok := strings.CutPrefix(s, prefix); ok {
  146. return prefix, after, true
  147. }
  148. }
  149. return "", "", false
  150. }
  151. func buildJsonRoutingSpec(object map[string]any) (jsonRoutingSpec, error) {
  152. spec := jsonRoutingSpec{}
  153. var err error
  154. if spec.DomainStrategy, err = routingString(object, "DomainStrategy"); err != nil {
  155. return spec, err
  156. }
  157. if spec.RemoteDNSDomain, err = routingString(object, "RemoteDNSDomain"); err != nil {
  158. return spec, err
  159. }
  160. if spec.RemoteDNSIP, err = routingString(object, "RemoteDNSIP"); err != nil {
  161. return spec, err
  162. }
  163. if spec.DomesticDNSDomain, err = routingString(object, "DomesticDNSDomain"); err != nil {
  164. return spec, err
  165. }
  166. if spec.DomesticDNSIP, err = routingString(object, "DomesticDNSIP"); err != nil {
  167. return spec, err
  168. }
  169. if spec.DirectSites, err = routingList(object, "DirectSites"); err != nil {
  170. return spec, err
  171. }
  172. if spec.DirectIp, err = routingList(object, "DirectIp"); err != nil {
  173. return spec, err
  174. }
  175. if spec.ProxySites, err = routingList(object, "ProxySites"); err != nil {
  176. return spec, err
  177. }
  178. if spec.ProxyIp, err = routingList(object, "ProxyIp"); err != nil {
  179. return spec, err
  180. }
  181. if spec.BlockSites, err = routingList(object, "BlockSites"); err != nil {
  182. return spec, err
  183. }
  184. if spec.BlockIp, err = routingList(object, "BlockIp"); err != nil {
  185. return spec, err
  186. }
  187. if spec.DnsHosts, err = routingHosts(object, "DnsHosts"); err != nil {
  188. return spec, err
  189. }
  190. order, err := routingString(object, "RouteOrder")
  191. if err != nil {
  192. return spec, err
  193. }
  194. for _, segment := range strings.Split(order, "-") {
  195. switch segment {
  196. case "block", "proxy", "direct":
  197. spec.RouteOrder = append(spec.RouteOrder, segment)
  198. }
  199. }
  200. return spec, nil
  201. }
  202. func routingString(object map[string]any, key string) (string, error) {
  203. value, ok := object[key]
  204. if !ok || value == nil {
  205. return "", nil
  206. }
  207. text, ok := value.(string)
  208. if !ok {
  209. return "", fmt.Errorf("routing field %q must be a string", key)
  210. }
  211. return text, nil
  212. }
  213. func routingList(object map[string]any, key string) ([]string, error) {
  214. value, ok := object[key]
  215. if !ok || value == nil {
  216. return nil, nil
  217. }
  218. entries, ok := value.([]any)
  219. if !ok {
  220. return nil, fmt.Errorf("routing field %q must be an array of strings", key)
  221. }
  222. list := make([]string, 0, len(entries))
  223. for _, entry := range entries {
  224. text, ok := entry.(string)
  225. if !ok {
  226. return nil, fmt.Errorf("routing field %q must be an array of strings", key)
  227. }
  228. list = append(list, text)
  229. }
  230. return list, nil
  231. }
  232. func routingHosts(object map[string]any, key string) (map[string]string, error) {
  233. value, ok := object[key]
  234. if !ok || value == nil {
  235. return nil, nil
  236. }
  237. raw, ok := value.(map[string]any)
  238. if !ok {
  239. return nil, fmt.Errorf("routing field %q must be a string-to-string map", key)
  240. }
  241. hosts := make(map[string]string, len(raw))
  242. for name, entry := range raw {
  243. address, ok := entry.(string)
  244. if !ok {
  245. return nil, fmt.Errorf("routing field %q must be a string-to-string map", key)
  246. }
  247. hosts[name] = address
  248. }
  249. return hosts, nil
  250. }
  251. func routeOrderGroups(order []string) []string {
  252. if len(order) == 0 {
  253. return []string{"block", "direct", "proxy"}
  254. }
  255. return order
  256. }
  257. // applyJsonRouting patches the base template with the spec's dns and routing
  258. // subtrees, mirroring the njs patcher panel admins previously ran behind nginx.
  259. func applyJsonRouting(configJson map[string]any, spec jsonRoutingSpec) {
  260. domestic := spec.DomesticDNSDomain
  261. if domestic == "" {
  262. ip := spec.DomesticDNSIP
  263. if ip == "" {
  264. ip = "77.88.8.8"
  265. }
  266. domestic = "https://" + ip + "/dns-query"
  267. }
  268. remote := spec.RemoteDNSDomain
  269. if remote == "" {
  270. ip := spec.RemoteDNSIP
  271. if ip == "" {
  272. ip = "8.8.8.8"
  273. }
  274. remote = "https://" + ip + "/dns-query"
  275. }
  276. dns := map[string]any{
  277. "tag": "dns_out",
  278. "queryStrategy": "UseIP",
  279. "servers": []any{},
  280. }
  281. if len(spec.DirectSites) > 0 {
  282. dns["servers"] = append(dns["servers"].([]any), map[string]any{
  283. "address": domestic,
  284. "domains": spec.DirectSites,
  285. })
  286. }
  287. dns["servers"] = append(dns["servers"].([]any), map[string]any{
  288. "address": remote,
  289. "skipFallback": false,
  290. })
  291. if len(spec.DnsHosts) > 0 {
  292. dns["hosts"] = spec.DnsHosts
  293. }
  294. domainStrategy := spec.DomainStrategy
  295. if domainStrategy == "" {
  296. domainStrategy = "IPIfNonMatch"
  297. }
  298. groups := map[string][]map[string]any{
  299. "block": {
  300. {"domain": stringList(spec.BlockSites), "outboundTag": "block"},
  301. {"ip": stringList(spec.BlockIp), "outboundTag": "block"},
  302. },
  303. "direct": {
  304. {"domain": stringList(spec.DirectSites), "outboundTag": "direct"},
  305. {"ip": stringList(spec.DirectIp), "outboundTag": "direct"},
  306. },
  307. "proxy": {
  308. {"domain": stringList(spec.ProxySites), "outboundTag": "proxy"},
  309. {"ip": stringList(spec.ProxyIp), "outboundTag": "proxy"},
  310. },
  311. }
  312. rules := make([]any, 0, len(routeOrderGroups(spec.RouteOrder))*2+1)
  313. for _, group := range routeOrderGroups(spec.RouteOrder) {
  314. for _, rule := range groups[group] {
  315. var key string
  316. if _, ok := rule["domain"]; ok {
  317. key = "domain"
  318. } else {
  319. key = "ip"
  320. }
  321. if len(rule[key].([]string)) == 0 {
  322. continue
  323. }
  324. entry := map[string]any{"type": "field", key: rule[key], "outboundTag": rule["outboundTag"]}
  325. rules = append(rules, entry)
  326. }
  327. }
  328. rules = append(rules, map[string]any{"type": "field", "network": "tcp,udp", "outboundTag": "proxy"})
  329. configJson["dns"] = dns
  330. configJson["routing"] = map[string]any{
  331. "domainStrategy": domainStrategy,
  332. "rules": rules,
  333. }
  334. }
  335. func stringList(list []string) []string {
  336. if len(list) == 0 {
  337. return nil
  338. }
  339. return list
  340. }