sub.go 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  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. // Determine if JSON subscription endpoint is enabled
  80. subJsonEnable, err := s.settingService.GetSubJsonEnable()
  81. if err != nil {
  82. return nil, err
  83. }
  84. // Set base_path based on LinksPath for template rendering
  85. engine.Use(func(c *gin.Context) {
  86. c.Set("base_path", LinksPath)
  87. })
  88. Encrypt, err := s.settingService.GetSubEncrypt()
  89. if err != nil {
  90. return nil, err
  91. }
  92. ShowInfo, err := s.settingService.GetSubShowInfo()
  93. if err != nil {
  94. return nil, err
  95. }
  96. RemarkModel, err := s.settingService.GetRemarkModel()
  97. if err != nil {
  98. RemarkModel = "-ieo"
  99. }
  100. SubUpdates, err := s.settingService.GetSubUpdates()
  101. if err != nil {
  102. SubUpdates = "10"
  103. }
  104. SubJsonFragment, err := s.settingService.GetSubJsonFragment()
  105. if err != nil {
  106. SubJsonFragment = ""
  107. }
  108. SubJsonNoises, err := s.settingService.GetSubJsonNoises()
  109. if err != nil {
  110. SubJsonNoises = ""
  111. }
  112. SubJsonMux, err := s.settingService.GetSubJsonMux()
  113. if err != nil {
  114. SubJsonMux = ""
  115. }
  116. SubJsonRules, err := s.settingService.GetSubJsonRules()
  117. if err != nil {
  118. SubJsonRules = ""
  119. }
  120. SubTitle, err := s.settingService.GetSubTitle()
  121. if err != nil {
  122. SubTitle = ""
  123. }
  124. // set per-request localizer from headers/cookies
  125. engine.Use(locale.LocalizerMiddleware())
  126. // register i18n function similar to web server
  127. i18nWebFunc := func(key string, params ...string) string {
  128. return locale.I18n(locale.Web, key, params...)
  129. }
  130. engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
  131. // Templates: prefer embedded; fallback to disk if necessary
  132. if err := setEmbeddedTemplates(engine); err != nil {
  133. logger.Warning("sub: failed to parse embedded templates:", err)
  134. if files, derr := s.getHtmlFiles(); derr == nil {
  135. engine.LoadHTMLFiles(files...)
  136. } else {
  137. logger.Error("sub: no templates available (embedded parse and disk load failed)", err, derr)
  138. }
  139. }
  140. // Assets: use disk if present, fallback to embedded
  141. // Serve under both root (/assets) and under the subscription path prefix (LinksPath + "assets")
  142. // so reverse proxies with a URI prefix can load assets correctly.
  143. // Determine LinksPath earlier to compute prefixed assets mount.
  144. // Note: LinksPath always starts and ends with "/" (validated in settings).
  145. var linksPathForAssets string
  146. if LinksPath == "/" {
  147. linksPathForAssets = "/assets"
  148. } else {
  149. // ensure single slash join
  150. linksPathForAssets = strings.TrimRight(LinksPath, "/") + "/assets"
  151. }
  152. if _, err := os.Stat("web/assets"); err == nil {
  153. engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
  154. if linksPathForAssets != "/assets" {
  155. engine.StaticFS(linksPathForAssets, http.FS(os.DirFS("web/assets")))
  156. }
  157. } else {
  158. if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
  159. engine.StaticFS("/assets", http.FS(subFS))
  160. if linksPathForAssets != "/assets" {
  161. engine.StaticFS(linksPathForAssets, http.FS(subFS))
  162. }
  163. } else {
  164. logger.Error("sub: failed to mount embedded assets:", err)
  165. }
  166. }
  167. g := engine.Group("/")
  168. s.sub = NewSUBController(
  169. g, LinksPath, JsonPath, subJsonEnable, Encrypt, ShowInfo, RemarkModel, SubUpdates,
  170. SubJsonFragment, SubJsonNoises, SubJsonMux, SubJsonRules, SubTitle)
  171. return engine, nil
  172. }
  173. // getHtmlFiles loads templates from local folder (used in debug mode)
  174. func (s *Server) getHtmlFiles() ([]string, error) {
  175. dir, _ := os.Getwd()
  176. files := []string{}
  177. // common layout
  178. common := filepath.Join(dir, "web", "html", "common", "page.html")
  179. if _, err := os.Stat(common); err == nil {
  180. files = append(files, common)
  181. }
  182. // components used
  183. theme := filepath.Join(dir, "web", "html", "component", "aThemeSwitch.html")
  184. if _, err := os.Stat(theme); err == nil {
  185. files = append(files, theme)
  186. }
  187. // page itself
  188. page := filepath.Join(dir, "web", "html", "subpage.html")
  189. if _, err := os.Stat(page); err == nil {
  190. files = append(files, page)
  191. } else {
  192. return nil, err
  193. }
  194. return files, nil
  195. }
  196. // Start initializes and starts the subscription server with configured settings.
  197. func (s *Server) Start() (err error) {
  198. // This is an anonymous function, no function name
  199. defer func() {
  200. if err != nil {
  201. s.Stop()
  202. }
  203. }()
  204. subEnable, err := s.settingService.GetSubEnable()
  205. if err != nil {
  206. return err
  207. }
  208. if !subEnable {
  209. return nil
  210. }
  211. engine, err := s.initRouter()
  212. if err != nil {
  213. return err
  214. }
  215. certFile, err := s.settingService.GetSubCertFile()
  216. if err != nil {
  217. return err
  218. }
  219. keyFile, err := s.settingService.GetSubKeyFile()
  220. if err != nil {
  221. return err
  222. }
  223. listen, err := s.settingService.GetSubListen()
  224. if err != nil {
  225. return err
  226. }
  227. port, err := s.settingService.GetSubPort()
  228. if err != nil {
  229. return err
  230. }
  231. listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
  232. listener, err := net.Listen("tcp", listenAddr)
  233. if err != nil {
  234. return err
  235. }
  236. if certFile != "" || keyFile != "" {
  237. cert, err := tls.LoadX509KeyPair(certFile, keyFile)
  238. if err == nil {
  239. c := &tls.Config{
  240. Certificates: []tls.Certificate{cert},
  241. }
  242. listener = network.NewAutoHttpsListener(listener)
  243. listener = tls.NewListener(listener, c)
  244. logger.Info("Sub server running HTTPS on", listener.Addr())
  245. } else {
  246. logger.Error("Error loading certificates:", err)
  247. logger.Info("Sub server running HTTP on", listener.Addr())
  248. }
  249. } else {
  250. logger.Info("Sub server running HTTP on", listener.Addr())
  251. }
  252. s.listener = listener
  253. s.httpServer = &http.Server{
  254. Handler: engine,
  255. }
  256. go func() {
  257. s.httpServer.Serve(listener)
  258. }()
  259. return nil
  260. }
  261. // Stop gracefully shuts down the subscription server and closes the listener.
  262. func (s *Server) Stop() error {
  263. s.cancel()
  264. var err1 error
  265. var err2 error
  266. if s.httpServer != nil {
  267. err1 = s.httpServer.Shutdown(s.ctx)
  268. }
  269. if s.listener != nil {
  270. err2 = s.listener.Close()
  271. }
  272. return common.Combine(err1, err2)
  273. }
  274. // GetCtx returns the server's context for cancellation and deadline management.
  275. func (s *Server) GetCtx() context.Context {
  276. return s.ctx
  277. }