sub.go 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. // Package sub provides subscription server functionality for the 3x-ui panel,
  2. // including HTTP/HTTPS servers for serving subscription links and JSON configurations.
  3. package sub
  4. import (
  5. "context"
  6. "crypto/tls"
  7. "html/template"
  8. "io"
  9. "io/fs"
  10. "net"
  11. "net/http"
  12. "os"
  13. "path/filepath"
  14. "strconv"
  15. "strings"
  16. "github.com/mhsanaei/3x-ui/v2/logger"
  17. "github.com/mhsanaei/3x-ui/v2/util/common"
  18. webpkg "github.com/mhsanaei/3x-ui/v2/web"
  19. "github.com/mhsanaei/3x-ui/v2/web/locale"
  20. "github.com/mhsanaei/3x-ui/v2/web/middleware"
  21. "github.com/mhsanaei/3x-ui/v2/web/network"
  22. "github.com/mhsanaei/3x-ui/v2/web/service"
  23. "github.com/gin-gonic/gin"
  24. )
  25. // setEmbeddedTemplates parses and sets embedded templates on the engine
  26. func setEmbeddedTemplates(engine *gin.Engine) error {
  27. t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
  28. webpkg.EmbeddedHTML(),
  29. "html/common/page.html",
  30. "html/component/aThemeSwitch.html",
  31. "html/settings/panel/subscription/subpage.html",
  32. )
  33. if err != nil {
  34. return err
  35. }
  36. engine.SetHTMLTemplate(t)
  37. return nil
  38. }
  39. // Server represents the subscription server that serves subscription links and JSON configurations.
  40. type Server struct {
  41. httpServer *http.Server
  42. listener net.Listener
  43. sub *SUBController
  44. settingService service.SettingService
  45. ctx context.Context
  46. cancel context.CancelFunc
  47. }
  48. // NewServer creates a new subscription server instance with a cancellable context.
  49. func NewServer() *Server {
  50. ctx, cancel := context.WithCancel(context.Background())
  51. return &Server{
  52. ctx: ctx,
  53. cancel: cancel,
  54. }
  55. }
  56. // initRouter configures the subscription server's Gin engine, middleware,
  57. // templates and static assets and returns the ready-to-use engine.
  58. func (s *Server) initRouter() (*gin.Engine, error) {
  59. // Always run in release mode for the subscription server
  60. gin.DefaultWriter = io.Discard
  61. gin.DefaultErrorWriter = io.Discard
  62. gin.SetMode(gin.ReleaseMode)
  63. engine := gin.Default()
  64. subDomain, err := s.settingService.GetSubDomain()
  65. if err != nil {
  66. return nil, err
  67. }
  68. if subDomain != "" {
  69. engine.Use(middleware.DomainValidatorMiddleware(subDomain))
  70. }
  71. LinksPath, err := s.settingService.GetSubPath()
  72. if err != nil {
  73. return nil, err
  74. }
  75. JsonPath, err := s.settingService.GetSubJsonPath()
  76. if err != nil {
  77. return nil, err
  78. }
  79. ClashPath, err := s.settingService.GetSubClashPath()
  80. if err != nil {
  81. return nil, err
  82. }
  83. subJsonEnable, err := s.settingService.GetSubJsonEnable()
  84. if err != nil {
  85. return nil, err
  86. }
  87. subClashEnable, err := s.settingService.GetSubClashEnable()
  88. if err != nil {
  89. return nil, err
  90. }
  91. // Set base_path based on LinksPath for template rendering
  92. // Ensure LinksPath ends with "/" for proper asset URL generation
  93. basePath := LinksPath
  94. if basePath != "/" && !strings.HasSuffix(basePath, "/") {
  95. basePath += "/"
  96. }
  97. // logger.Debug("sub: Setting base_path to:", basePath)
  98. engine.Use(func(c *gin.Context) {
  99. c.Set("base_path", basePath)
  100. })
  101. Encrypt, err := s.settingService.GetSubEncrypt()
  102. if err != nil {
  103. return nil, err
  104. }
  105. ShowInfo, err := s.settingService.GetSubShowInfo()
  106. if err != nil {
  107. return nil, err
  108. }
  109. RemarkModel, err := s.settingService.GetRemarkModel()
  110. if err != nil {
  111. RemarkModel = "-ieo"
  112. }
  113. SubUpdates, err := s.settingService.GetSubUpdates()
  114. if err != nil {
  115. SubUpdates = "10"
  116. }
  117. SubJsonFragment, err := s.settingService.GetSubJsonFragment()
  118. if err != nil {
  119. SubJsonFragment = ""
  120. }
  121. SubJsonNoises, err := s.settingService.GetSubJsonNoises()
  122. if err != nil {
  123. SubJsonNoises = ""
  124. }
  125. SubJsonMux, err := s.settingService.GetSubJsonMux()
  126. if err != nil {
  127. SubJsonMux = ""
  128. }
  129. SubJsonRules, err := s.settingService.GetSubJsonRules()
  130. if err != nil {
  131. SubJsonRules = ""
  132. }
  133. SubTitle, err := s.settingService.GetSubTitle()
  134. if err != nil {
  135. SubTitle = ""
  136. }
  137. SubSupportUrl, err := s.settingService.GetSubSupportUrl()
  138. if err != nil {
  139. SubSupportUrl = ""
  140. }
  141. SubProfileUrl, err := s.settingService.GetSubProfileUrl()
  142. if err != nil {
  143. SubProfileUrl = ""
  144. }
  145. SubAnnounce, err := s.settingService.GetSubAnnounce()
  146. if err != nil {
  147. SubAnnounce = ""
  148. }
  149. SubEnableRouting, err := s.settingService.GetSubEnableRouting()
  150. if err != nil {
  151. return nil, err
  152. }
  153. SubRoutingRules, err := s.settingService.GetSubRoutingRules()
  154. if err != nil {
  155. SubRoutingRules = ""
  156. }
  157. // set per-request localizer from headers/cookies
  158. engine.Use(locale.LocalizerMiddleware())
  159. // register i18n function similar to web server
  160. i18nWebFunc := func(key string, params ...string) string {
  161. return locale.I18n(locale.Web, key, params...)
  162. }
  163. engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
  164. // Templates: prefer embedded; fallback to disk if necessary
  165. if err := setEmbeddedTemplates(engine); err != nil {
  166. logger.Warning("sub: failed to parse embedded templates:", err)
  167. if files, derr := s.getHtmlFiles(); derr == nil {
  168. engine.LoadHTMLFiles(files...)
  169. } else {
  170. logger.Error("sub: no templates available (embedded parse and disk load failed)", err, derr)
  171. }
  172. }
  173. // Assets: use disk if present, fallback to embedded
  174. // Serve under both root (/assets) and under the subscription path prefix (LinksPath + "assets")
  175. // so reverse proxies with a URI prefix can load assets correctly.
  176. // Determine LinksPath earlier to compute prefixed assets mount.
  177. // Note: LinksPath always starts and ends with "/" (validated in settings).
  178. var linksPathForAssets string
  179. if LinksPath == "/" {
  180. linksPathForAssets = "/assets"
  181. } else {
  182. // ensure single slash join
  183. linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
  184. }
  185. // Mount assets in multiple paths to handle different URL patterns
  186. var assetsFS http.FileSystem
  187. if _, err := os.Stat("web/assets"); err == nil {
  188. assetsFS = http.FS(os.DirFS("web/assets"))
  189. } else {
  190. if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
  191. assetsFS = http.FS(subFS)
  192. } else {
  193. logger.Error("sub: failed to mount embedded assets:", err)
  194. }
  195. }
  196. if assetsFS != nil {
  197. engine.StaticFS("/assets", assetsFS)
  198. if linksPathForAssets != "/assets" {
  199. engine.StaticFS(linksPathForAssets, assetsFS)
  200. }
  201. // Add middleware to handle dynamic asset paths with subid
  202. if LinksPath != "/" {
  203. engine.Use(func(c *gin.Context) {
  204. path := c.Request.URL.Path
  205. // Check if this is an asset request with subid pattern: /sub/path/{subid}/assets/...
  206. pathPrefix := strings.TrimRight(LinksPath, "/") + "/"
  207. if strings.HasPrefix(path, pathPrefix) && strings.Contains(path, "/assets/") {
  208. // Extract the asset path after /assets/
  209. assetsIndex := strings.Index(path, "/assets/")
  210. if assetsIndex != -1 {
  211. assetPath := path[assetsIndex+8:] // +8 to skip "/assets/"
  212. if assetPath != "" {
  213. // Serve the asset file
  214. c.FileFromFS(assetPath, assetsFS)
  215. c.Abort()
  216. return
  217. }
  218. }
  219. }
  220. c.Next()
  221. })
  222. }
  223. }
  224. g := engine.Group("/")
  225. s.sub = NewSUBController(
  226. g, LinksPath, JsonPath, ClashPath, subJsonEnable, subClashEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
  227. SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle, SubSupportUrl,
  228. SubProfileUrl, SubAnnounce, SubEnableRouting, SubRoutingRules)
  229. return engine, nil
  230. }
  231. // getHtmlFiles loads templates from local folder (used in debug mode)
  232. func (s *Server) getHtmlFiles() ([]string, error) {
  233. dir, _ := os.Getwd()
  234. files := []string{}
  235. // common layout
  236. common := filepath.Join(dir, "web", "html", "common", "page.html")
  237. if _, err := os.Stat(common); err == nil {
  238. files = append(files, common)
  239. }
  240. // components used
  241. theme := filepath.Join(dir, "web", "html", "component", "aThemeSwitch.html")
  242. if _, err := os.Stat(theme); err == nil {
  243. files = append(files, theme)
  244. }
  245. // page itself
  246. page := filepath.Join(dir, "web", "html", "subpage.html")
  247. if _, err := os.Stat(page); err == nil {
  248. files = append(files, page)
  249. } else {
  250. return nil, err
  251. }
  252. return files, nil
  253. }
  254. // Start initializes and starts the subscription server with configured settings.
  255. func (s *Server) Start() (err error) {
  256. // This is an anonymous function, no function name
  257. defer func() {
  258. if err != nil {
  259. s.Stop()
  260. }
  261. }()
  262. subEnable, err := s.settingService.GetSubEnable()
  263. if err != nil {
  264. return err
  265. }
  266. if !subEnable {
  267. return nil
  268. }
  269. engine, err := s.initRouter()
  270. if err != nil {
  271. return err
  272. }
  273. certFile, err := s.settingService.GetSubCertFile()
  274. if err != nil {
  275. return err
  276. }
  277. keyFile, err := s.settingService.GetSubKeyFile()
  278. if err != nil {
  279. return err
  280. }
  281. listen, err := s.settingService.GetSubListen()
  282. if err != nil {
  283. return err
  284. }
  285. port, err := s.settingService.GetSubPort()
  286. if err != nil {
  287. return err
  288. }
  289. listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
  290. listener, err := net.Listen("tcp", listenAddr)
  291. if err != nil {
  292. return err
  293. }
  294. if certFile != "" || keyFile != "" {
  295. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
  296. if err == nil {
  297. c := &tls.Config{
  298. Certificates: []tls.Certificate{cert},
  299. }
  300. listener = network.NewAutoHttpsListener(listener)
  301. listener = tls.NewListener(listener, c)
  302. logger.Info("Sub server running HTTPS on", listener.Addr())
  303. } else {
  304. logger.Error("Error loading certificates:", err)
  305. logger.Info("Sub server running HTTP on", listener.Addr())
  306. }
  307. } else {
  308. logger.Info("Sub server running HTTP on", listener.Addr())
  309. }
  310. s.listener = listener
  311. s.httpServer = &http.Server{
  312. Handler: engine,
  313. }
  314. go func() {
  315. s.httpServer.Serve(listener)
  316. }()
  317. return nil
  318. }
  319. // Stop gracefully shuts down the subscription server and closes the listener.
  320. func (s *Server) Stop() error {
  321. s.cancel()
  322. var err1 error
  323. var err2 error
  324. if s.httpServer != nil {
  325. err1 = s.httpServer.Shutdown(s.ctx)
  326. }
  327. if s.listener != nil {
  328. err2 = s.listener.Close()
  329. }
  330. return common.Combine(err1, err2)
  331. }
  332. // GetCtx returns the server's context for cancellation and deadline management.
  333. func (s *Server) GetCtx() context.Context {
  334. return s.ctx
  335. }