mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-04-22 15:35:52 +00:00
* Add SSRF protection for custom geo downloads Introduce SSRF-safe HTTP transport for custom geo operations by adding ssrfSafeTransport and isBlockedIP helpers. The transport resolves hosts and blocks loopback, private, link-local and unspecified addresses, returning ErrCustomGeoSSRFBlocked on violations. Update probeCustomGeoURLWithGET, probeCustomGeoURL and downloadToPathOnce to use the safe transport. Also add the new error ErrCustomGeoSSRFBlocked and necessary imports. Minor whitespace/formatting adjustments in subClashService.go, web/entity/entity.go and web/service/setting.go. * Add path traversal protection for custom geo Prevent path traversal when handling custom geo downloads by adding ErrCustomGeoPathTraversal and a validateDestPath() helper that ensures destination paths stay inside the bin folder. Call validateDestPath from downloadToPathOnce, Update and Delete paths and wrap errors appropriately. Reconstruct sanitized URLs in sanitizeURL to break taint propagation before use. Map the new path-traversal error to a user-facing i18n message in the controller. * fix
180 lines
5.5 KiB
Go
180 lines
5.5 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
"github.com/mhsanaei/3x-ui/v2/database/model"
|
|
"github.com/mhsanaei/3x-ui/v2/logger"
|
|
"github.com/mhsanaei/3x-ui/v2/web/entity"
|
|
"github.com/mhsanaei/3x-ui/v2/web/service"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type CustomGeoController struct {
|
|
BaseController
|
|
customGeoService *service.CustomGeoService
|
|
}
|
|
|
|
func NewCustomGeoController(g *gin.RouterGroup, customGeo *service.CustomGeoService) *CustomGeoController {
|
|
a := &CustomGeoController{customGeoService: customGeo}
|
|
a.initRouter(g)
|
|
return a
|
|
}
|
|
|
|
func (a *CustomGeoController) initRouter(g *gin.RouterGroup) {
|
|
g.GET("/list", a.list)
|
|
g.GET("/aliases", a.aliases)
|
|
g.POST("/add", a.add)
|
|
g.POST("/update/:id", a.update)
|
|
g.POST("/delete/:id", a.delete)
|
|
g.POST("/download/:id", a.download)
|
|
g.POST("/update-all", a.updateAll)
|
|
}
|
|
|
|
func mapCustomGeoErr(c *gin.Context, err error) error {
|
|
if err == nil {
|
|
return nil
|
|
}
|
|
switch {
|
|
case errors.Is(err, service.ErrCustomGeoInvalidType):
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrInvalidType"))
|
|
case errors.Is(err, service.ErrCustomGeoAliasRequired):
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrAliasRequired"))
|
|
case errors.Is(err, service.ErrCustomGeoAliasPattern):
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrAliasPattern"))
|
|
case errors.Is(err, service.ErrCustomGeoAliasReserved):
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrAliasReserved"))
|
|
case errors.Is(err, service.ErrCustomGeoURLRequired):
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrUrlRequired"))
|
|
case errors.Is(err, service.ErrCustomGeoInvalidURL):
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrInvalidUrl"))
|
|
case errors.Is(err, service.ErrCustomGeoURLScheme):
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrUrlScheme"))
|
|
case errors.Is(err, service.ErrCustomGeoURLHost):
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrUrlHost"))
|
|
case errors.Is(err, service.ErrCustomGeoDuplicateAlias):
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrDuplicateAlias"))
|
|
case errors.Is(err, service.ErrCustomGeoNotFound):
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrNotFound"))
|
|
case errors.Is(err, service.ErrCustomGeoDownload):
|
|
logger.Warning("custom geo download:", err)
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrDownload"))
|
|
case errors.Is(err, service.ErrCustomGeoSSRFBlocked):
|
|
logger.Warning("custom geo SSRF blocked:", err)
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrUrlHost"))
|
|
case errors.Is(err, service.ErrCustomGeoPathTraversal):
|
|
logger.Warning("custom geo path traversal blocked:", err)
|
|
return errors.New(I18nWeb(c, "pages.index.customGeoErrDownload"))
|
|
default:
|
|
return err
|
|
}
|
|
}
|
|
|
|
func (a *CustomGeoController) list(c *gin.Context) {
|
|
list, err := a.customGeoService.GetAll()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoToastList"), mapCustomGeoErr(c, err))
|
|
return
|
|
}
|
|
jsonObj(c, list, nil)
|
|
}
|
|
|
|
func (a *CustomGeoController) aliases(c *gin.Context) {
|
|
out, err := a.customGeoService.GetAliasesForUI()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoAliasesError"), mapCustomGeoErr(c, err))
|
|
return
|
|
}
|
|
jsonObj(c, out, nil)
|
|
}
|
|
|
|
type customGeoForm struct {
|
|
Type string `json:"type" form:"type"`
|
|
Alias string `json:"alias" form:"alias"`
|
|
Url string `json:"url" form:"url"`
|
|
}
|
|
|
|
func (a *CustomGeoController) add(c *gin.Context) {
|
|
var form customGeoForm
|
|
if err := c.ShouldBind(&form); err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoToastAdd"), err)
|
|
return
|
|
}
|
|
r := &model.CustomGeoResource{
|
|
Type: form.Type,
|
|
Alias: form.Alias,
|
|
Url: form.Url,
|
|
}
|
|
err := a.customGeoService.Create(r)
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoToastAdd"), mapCustomGeoErr(c, err))
|
|
}
|
|
|
|
func parseCustomGeoID(c *gin.Context, idStr string) (int, bool) {
|
|
id, err := strconv.Atoi(idStr)
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoInvalidId"), err)
|
|
return 0, false
|
|
}
|
|
if id <= 0 {
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoInvalidId"), errors.New(""))
|
|
return 0, false
|
|
}
|
|
return id, true
|
|
}
|
|
|
|
func (a *CustomGeoController) update(c *gin.Context) {
|
|
id, ok := parseCustomGeoID(c, c.Param("id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
var form customGeoForm
|
|
if bindErr := c.ShouldBind(&form); bindErr != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoToastUpdate"), bindErr)
|
|
return
|
|
}
|
|
r := &model.CustomGeoResource{
|
|
Type: form.Type,
|
|
Alias: form.Alias,
|
|
Url: form.Url,
|
|
}
|
|
err := a.customGeoService.Update(id, r)
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoToastUpdate"), mapCustomGeoErr(c, err))
|
|
}
|
|
|
|
func (a *CustomGeoController) delete(c *gin.Context) {
|
|
id, ok := parseCustomGeoID(c, c.Param("id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
name, err := a.customGeoService.Delete(id)
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoToastDelete", "fileName=="+name), mapCustomGeoErr(c, err))
|
|
}
|
|
|
|
func (a *CustomGeoController) download(c *gin.Context) {
|
|
id, ok := parseCustomGeoID(c, c.Param("id"))
|
|
if !ok {
|
|
return
|
|
}
|
|
name, err := a.customGeoService.TriggerUpdate(id)
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoToastDownload", "fileName=="+name), mapCustomGeoErr(c, err))
|
|
}
|
|
|
|
func (a *CustomGeoController) updateAll(c *gin.Context) {
|
|
res, err := a.customGeoService.TriggerUpdateAll()
|
|
if err != nil {
|
|
jsonMsg(c, I18nWeb(c, "pages.index.customGeoToastUpdateAll"), mapCustomGeoErr(c, err))
|
|
return
|
|
}
|
|
if len(res.Failed) > 0 {
|
|
c.JSON(http.StatusOK, entity.Msg{
|
|
Success: false,
|
|
Msg: I18nWeb(c, "pages.index.customGeoErrUpdateAllIncomplete"),
|
|
Obj: res,
|
|
})
|
|
return
|
|
}
|
|
jsonMsgObj(c, I18nWeb(c, "pages.index.customGeoToastUpdateAll"), res, nil)
|
|
}
|