diff --git a/agent/app/api/v2/website.go b/agent/app/api/v2/website.go index 402083e63..86911ee19 100644 --- a/agent/app/api/v2/website.go +++ b/agent/app/api/v2/website.go @@ -1161,3 +1161,45 @@ func (b *BaseApi) BatchOpWebsites(c *gin.Context) { } helper.Success(c) } + +// @Tags Website +// @Summary Get CORS Config +// @Accept json +// @Param id path int true "id" +// @Success 200 {object} request.CorsConfig +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/cors/{id} [get] +func (b *BaseApi) GetCORSConfig(c *gin.Context) { + id, err := helper.GetParamID(c) + if err != nil { + helper.BadRequest(c, err) + return + } + res, err := websiteService.GetCors(id) + if err != nil { + helper.InternalServer(c, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Update CORS Config +// @Accept json +// @Param request body request.CorsConfigReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /websites/cors/update [post] +func (b *BaseApi) UpdateCORSConfig(c *gin.Context) { + var req request.CorsConfigReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := websiteService.UpdateCors(req); err != nil { + helper.InternalServer(c, err) + return + } + helper.Success(c) +} diff --git a/agent/app/dto/request/website.go b/agent/app/dto/request/website.go index b070f73d6..3e46c1829 100644 --- a/agent/app/dto/request/website.go +++ b/agent/app/dto/request/website.go @@ -211,30 +211,39 @@ type WebsiteUpdateDirPermission struct { } type WebsiteProxyConfig struct { - ID uint `json:"id" validate:"required"` - Operate string `json:"operate" validate:"required"` - Enable bool `json:"enable" ` - Cache bool `json:"cache" ` - CacheTime int `json:"cacheTime"` - CacheUnit string `json:"cacheUnit"` - ServerCacheTime int `json:"serverCacheTime"` - ServerCacheUnit string `json:"serverCacheUnit"` - Name string `json:"name" validate:"required"` - Modifier string `json:"modifier"` - Match string `json:"match" validate:"required"` - ProxyPass string `json:"proxyPass" validate:"required"` - ProxyHost string `json:"proxyHost" validate:"required"` - Content string `json:"content"` - FilePath string `json:"filePath"` - Replaces map[string]string `json:"replaces"` - SNI bool `json:"sni"` - ProxySSLName string `json:"proxySSLName"` - Cors bool `json:"cors"` - AllowOrigins string `json:"allowOrigins"` - AllowMethods string `json:"allowMethods"` - AllowHeaders string `json:"allowHeaders"` - AllowCredentials bool `json:"allowCredentials"` - Preflight bool `json:"preflight"` + ID uint `json:"id" validate:"required"` + Operate string `json:"operate" validate:"required"` + Enable bool `json:"enable" ` + Cache bool `json:"cache" ` + CacheTime int `json:"cacheTime"` + CacheUnit string `json:"cacheUnit"` + ServerCacheTime int `json:"serverCacheTime"` + ServerCacheUnit string `json:"serverCacheUnit"` + Name string `json:"name" validate:"required"` + Modifier string `json:"modifier"` + Match string `json:"match" validate:"required"` + ProxyPass string `json:"proxyPass" validate:"required"` + ProxyHost string `json:"proxyHost" validate:"required"` + Content string `json:"content"` + FilePath string `json:"filePath"` + Replaces map[string]string `json:"replaces"` + SNI bool `json:"sni"` + ProxySSLName string `json:"proxySSLName"` + CorsConfig +} + +type CorsConfig struct { + Cors bool `json:"cors"` + AllowOrigins string `json:"allowOrigins"` + AllowMethods string `json:"allowMethods"` + AllowHeaders string `json:"allowHeaders"` + AllowCredentials bool `json:"allowCredentials"` + Preflight bool `json:"preflight"` +} + +type CorsConfigReq struct { + WebsiteID uint `json:"websiteID" validate:"required"` + CorsConfig } type WebsiteProxyReq struct { diff --git a/agent/app/service/website.go b/agent/app/service/website.go index a757841f5..9e6521246 100644 --- a/agent/app/service/website.go +++ b/agent/app/service/website.go @@ -96,6 +96,9 @@ type IWebsiteService interface { ClearProxyCache(req request.NginxCommonReq) error DeleteProxy(req request.WebsiteProxyDel) (err error) + UpdateCors(req request.CorsConfigReq) error + GetCors(websiteID uint) (*request.CorsConfig, error) + GetAntiLeech(id uint) (*response.NginxAntiLeechRes, error) UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err error) @@ -2018,6 +2021,65 @@ func (w WebsiteService) DeleteProxy(req request.WebsiteProxyDel) (err error) { return updateNginxConfig(constant.NginxScopeServer, nil, &website) } +func (w WebsiteService) UpdateCors(req request.CorsConfigReq) error { + website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID)) + if err != nil { + return err + } + params := []dto.NginxParam{ + {Name: "add_header", Params: []string{"Access-Control-Allow-Origin"}}, + {Name: "add_header", Params: []string{"Access-Control-Allow-Methods"}}, + {Name: "add_header", Params: []string{"Access-Control-Allow-Headers"}}, + {Name: "add_header", Params: []string{"Access-Control-Allow-Credentials"}}, + {Name: "if", Params: []string{"(", "$request_method", "=", "'OPTIONS'", ")"}}, + } + if err := deleteNginxConfig(constant.NginxScopeServer, params, &website); err != nil { + return err + } + if req.Cors { + return updateWebsiteConfig(website, func(server *components.Server) error { + server.UpdateDirective("add_header", []string{"Access-Control-Allow-Origin", req.AllowOrigins, "always"}) + if req.AllowMethods != "" { + server.UpdateDirective("add_header", []string{"Access-Control-Allow-Methods", req.AllowMethods, "always"}) + } + if req.AllowHeaders != "" { + server.UpdateDirective("add_header", []string{"Access-Control-Allow-Headers", req.AllowHeaders, "always"}) + } + if req.AllowCredentials { + server.UpdateDirective("add_header", []string{"Access-Control-Allow-Credentials", "true", "always"}) + } + if req.Preflight { + server.AddCorsOption() + } + return nil + }) + } + return nil +} + +func (w WebsiteService) GetCors(websiteID uint) (*request.CorsConfig, error) { + website, err := websiteRepo.GetFirst(repo.WithByID(websiteID)) + if err != nil { + return nil, err + } + server, err := getServer(website) + if err != nil { + return nil, err + } + if server == nil { + return nil, nil + } + cors := &request.CorsConfig{ + Cors: server.Cors, + AllowOrigins: server.AllowOrigins, + AllowMethods: server.AllowMethods, + AllowHeaders: server.AllowHeaders, + AllowCredentials: server.AllowCredentials, + Preflight: server.Preflight, + } + return cors, nil +} + func (w WebsiteService) GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) { var ( website model.Website diff --git a/agent/app/service/website_utils.go b/agent/app/service/website_utils.go index 5cf5def38..dd3433f3d 100644 --- a/agent/app/service/website_utils.go +++ b/agent/app/service/website_utils.go @@ -1303,6 +1303,7 @@ const ( SiteRootAuthBasicPath = "SiteRootAuthBasicPath" SitePathAuthBasicDir = "SitePathAuthBasicDir" SiteUpstreamDir = "SiteUpstreamDir" + SiteCorsPath = "SiteCorsPath" ) func GetSitePath(website model.Website, confType string) string { @@ -1333,6 +1334,8 @@ func GetSitePath(website model.Website, confType string) string { return path.Join(GteSiteDir(website.Alias), "path_auth") case SiteUpstreamDir: return path.Join(GteSiteDir(website.Alias), "upstream") + case SiteCorsPath: + return path.Join(GteSiteDir(website.Alias), "cors", "cors.conf") } return "" } @@ -1481,3 +1484,53 @@ func ParseDomain(domain string) (*model.WebsiteDomain, error) { Port: port, }, nil } + +func updateWebsiteConfig(website model.Website, updateFunc func(server *components.Server) error) error { + configPath := GetSitePath(website, SiteConf) + nginxContent, err := files.NewFileOp().GetContent(configPath) + if err != nil { + return err + } + + config, err := parser.NewStringParser(string(nginxContent)).Parse() + if err != nil { + return err + } + config.FilePath = configPath + servers := config.FindServers() + if len(servers) == 0 { + return errors.New("nginx config is not valid") + } + server := servers[0] + if err := updateFunc(server); err != nil { + return err + } + if err = nginx.WriteConfig(config, nginx.IndentedStyle); err != nil { + return err + } + nginxInstall, err := getAppInstallByKey(constant.AppOpenresty) + if err != nil { + return err + } + return nginxCheckAndReload(string(nginxContent), configPath, nginxInstall.ContainerName) +} + +func getServer(website model.Website) (*components.Server, error) { + configPath := GetSitePath(website, SiteConf) + nginxContent, err := files.NewFileOp().GetContent(configPath) + if err != nil { + return nil, err + } + + config, err := parser.NewStringParser(string(nginxContent)).Parse() + if err != nil { + return nil, err + } + + servers := config.FindServers() + if len(servers) == 0 { + return nil, errors.New("nginx config is not valid") + } + server := servers[0] + return server, nil +} diff --git a/agent/router/ro_website.go b/agent/router/ro_website.go index 16bdb9dcb..ec6606e5b 100644 --- a/agent/router/ro_website.go +++ b/agent/router/ro_website.go @@ -61,6 +61,9 @@ func (a *WebsiteRouter) InitRouter(Router *gin.RouterGroup) { websiteRouter.POST("/auths/path", baseApi.GetPathAuthConfig) websiteRouter.POST("/auths/path/update", baseApi.UpdatePathAuthConfig) + websiteRouter.GET("/cors/:id", baseApi.GetCORSConfig) + websiteRouter.POST("/cors/update", baseApi.UpdateCORSConfig) + websiteRouter.POST("/leech", baseApi.GetAntiLeech) websiteRouter.POST("/leech/update", baseApi.UpdateAntiLeech) diff --git a/agent/utils/nginx/components/server.go b/agent/utils/nginx/components/server.go index 8e7d5f352..2ea3eb9dd 100644 --- a/agent/utils/nginx/components/server.go +++ b/agent/utils/nginx/components/server.go @@ -5,10 +5,16 @@ import ( ) type Server struct { - Comment string - Listens []*ServerListen - Directives []IDirective - Line int + Comment string + Listens []*ServerListen + Directives []IDirective + Line int + Cors bool + AllowMethods string + AllowHeaders string + AllowOrigins string + AllowCredentials bool + Preflight bool } func (s *Server) GetCodeBlock() string { @@ -25,6 +31,28 @@ func NewServer(directive IDirective) (*Server, error) { switch dir.GetName() { case "listen": server.Listens = append(server.Listens, NewServerListen(dir.GetParameters(), dir.GetLine())) + case "add_header": + params := dir.GetParameters() + if params[0] == "Access-Control-Allow-Origin" { + server.Cors = true + server.AllowOrigins = params[1] + } + if params[0] == "Access-Control-Allow-Methods" { + server.AllowMethods = params[1] + } + if params[0] == "Access-Control-Allow-Headers" { + server.AllowHeaders = params[1] + } + if params[0] == "Access-Control-Allow-Credentials" && params[1] == "true" { + server.AllowCredentials = true + } + server.Directives = append(server.Directives, dir) + case "if": + params := dir.GetParameters() + if params[0] == "(" && params[1] == "$request_method" && params[2] == `=` && params[3] == `'OPTIONS'` && params[4] == ")" { + server.Preflight = true + } + server.Directives = append(server.Directives, dir) default: server.Directives = append(server.Directives, dir) } @@ -479,3 +507,26 @@ func (s *Server) UpdateAllowIPs(ips []string) { s.Directives = append(s.Directives, ipDirectives...) } } + +func (s *Server) AddCorsOption() { + newDir := &Directive{ + Name: "if", + Parameters: []string{"(", "$request_method", "=", "'OPTIONS'", ")"}, + Block: &Block{}, + } + block := &Block{} + block.AppendDirectives(&Directive{ + Name: "return", + Parameters: []string{"204"}, + }) + newDir.Block = block + directives := s.GetDirectives() + newDirectives := make([]IDirective, 0) + for _, dir := range directives { + if dir.GetName() != "listen" { + newDirectives = append(newDirectives, dir) + } + } + newDirectives = append(newDirectives, newDir) + s.Directives = newDirectives +} diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index e0ab8a6dc..24edc6645 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -681,4 +681,17 @@ export namespace Website { operate: string; taskID: string; } + + export interface CorsConfig { + cors: boolean; + allowOrigins: string; + allowMethods: string; + allowHeaders: string; + allowCredentials: boolean; + preflight: boolean; + } + + export interface CorsConfigReq extends CorsConfig { + websiteID: number; + } } diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index dcc1a3bb0..a8c4aa05b 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -359,3 +359,11 @@ export const execComposer = (req: Website.ExecComposer) => { export const batchOpreate = (req: Website.BatchOperate) => { return http.post(`/websites/batch/operate`, req); }; + +export const getCorsConfig = (id: number) => { + return http.get(`/websites/cors/${id}`); +}; + +export const updateCorsConfig = (req: Website.CorsConfigReq) => { + return http.post(`/websites/cors/update`, req); +}; diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 39ed7c00c..55152eb53 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -2414,7 +2414,7 @@ const message = { ipFromExample3: '如果不确定,可以填 0.0.0.0/0(ipv4) ::/0(ipv6) [注意:允许任意来源 IP 不安全]', http3Helper: 'HTTP/3 是 HTTP/2 的升级版本,提供更快的连接速度和更好的性能,但是不是所有浏览器都支持 HTTP/3,开启后可能会导致部分浏览器无法访问', - cors: '跨域访问(CORS)', + cors: '跨域访问', enableCors: '开启跨域', allowOrigins: '允许访问的域名', allowMethods: '允许的请求方法', diff --git a/frontend/src/views/website/website/config/basic/cors/index.vue b/frontend/src/views/website/website/config/basic/cors/index.vue new file mode 100644 index 000000000..30269ff43 --- /dev/null +++ b/frontend/src/views/website/website/config/basic/cors/index.vue @@ -0,0 +1,102 @@ + + + diff --git a/frontend/src/views/website/website/config/basic/index.vue b/frontend/src/views/website/website/config/basic/index.vue index d8ade8238..61aa2cf22 100644 --- a/frontend/src/views/website/website/config/basic/index.vue +++ b/frontend/src/views/website/website/config/basic/index.vue @@ -1,40 +1,43 @@