1
0

xray_setting.go 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651
  1. package controller
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "strconv"
  7. "strings"
  8. "time"
  9. piaprotocol "github.com/mhsanaei/3x-ui/v3/internal/pia"
  10. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  11. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  12. "github.com/mhsanaei/3x-ui/v3/internal/web/service/integration"
  13. "github.com/mhsanaei/3x-ui/v3/internal/web/service/outbound"
  14. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  15. "github.com/gin-gonic/gin"
  16. )
  17. // XraySettingController handles Xray configuration and settings operations.
  18. type XraySettingController struct {
  19. XraySettingService service.XraySettingService
  20. SettingService service.SettingService
  21. InboundService service.InboundService
  22. OutboundService outbound.OutboundService
  23. XrayService service.XrayService
  24. WarpService integration.WarpService
  25. NordService integration.NordService
  26. PiaService integration.PiaService
  27. OutboundSubscriptionService service.OutboundSubscriptionService
  28. GeodataService service.GeodataService
  29. }
  30. // NewXraySettingController creates a new XraySettingController and initializes its routes.
  31. func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
  32. a := &XraySettingController{PiaService: *integration.NewPiaService()}
  33. a.initRouter(g)
  34. return a
  35. }
  36. // initRouter sets up the routes for Xray settings management.
  37. func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
  38. g = g.Group("/xray")
  39. g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
  40. g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
  41. g.GET("/getXrayResult", a.getXrayResult)
  42. g.POST("/", a.getXraySetting)
  43. g.POST("/warp/:action", a.warp)
  44. g.POST("/nord/:action", a.nord)
  45. g.POST("/pia/:action", a.pia)
  46. g.POST("/update", a.updateSetting)
  47. g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
  48. g.POST("/testOutbound", a.testOutbound)
  49. g.POST("/testOutbounds", a.testOutbounds)
  50. g.POST("/balancerStatus", a.balancerStatus)
  51. g.POST("/balancerOverride", a.balancerOverride)
  52. g.POST("/routeTest", a.routeTest)
  53. g.GET("/geodata/files", a.geodataFiles)
  54. g.GET("/geodata/categories", a.geodataCategories)
  55. g.GET("/geodata/entries", a.geodataEntries)
  56. g.POST("/geodata/validate", a.geodataValidate)
  57. // Outbound subscription (remote outbound lists)
  58. g.GET("/outbound-subs", a.listOutboundSubs)
  59. g.POST("/outbound-subs", a.createOutboundSub)
  60. g.POST("/outbound-subs/:id/refresh", a.refreshOutboundSub)
  61. g.POST("/outbound-subs/:id/move", a.moveOutboundSub)
  62. g.POST("/outbound-subs/:id", a.updateOutboundSub)
  63. g.DELETE("/outbound-subs/:id", a.deleteOutboundSub)
  64. g.POST("/outbound-subs/:id/del", a.deleteOutboundSub) // POST alias for clients that can't send DELETE
  65. g.POST("/outbound-subs/parse", a.parseOutboundSubURL) // preview without saving
  66. }
  67. // getXraySetting retrieves the Xray configuration template, inbound tags, and outbound test URL.
  68. func (a *XraySettingController) getXraySetting(c *gin.Context) {
  69. xraySetting, err := a.SettingService.GetXrayConfigTemplate()
  70. if err != nil {
  71. jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
  72. return
  73. }
  74. // Older versions of this handler embedded the raw DB value as
  75. // `xraySetting` in the response without checking if the value
  76. // already had that wrapper shape. When the frontend saved it
  77. // back through the textarea verbatim, the wrapper got persisted
  78. // and every subsequent save nested another layer, which is what
  79. // eventually produced the blank Xray Settings page in #4059.
  80. // Strip any such wrapper here, and heal the DB if we found one so
  81. // the next read is O(1) instead of climbing the same pile again.
  82. if unwrapped := service.UnwrapXrayTemplateConfig(xraySetting); unwrapped != xraySetting {
  83. if saveErr := a.XraySettingService.SaveXraySetting(unwrapped); saveErr == nil {
  84. xraySetting = unwrapped
  85. } else {
  86. // Don't fail the read — just serve the unwrapped value
  87. // and leave the DB healing for a later save.
  88. xraySetting = unwrapped
  89. }
  90. }
  91. inboundTags, err := a.InboundService.GetInboundTags()
  92. if err != nil {
  93. jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
  94. return
  95. }
  96. clientReverseTags, err := a.InboundService.GetClientReverseTags()
  97. if err != nil {
  98. clientReverseTags = "[]"
  99. }
  100. outboundTestUrl, _ := a.SettingService.GetXrayOutboundTestUrl()
  101. if outboundTestUrl == "" {
  102. outboundTestUrl = "https://www.google.com/generate_204"
  103. }
  104. xrayResponse := map[string]any{
  105. "xraySetting": json.RawMessage(xraySetting),
  106. "inboundTags": json.RawMessage(inboundTags),
  107. "clientReverseTags": json.RawMessage(clientReverseTags),
  108. "outboundTestUrl": outboundTestUrl,
  109. }
  110. // Surface subscription outbounds (and their tags) so the frontend can:
  111. // - show them as read-only items in the Outbounds tab
  112. // - let users pick them in balancers and routing rules
  113. // These are not part of the editable template; they are injected at runtime.
  114. if subObs, err := a.OutboundSubscriptionService.AllActiveOutbounds(); err == nil && len(subObs) > 0 {
  115. xrayResponse["subscriptionOutbounds"] = subObs
  116. }
  117. if subTags, err := a.OutboundSubscriptionService.AllActiveOutboundTags(); err == nil && len(subTags) > 0 {
  118. xrayResponse["subscriptionOutboundTags"] = subTags
  119. }
  120. result, err := json.Marshal(xrayResponse)
  121. if err != nil {
  122. jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
  123. return
  124. }
  125. jsonObj(c, string(result), nil)
  126. }
  127. // updateSetting updates the Xray configuration settings and applies them to
  128. // the running core right away — through the gRPC API when only inbounds,
  129. // outbounds or routing rules changed, with a process restart otherwise.
  130. func (a *XraySettingController) updateSetting(c *gin.Context) {
  131. xraySetting := c.PostForm("xraySetting")
  132. if err := a.XraySettingService.SaveXraySetting(xraySetting); err != nil {
  133. jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
  134. return
  135. }
  136. outboundTestUrl := c.PostForm("outboundTestUrl")
  137. if outboundTestUrl == "" {
  138. outboundTestUrl = "https://www.google.com/generate_204"
  139. }
  140. if err := a.SettingService.SetXrayOutboundTestUrl(outboundTestUrl); err != nil {
  141. jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
  142. return
  143. }
  144. // Only reconcile a running core; a manually stopped xray stays stopped.
  145. if a.XrayService.IsXrayRunning() {
  146. if err := a.XrayService.RestartXray(false); err != nil {
  147. jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
  148. return
  149. }
  150. }
  151. jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)
  152. }
  153. // getDefaultXrayConfig retrieves the default Xray configuration.
  154. func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
  155. defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
  156. if err != nil {
  157. jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
  158. return
  159. }
  160. jsonObj(c, defaultJsonConfig, nil)
  161. }
  162. // getXrayResult retrieves the current Xray service result.
  163. func (a *XraySettingController) getXrayResult(c *gin.Context) {
  164. jsonObj(c, a.XrayService.GetXrayResult(), nil)
  165. }
  166. // warp handles Warp-related operations based on the action parameter.
  167. func (a *XraySettingController) warp(c *gin.Context) {
  168. action := c.Param("action")
  169. var resp string
  170. var err error
  171. switch action {
  172. case "data":
  173. resp, err = a.WarpService.GetWarpData()
  174. case "del":
  175. err = a.WarpService.DelWarpData()
  176. case "config":
  177. resp, err = a.WarpService.GetWarpConfig()
  178. case "reg":
  179. skey := c.PostForm("privateKey")
  180. pkey := c.PostForm("publicKey")
  181. resp, err = a.WarpService.RegWarp(skey, pkey)
  182. case "changeIp":
  183. resp, err = a.WarpService.ChangeWarpIP()
  184. if err == nil {
  185. a.XrayService.SetToNeedRestart()
  186. // Restart the auto-update clock so a scheduled rotation
  187. // doesn't fire right after this manual one.
  188. err = a.SettingService.SetWarpLastUpdate(time.Now().Unix())
  189. }
  190. case "license":
  191. license := c.PostForm("license")
  192. resp, err = a.WarpService.SetWarpLicense(license)
  193. case "interval":
  194. interval, convErr := strconv.Atoi(c.PostForm("interval"))
  195. if convErr != nil || interval < 0 {
  196. err = common.NewError("invalid warp update interval")
  197. } else if err = a.SettingService.SetWarpUpdateInterval(interval); err == nil && interval > 0 {
  198. // Count the interval from now rather than from epoch 0,
  199. // otherwise the job would rotate on its next tick.
  200. err = a.SettingService.SetWarpLastUpdate(time.Now().Unix())
  201. }
  202. }
  203. jsonObj(c, resp, err)
  204. }
  205. // nord handles NordVPN-related operations based on the action parameter.
  206. func (a *XraySettingController) nord(c *gin.Context) {
  207. action := c.Param("action")
  208. var resp string
  209. var err error
  210. switch action {
  211. case "countries":
  212. resp, err = a.NordService.GetCountries()
  213. case "servers":
  214. countryId := c.PostForm("countryId")
  215. resp, err = a.NordService.GetServers(countryId)
  216. case "reg":
  217. token := c.PostForm("token")
  218. resp, err = a.NordService.GetCredentials(token)
  219. case "setKey":
  220. key := c.PostForm("key")
  221. resp, err = a.NordService.SetKey(key)
  222. case "data":
  223. resp, err = a.NordService.GetNordData()
  224. case "del":
  225. err = a.NordService.DelNordData()
  226. }
  227. jsonObj(c, resp, err)
  228. }
  229. func (a *XraySettingController) pia(c *gin.Context) {
  230. action := c.Param("action")
  231. var resp any
  232. var err error
  233. switch action {
  234. case "countries":
  235. resp, err = a.PiaService.GetCountries()
  236. case "servers":
  237. resp, err = a.PiaService.GetServers(c.PostForm("countryCode"))
  238. case "reg":
  239. resp, err = a.PiaService.Login(c.PostForm("username"), c.PostForm("password"))
  240. case "data":
  241. resp, err = a.PiaService.GetPiaData()
  242. case "del":
  243. err = a.PiaService.DelPiaData()
  244. case "addKey":
  245. resp, err = a.PiaService.AddKey(c.PostForm("hostname"))
  246. default:
  247. jsonMsg(c, "unknown action", common.NewError("unknown action"))
  248. return
  249. }
  250. if err != nil {
  251. var pe *piaprotocol.Error
  252. if errors.As(err, &pe) && pe != nil {
  253. jsonObj(c, nil, common.NewError(pe.Message))
  254. return
  255. }
  256. jsonObj(c, nil, err)
  257. return
  258. }
  259. jsonObj(c, resp, nil)
  260. }
  261. // getOutboundsTraffic retrieves the traffic statistics for outbounds.
  262. func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
  263. outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
  264. if err != nil {
  265. jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getOutboundTrafficError"), err)
  266. return
  267. }
  268. jsonObj(c, outboundsTraffic, nil)
  269. }
  270. // resetOutboundsTraffic resets the traffic statistics for the specified outbound tag.
  271. func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
  272. tag := c.PostForm("tag")
  273. err := a.OutboundService.ResetOutboundTraffic(tag)
  274. if err != nil {
  275. jsonMsg(c, I18nWeb(c, "pages.settings.toasts.resetOutboundTrafficError"), err)
  276. return
  277. }
  278. jsonObj(c, "", nil)
  279. }
  280. // testOutbound tests an outbound configuration and returns the delay/response time.
  281. // Optional form "allOutbounds": JSON array of all outbounds; used to resolve sockopt.dialerProxy dependencies.
  282. // Optional form "mode": "tcp" for a fast dial-only probe, "real" for the cold
  283. // full-request delay, anything else (default) for a full HTTP probe through a temp xray instance.
  284. func (a *XraySettingController) testOutbound(c *gin.Context) {
  285. outboundJSON := c.PostForm("outbound")
  286. allOutboundsJSON := c.PostForm("allOutbounds")
  287. mode := c.PostForm("mode")
  288. if outboundJSON == "" {
  289. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("outbound parameter is required"))
  290. return
  291. }
  292. // Load the test URL from server settings to prevent SSRF via user-controlled URLs
  293. testURL, _ := a.SettingService.GetXrayOutboundTestUrl()
  294. testURL, err := service.SanitizePublicHTTPURL(testURL, false)
  295. if err != nil {
  296. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  297. return
  298. }
  299. result, err := a.OutboundService.TestOutbound(outboundJSON, testURL, allOutboundsJSON, mode)
  300. if err != nil {
  301. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  302. return
  303. }
  304. jsonObj(c, result, nil)
  305. }
  306. // testOutbounds tests a batch of outbound configurations through one shared
  307. // temp xray instance and returns an array of results in input order.
  308. // Form "outbounds": JSON array of outbound configs (required).
  309. // Optional form "allOutbounds": JSON array of all outbounds; used to resolve sockopt.dialerProxy dependencies.
  310. // Optional form "mode": "tcp" for fast dial-only probes, "real" for the cold
  311. // full-request delay, anything else (default) for real HTTP requests routed through each outbound.
  312. func (a *XraySettingController) testOutbounds(c *gin.Context) {
  313. outboundsJSON := c.PostForm("outbounds")
  314. allOutboundsJSON := c.PostForm("allOutbounds")
  315. mode := c.PostForm("mode")
  316. if outboundsJSON == "" {
  317. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("outbounds parameter is required"))
  318. return
  319. }
  320. // Load the test URL from server settings to prevent SSRF via user-controlled URLs
  321. testURL, _ := a.SettingService.GetXrayOutboundTestUrl()
  322. testURL, err := service.SanitizePublicHTTPURL(testURL, false)
  323. if err != nil {
  324. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  325. return
  326. }
  327. results, err := a.OutboundService.TestOutbounds(outboundsJSON, testURL, allOutboundsJSON, mode)
  328. if err != nil {
  329. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  330. return
  331. }
  332. jsonObj(c, results, nil)
  333. }
  334. // balancerStatus reports the live state (override + strategy picks) of the
  335. // balancer tags given as a comma-separated "tags" form field.
  336. func (a *XraySettingController) balancerStatus(c *gin.Context) {
  337. raw := c.PostForm("tags")
  338. var tags []string
  339. for tag := range strings.SplitSeq(raw, ",") {
  340. if tag = strings.TrimSpace(tag); tag != "" {
  341. tags = append(tags, tag)
  342. }
  343. }
  344. statuses, err := a.XrayService.GetBalancersStatus(tags)
  345. if err != nil {
  346. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  347. return
  348. }
  349. byTag := make(map[string]service.BalancerStatus, len(statuses))
  350. for _, status := range statuses {
  351. byTag[status.Tag] = status
  352. }
  353. jsonObj(c, byTag, nil)
  354. }
  355. // balancerOverride forces a balancer to a specific outbound tag; an empty
  356. // "target" clears the override.
  357. func (a *XraySettingController) balancerOverride(c *gin.Context) {
  358. tag := c.PostForm("tag")
  359. if tag == "" {
  360. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("tag is required"))
  361. return
  362. }
  363. target := c.PostForm("target")
  364. if err := a.XrayService.OverrideBalancer(tag, target); err != nil {
  365. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  366. return
  367. }
  368. jsonObj(c, "", nil)
  369. }
  370. // routeTest asks the running core which outbound it would route a synthetic
  371. // connection to.
  372. func (a *XraySettingController) routeTest(c *gin.Context) {
  373. port := 0
  374. if portStr := c.PostForm("port"); portStr != "" {
  375. parsed, err := strconv.Atoi(portStr)
  376. if err != nil || parsed < 0 || parsed > 65535 {
  377. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("invalid port"))
  378. return
  379. }
  380. port = parsed
  381. }
  382. req := xray.RouteTestRequest{
  383. InboundTag: c.PostForm("inboundTag"),
  384. Domain: c.PostForm("domain"),
  385. IP: c.PostForm("ip"),
  386. Port: port,
  387. Network: c.PostForm("network"),
  388. Protocol: c.PostForm("protocol"),
  389. Email: c.PostForm("email"),
  390. }
  391. if req.Domain == "" && req.IP == "" {
  392. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("domain or ip is required"))
  393. return
  394. }
  395. result, err := a.XrayService.TestRoute(req)
  396. if err != nil {
  397. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  398. return
  399. }
  400. jsonObj(c, result, nil)
  401. }
  402. // maxGeodataTokens bounds one validation request; a routing rule listing more
  403. // categories than this is not something the panel needs to answer for.
  404. const maxGeodataTokens = 500
  405. // geodataFiles lists the geo databases Xray resolves geosite:/geoip: tokens
  406. // against, including ones that failed to parse.
  407. func (a *XraySettingController) geodataFiles(c *gin.Context) {
  408. files, err := a.GeodataService.Files()
  409. if err != nil {
  410. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  411. return
  412. }
  413. jsonObj(c, files, nil)
  414. }
  415. // geodataCategories returns one page of a database's categories.
  416. func (a *XraySettingController) geodataCategories(c *gin.Context) {
  417. offset, limit := geodataPaging(c)
  418. page, err := a.GeodataService.Categories(c.Query("file"), c.Query("q"), offset, limit)
  419. if err != nil {
  420. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  421. return
  422. }
  423. jsonObj(c, page, nil)
  424. }
  425. // geodataEntries returns one page of the domains or CIDRs inside a category.
  426. func (a *XraySettingController) geodataEntries(c *gin.Context) {
  427. code := c.Query("code")
  428. if code == "" {
  429. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("code is required"))
  430. return
  431. }
  432. offset, limit := geodataPaging(c)
  433. page, err := a.GeodataService.Entries(c.Query("file"), code, c.Query("q"), offset, limit)
  434. if err != nil {
  435. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
  436. return
  437. }
  438. jsonObj(c, page, nil)
  439. }
  440. // geodataValidate reports which routing tokens do not resolve against the
  441. // databases on disk.
  442. func (a *XraySettingController) geodataValidate(c *gin.Context) {
  443. // Split with a bound rather than splitting first: a 10 MB body of commas
  444. // would otherwise allocate millions of strings before the limit is checked.
  445. tokens := strings.SplitN(c.PostForm("tokens"), ",", maxGeodataTokens+1)
  446. if len(tokens) > maxGeodataTokens {
  447. jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewErrorf("too many tokens: over %d", maxGeodataTokens))
  448. return
  449. }
  450. jsonObj(c, a.GeodataService.Validate(c.PostForm("kind") == "ip", tokens), nil)
  451. }
  452. func geodataPaging(c *gin.Context) (int, int) {
  453. offset, err := strconv.Atoi(c.Query("offset"))
  454. if err != nil {
  455. offset = 0
  456. }
  457. limit, err := strconv.Atoi(c.Query("limit"))
  458. if err != nil {
  459. limit = 0
  460. }
  461. return offset, limit
  462. }
  463. // --- Outbound Subscription handlers ---
  464. func (a *XraySettingController) listOutboundSubs(c *gin.Context) {
  465. list, err := a.OutboundSubscriptionService.List()
  466. if err != nil {
  467. jsonMsg(c, "Failed to list outbound subscriptions", err)
  468. return
  469. }
  470. jsonObj(c, list, nil)
  471. }
  472. func (a *XraySettingController) createOutboundSub(c *gin.Context) {
  473. remark := c.PostForm("remark")
  474. rawURL := c.PostForm("url")
  475. prefix := c.PostForm("tagPrefix")
  476. enabled := c.PostForm("enabled") != "false"
  477. allowPrivate := c.PostForm("allowPrivate") == "true"
  478. allowInsecure := c.PostForm("allowInsecure") == "true"
  479. prepend := c.PostForm("prepend") == "true"
  480. intervalStr := c.PostForm("updateInterval")
  481. interval := 600
  482. if intervalStr != "" {
  483. if v, err := parseIntSafe(intervalStr); err == nil && v > 0 {
  484. interval = v
  485. }
  486. }
  487. sub, err := a.OutboundSubscriptionService.Create(remark, rawURL, prefix, enabled, interval, allowPrivate, prepend, allowInsecure)
  488. if err != nil {
  489. jsonMsg(c, "Failed to create outbound subscription", err)
  490. return
  491. }
  492. jsonObj(c, sub, nil)
  493. }
  494. func (a *XraySettingController) updateOutboundSub(c *gin.Context) {
  495. id := c.Param("id")
  496. var subID int
  497. if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
  498. jsonMsg(c, "Invalid id", err)
  499. return
  500. }
  501. remark := c.PostForm("remark")
  502. rawURL := c.PostForm("url")
  503. prefix := c.PostForm("tagPrefix")
  504. enabled := c.PostForm("enabled") != "false"
  505. allowPrivate := c.PostForm("allowPrivate") == "true"
  506. allowInsecure := c.PostForm("allowInsecure") == "true"
  507. prepend := c.PostForm("prepend") == "true"
  508. intervalStr := c.PostForm("updateInterval")
  509. interval := 600
  510. if intervalStr != "" {
  511. if v, err := parseIntSafe(intervalStr); err == nil && v > 0 {
  512. interval = v
  513. }
  514. }
  515. if err := a.OutboundSubscriptionService.Update(subID, remark, rawURL, prefix, enabled, interval, allowPrivate, prepend, allowInsecure); err != nil {
  516. jsonMsg(c, "Failed to update outbound subscription", err)
  517. return
  518. }
  519. jsonObj(c, "", nil)
  520. }
  521. func (a *XraySettingController) deleteOutboundSub(c *gin.Context) {
  522. id := c.Param("id")
  523. var subID int
  524. if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
  525. jsonMsg(c, "Invalid id", err)
  526. return
  527. }
  528. if err := a.OutboundSubscriptionService.Delete(subID); err != nil {
  529. jsonMsg(c, "Failed to delete outbound subscription", err)
  530. return
  531. }
  532. // Signal that xray should drop this subscription's outbounds on next reload.
  533. a.XrayService.SetToNeedRestart()
  534. jsonObj(c, "", nil)
  535. }
  536. func (a *XraySettingController) refreshOutboundSub(c *gin.Context) {
  537. id := c.Param("id")
  538. var subID int
  539. if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
  540. jsonMsg(c, "Invalid id", err)
  541. return
  542. }
  543. obs, err := a.OutboundSubscriptionService.Refresh(subID)
  544. if err != nil {
  545. jsonMsg(c, "Refresh failed", err)
  546. return
  547. }
  548. // Signal that xray should pick up the new outbounds on next restart/reload
  549. a.XrayService.SetToNeedRestart()
  550. jsonObj(c, obs, nil)
  551. }
  552. func (a *XraySettingController) moveOutboundSub(c *gin.Context) {
  553. id := c.Param("id")
  554. var subID int
  555. if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
  556. jsonMsg(c, "Invalid id", err)
  557. return
  558. }
  559. up := c.PostForm("dir") == "up"
  560. if err := a.OutboundSubscriptionService.Move(subID, up); err != nil {
  561. jsonMsg(c, "Failed to reorder outbound subscription", err)
  562. return
  563. }
  564. // Order affects the merged outbounds, so xray needs a reload.
  565. a.XrayService.SetToNeedRestart()
  566. jsonObj(c, "", nil)
  567. }
  568. // parseOutboundSubURL is a preview endpoint: it fetches + parses the provided
  569. // URL but does not persist anything. Useful for the "add subscription" flow
  570. // so the user can see the resulting outbounds (and assigned tags) before saving.
  571. func (a *XraySettingController) parseOutboundSubURL(c *gin.Context) {
  572. rawURL := c.PostForm("url")
  573. if rawURL == "" {
  574. jsonMsg(c, "url is required", common.NewError("missing url"))
  575. return
  576. }
  577. allowPrivate := c.PostForm("allowPrivate") == "true"
  578. allowInsecure := c.PostForm("allowInsecure") == "true"
  579. // Use a throw-away service instance; it only needs the settingService for proxy.
  580. svc := service.OutboundSubscriptionService{}
  581. // We don't have a direct "fetch once" that returns without storing, so we
  582. // temporarily create a disabled row, refresh it, then delete. Cleaner would
  583. // be to expose a pure ParseURL on the service, but this keeps the surface small.
  584. tmp, err := svc.Create("preview", rawURL, "", false, 600, allowPrivate, false, allowInsecure)
  585. if err != nil {
  586. jsonMsg(c, "Failed to preview subscription", err)
  587. return
  588. }
  589. obs, err := svc.Refresh(tmp.Id)
  590. // best-effort cleanup
  591. _ = svc.Delete(tmp.Id)
  592. if err != nil {
  593. jsonMsg(c, "Failed to fetch/parse subscription", err)
  594. return
  595. }
  596. jsonObj(c, obs, nil)
  597. }
  598. func parseIntSafe(s string) (int, error) {
  599. var v int
  600. _, err := fmt.Sscanf(s, "%d", &v)
  601. return v, err
  602. }