| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417 |
- package controller
- import (
- "encoding/json"
- "fmt"
- "strconv"
- "time"
- "github.com/mhsanaei/3x-ui/v3/util/common"
- "github.com/mhsanaei/3x-ui/v3/web/service"
- "github.com/gin-gonic/gin"
- )
- // XraySettingController handles Xray configuration and settings operations.
- type XraySettingController struct {
- XraySettingService service.XraySettingService
- SettingService service.SettingService
- InboundService service.InboundService
- OutboundService service.OutboundService
- XrayService service.XrayService
- WarpService service.WarpService
- NordService service.NordService
- OutboundSubscriptionService service.OutboundSubscriptionService
- }
- // NewXraySettingController creates a new XraySettingController and initializes its routes.
- func NewXraySettingController(g *gin.RouterGroup) *XraySettingController {
- a := &XraySettingController{}
- a.initRouter(g)
- return a
- }
- // initRouter sets up the routes for Xray settings management.
- func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
- g = g.Group("/xray")
- g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
- g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
- g.GET("/getXrayResult", a.getXrayResult)
- g.POST("/", a.getXraySetting)
- g.POST("/warp/:action", a.warp)
- g.POST("/nord/:action", a.nord)
- g.POST("/update", a.updateSetting)
- g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
- g.POST("/testOutbound", a.testOutbound)
- // Outbound subscription (remote outbound lists)
- g.GET("/outbound-subs", a.listOutboundSubs)
- g.POST("/outbound-subs", a.createOutboundSub)
- g.POST("/outbound-subs/:id/refresh", a.refreshOutboundSub)
- g.POST("/outbound-subs/:id/move", a.moveOutboundSub)
- g.POST("/outbound-subs/:id", a.updateOutboundSub)
- g.DELETE("/outbound-subs/:id", a.deleteOutboundSub)
- g.POST("/outbound-subs/:id/del", a.deleteOutboundSub) // axios-friendly alias
- g.POST("/outbound-subs/parse", a.parseOutboundSubURL) // preview without saving
- }
- // getXraySetting retrieves the Xray configuration template, inbound tags, and outbound test URL.
- func (a *XraySettingController) getXraySetting(c *gin.Context) {
- xraySetting, err := a.SettingService.GetXrayConfigTemplate()
- if err != nil {
- jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
- return
- }
- // Older versions of this handler embedded the raw DB value as
- // `xraySetting` in the response without checking if the value
- // already had that wrapper shape. When the frontend saved it
- // back through the textarea verbatim, the wrapper got persisted
- // and every subsequent save nested another layer, which is what
- // eventually produced the blank Xray Settings page in #4059.
- // Strip any such wrapper here, and heal the DB if we found one so
- // the next read is O(1) instead of climbing the same pile again.
- if unwrapped := service.UnwrapXrayTemplateConfig(xraySetting); unwrapped != xraySetting {
- if saveErr := a.XraySettingService.SaveXraySetting(unwrapped); saveErr == nil {
- xraySetting = unwrapped
- } else {
- // Don't fail the read — just serve the unwrapped value
- // and leave the DB healing for a later save.
- xraySetting = unwrapped
- }
- }
- inboundTags, err := a.InboundService.GetInboundTags()
- if err != nil {
- jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
- return
- }
- clientReverseTags, err := a.InboundService.GetClientReverseTags()
- if err != nil {
- clientReverseTags = "[]"
- }
- outboundTestUrl, _ := a.SettingService.GetXrayOutboundTestUrl()
- if outboundTestUrl == "" {
- outboundTestUrl = "https://www.google.com/generate_204"
- }
- xrayResponse := map[string]any{
- "xraySetting": json.RawMessage(xraySetting),
- "inboundTags": json.RawMessage(inboundTags),
- "clientReverseTags": json.RawMessage(clientReverseTags),
- "outboundTestUrl": outboundTestUrl,
- }
- // Surface subscription outbounds (and their tags) so the frontend can:
- // - show them as read-only items in the Outbounds tab
- // - let users pick them in balancers and routing rules
- // These are not part of the editable template; they are injected at runtime.
- if subObs, err := a.OutboundSubscriptionService.AllActiveOutbounds(); err == nil && len(subObs) > 0 {
- xrayResponse["subscriptionOutbounds"] = subObs
- }
- if subTags, err := a.OutboundSubscriptionService.AllActiveOutboundTags(); err == nil && len(subTags) > 0 {
- xrayResponse["subscriptionOutboundTags"] = subTags
- }
- result, err := json.Marshal(xrayResponse)
- if err != nil {
- jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
- return
- }
- jsonObj(c, string(result), nil)
- }
- // updateSetting updates the Xray configuration settings.
- func (a *XraySettingController) updateSetting(c *gin.Context) {
- xraySetting := c.PostForm("xraySetting")
- if err := a.XraySettingService.SaveXraySetting(xraySetting); err != nil {
- jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
- return
- }
- outboundTestUrl := c.PostForm("outboundTestUrl")
- if outboundTestUrl == "" {
- outboundTestUrl = "https://www.google.com/generate_204"
- }
- if err := a.SettingService.SetXrayOutboundTestUrl(outboundTestUrl); err != nil {
- jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err)
- return
- }
- jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), nil)
- }
- // getDefaultXrayConfig retrieves the default Xray configuration.
- func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) {
- defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig()
- if err != nil {
- jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
- return
- }
- jsonObj(c, defaultJsonConfig, nil)
- }
- // getXrayResult retrieves the current Xray service result.
- func (a *XraySettingController) getXrayResult(c *gin.Context) {
- jsonObj(c, a.XrayService.GetXrayResult(), nil)
- }
- // warp handles Warp-related operations based on the action parameter.
- func (a *XraySettingController) warp(c *gin.Context) {
- action := c.Param("action")
- var resp string
- var err error
- switch action {
- case "data":
- resp, err = a.WarpService.GetWarpData()
- case "del":
- err = a.WarpService.DelWarpData()
- case "config":
- resp, err = a.WarpService.GetWarpConfig()
- case "reg":
- skey := c.PostForm("privateKey")
- pkey := c.PostForm("publicKey")
- resp, err = a.WarpService.RegWarp(skey, pkey)
- case "changeIp":
- resp, err = a.WarpService.ChangeWarpIP()
- if err == nil {
- a.XrayService.SetToNeedRestart()
- // Restart the auto-update clock so a scheduled rotation
- // doesn't fire right after this manual one.
- _ = a.SettingService.SetWarpLastUpdate(time.Now().Unix())
- }
- case "license":
- license := c.PostForm("license")
- resp, err = a.WarpService.SetWarpLicense(license)
- case "interval":
- interval, convErr := strconv.Atoi(c.PostForm("interval"))
- if convErr != nil || interval < 0 {
- err = common.NewError("invalid warp update interval")
- } else if err = a.SettingService.SetWarpUpdateInterval(interval); err == nil && interval > 0 {
- // Count the interval from now rather than from epoch 0,
- // otherwise the job would rotate on its next tick.
- _ = a.SettingService.SetWarpLastUpdate(time.Now().Unix())
- }
- }
- jsonObj(c, resp, err)
- }
- // nord handles NordVPN-related operations based on the action parameter.
- func (a *XraySettingController) nord(c *gin.Context) {
- action := c.Param("action")
- var resp string
- var err error
- switch action {
- case "countries":
- resp, err = a.NordService.GetCountries()
- case "servers":
- countryId := c.PostForm("countryId")
- resp, err = a.NordService.GetServers(countryId)
- case "reg":
- token := c.PostForm("token")
- resp, err = a.NordService.GetCredentials(token)
- case "setKey":
- key := c.PostForm("key")
- resp, err = a.NordService.SetKey(key)
- case "data":
- resp, err = a.NordService.GetNordData()
- case "del":
- err = a.NordService.DelNordData()
- }
- jsonObj(c, resp, err)
- }
- // getOutboundsTraffic retrieves the traffic statistics for outbounds.
- func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
- outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
- if err != nil {
- jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getOutboundTrafficError"), err)
- return
- }
- jsonObj(c, outboundsTraffic, nil)
- }
- // resetOutboundsTraffic resets the traffic statistics for the specified outbound tag.
- func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
- tag := c.PostForm("tag")
- err := a.OutboundService.ResetOutboundTraffic(tag)
- if err != nil {
- jsonMsg(c, I18nWeb(c, "pages.settings.toasts.resetOutboundTrafficError"), err)
- return
- }
- jsonObj(c, "", nil)
- }
- // testOutbound tests an outbound configuration and returns the delay/response time.
- // Optional form "allOutbounds": JSON array of all outbounds; used to resolve sockopt.dialerProxy dependencies.
- // Optional form "mode": "tcp" for a fast dial-only probe (parallel-safe),
- // anything else (default) for a full HTTP probe through a temp xray instance.
- func (a *XraySettingController) testOutbound(c *gin.Context) {
- outboundJSON := c.PostForm("outbound")
- allOutboundsJSON := c.PostForm("allOutbounds")
- mode := c.PostForm("mode")
- if outboundJSON == "" {
- jsonMsg(c, I18nWeb(c, "somethingWentWrong"), common.NewError("outbound parameter is required"))
- return
- }
- // Load the test URL from server settings to prevent SSRF via user-controlled URLs
- testURL, _ := a.SettingService.GetXrayOutboundTestUrl()
- testURL, err := service.SanitizePublicHTTPURL(testURL, false)
- if err != nil {
- jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
- return
- }
- result, err := a.OutboundService.TestOutbound(outboundJSON, testURL, allOutboundsJSON, mode)
- if err != nil {
- jsonMsg(c, I18nWeb(c, "somethingWentWrong"), err)
- return
- }
- jsonObj(c, result, nil)
- }
- // --- Outbound Subscription handlers ---
- func (a *XraySettingController) listOutboundSubs(c *gin.Context) {
- list, err := a.OutboundSubscriptionService.List()
- if err != nil {
- jsonMsg(c, "Failed to list outbound subscriptions", err)
- return
- }
- jsonObj(c, list, nil)
- }
- func (a *XraySettingController) createOutboundSub(c *gin.Context) {
- remark := c.PostForm("remark")
- rawURL := c.PostForm("url")
- prefix := c.PostForm("tagPrefix")
- enabled := c.PostForm("enabled") != "false"
- allowPrivate := c.PostForm("allowPrivate") == "true"
- prepend := c.PostForm("prepend") == "true"
- intervalStr := c.PostForm("updateInterval")
- interval := 600
- if intervalStr != "" {
- if v, err := parseIntSafe(intervalStr); err == nil && v > 0 {
- interval = v
- }
- }
- sub, err := a.OutboundSubscriptionService.Create(remark, rawURL, prefix, enabled, interval, allowPrivate, prepend)
- if err != nil {
- jsonMsg(c, "Failed to create outbound subscription", err)
- return
- }
- jsonObj(c, sub, nil)
- }
- func (a *XraySettingController) updateOutboundSub(c *gin.Context) {
- id := c.Param("id")
- var subID int
- if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
- jsonMsg(c, "Invalid id", err)
- return
- }
- remark := c.PostForm("remark")
- rawURL := c.PostForm("url")
- prefix := c.PostForm("tagPrefix")
- enabled := c.PostForm("enabled") != "false"
- allowPrivate := c.PostForm("allowPrivate") == "true"
- prepend := c.PostForm("prepend") == "true"
- intervalStr := c.PostForm("updateInterval")
- interval := 600
- if intervalStr != "" {
- if v, err := parseIntSafe(intervalStr); err == nil && v > 0 {
- interval = v
- }
- }
- if err := a.OutboundSubscriptionService.Update(subID, remark, rawURL, prefix, enabled, interval, allowPrivate, prepend); err != nil {
- jsonMsg(c, "Failed to update outbound subscription", err)
- return
- }
- jsonObj(c, "", nil)
- }
- func (a *XraySettingController) deleteOutboundSub(c *gin.Context) {
- id := c.Param("id")
- var subID int
- if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
- jsonMsg(c, "Invalid id", err)
- return
- }
- if err := a.OutboundSubscriptionService.Delete(subID); err != nil {
- jsonMsg(c, "Failed to delete outbound subscription", err)
- return
- }
- // Signal that xray should drop this subscription's outbounds on next reload.
- a.XrayService.SetToNeedRestart()
- jsonObj(c, "", nil)
- }
- func (a *XraySettingController) refreshOutboundSub(c *gin.Context) {
- id := c.Param("id")
- var subID int
- if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
- jsonMsg(c, "Invalid id", err)
- return
- }
- obs, err := a.OutboundSubscriptionService.Refresh(subID)
- if err != nil {
- jsonMsg(c, "Refresh failed", err)
- return
- }
- // Signal that xray should pick up the new outbounds on next restart/reload
- a.XrayService.SetToNeedRestart()
- jsonObj(c, obs, nil)
- }
- func (a *XraySettingController) moveOutboundSub(c *gin.Context) {
- id := c.Param("id")
- var subID int
- if _, err := fmt.Sscanf(id, "%d", &subID); err != nil {
- jsonMsg(c, "Invalid id", err)
- return
- }
- up := c.PostForm("dir") == "up"
- if err := a.OutboundSubscriptionService.Move(subID, up); err != nil {
- jsonMsg(c, "Failed to reorder outbound subscription", err)
- return
- }
- // Order affects the merged outbounds, so xray needs a reload.
- a.XrayService.SetToNeedRestart()
- jsonObj(c, "", nil)
- }
- // parseOutboundSubURL is a preview endpoint: it fetches + parses the provided
- // URL but does not persist anything. Useful for the "add subscription" flow
- // so the user can see the resulting outbounds (and assigned tags) before saving.
- func (a *XraySettingController) parseOutboundSubURL(c *gin.Context) {
- rawURL := c.PostForm("url")
- if rawURL == "" {
- jsonMsg(c, "url is required", common.NewError("missing url"))
- return
- }
- allowPrivate := c.PostForm("allowPrivate") == "true"
- // Use a throw-away service instance; it only needs the settingService for proxy.
- svc := service.OutboundSubscriptionService{}
- // We don't have a direct "fetch once" that returns without storing, so we
- // temporarily create a disabled row, refresh it, then delete. Cleaner would
- // be to expose a pure ParseURL on the service, but this keeps the surface small.
- tmp, err := svc.Create("preview", rawURL, "", false, 600, allowPrivate, false)
- if err != nil {
- jsonMsg(c, "Failed to preview subscription", err)
- return
- }
- obs, err := svc.Refresh(tmp.Id)
- // best-effort cleanup
- _ = svc.Delete(tmp.Id)
- if err != nil {
- jsonMsg(c, "Failed to fetch/parse subscription", err)
- return
- }
- jsonObj(c, obs, nil)
- }
- func parseIntSafe(s string) (int, error) {
- var v int
- _, err := fmt.Sscanf(s, "%d", &v)
- return v, err
- }
|