diff --git a/backend/app/api/v1/website.go b/backend/app/api/v1/website.go index 2c6298303..8be758283 100644 --- a/backend/app/api/v1/website.go +++ b/backend/app/api/v1/website.go @@ -758,3 +758,46 @@ func (b *BaseApi) UpdateAuthConfig(c *gin.Context) { } helper.SuccessWithOutData(c) } + +// @Tags Website +// @Summary Get AntiLeech conf +// @Description 获取防盗链配置 +// @Accept json +// @Param request body request.NginxCommonReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/leech [post] +func (b *BaseApi) GetAntiLeech(c *gin.Context) { + var req request.NginxCommonReq + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + res, err := websiteService.GetAntiLeech(req.WebsiteID) + if err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithData(c, res) +} + +// @Tags Website +// @Summary Update AntiLeech +// @Description 更新防盗链配置 +// @Accept json +// @Param request body request.NginxAntiLeechUpdate true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Router /websites/leech/update [post] +func (b *BaseApi) UpdateAntiLeech(c *gin.Context) { + var req request.NginxAntiLeechUpdate + if err := c.ShouldBindJSON(&req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInvalidParams, err) + return + } + if err := websiteService.UpdateAntiLeech(req); err != nil { + helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err) + return + } + helper.SuccessWithOutData(c) +} diff --git a/backend/app/dto/request/nginx.go b/backend/app/dto/request/nginx.go index 0ba0156f8..07c3e79bc 100644 --- a/backend/app/dto/request/nginx.go +++ b/backend/app/dto/request/nginx.go @@ -48,3 +48,21 @@ type NginxAuthUpdate struct { type NginxAuthReq struct { WebsiteID uint `json:"websiteID" validate:"required"` } + +type NginxCommonReq struct { + WebsiteID uint `json:"websiteID" validate:"required"` +} + +type NginxAntiLeechUpdate struct { + WebsiteID uint `json:"websiteID" validate:"required"` + Extends string `json:"extends" validate:"required"` + Return string `json:"return" validate:"required"` + Enable bool `json:"enable" validate:"required"` + ServerNames []string `json:"serverNames"` + Cache bool `json:"cache"` + CacheTime int `json:"cacheTime"` + CacheUint string `json:"cacheUint"` + NoneRef bool `json:"noneRef"` + LogEnable bool `json:"logEnable"` + Blocked bool `json:"blocked"` +} diff --git a/backend/app/dto/response/nginx.go b/backend/app/dto/response/nginx.go index 7dcf14dd8..230337743 100644 --- a/backend/app/dto/response/nginx.go +++ b/backend/app/dto/response/nginx.go @@ -21,3 +21,16 @@ type NginxAuthRes struct { Enable bool `json:"enable"` Items []dto.NginxAuth `json:"items"` } + +type NginxAntiLeechRes struct { + Enable bool `json:"enable"` + Extends string `json:"extends"` + Return string `json:"return"` + ServerNames []string `json:"serverNames"` + Cache bool `json:"cache"` + CacheTime int `json:"cacheTime"` + CacheUint string `json:"cacheUint"` + NoneRef bool `json:"noneRef"` + LogEnable bool `json:"logEnable"` + Blocked bool `json:"blocked"` +} diff --git a/backend/app/service/website.go b/backend/app/service/website.go index 9544faf57..4814aa044 100644 --- a/backend/app/service/website.go +++ b/backend/app/service/website.go @@ -76,6 +76,8 @@ type IWebsiteService interface { UpdateProxyFile(req request.NginxProxyUpdate) (err error) GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) UpdateAuthBasic(req request.NginxAuthUpdate) (err error) + GetAntiLeech(id uint) (*response.NginxAntiLeechRes, error) + UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err error) } func NewIWebsiteService() IWebsiteService { @@ -1645,3 +1647,172 @@ func (w WebsiteService) GetAuthBasics(req request.NginxAuthReq) (res response.Ng } return } + +func (w WebsiteService) UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID)) + if err != nil { + return + } + nginxFull, err := getNginxFull(&website) + if err != nil { + return + } + fileOp := files.NewFileOp() + backpContent, err := fileOp.GetContent(nginxFull.SiteConfig.Config.FilePath) + if err != nil { + return + } + block := nginxFull.SiteConfig.Config.FindServers()[0] + locations := block.FindDirectives("location") + for _, location := range locations { + loParams := location.GetParameters() + if len(loParams) > 1 || loParams[0] == "~" { + extendStr := loParams[1] + if strings.HasPrefix(extendStr, `.*\.(`) && strings.HasSuffix(extendStr, `)$`) { + block.RemoveDirective("location", loParams) + } + } + } + if req.Enable { + exts := strings.Split(req.Extends, ",") + newDirective := components.Directive{ + Name: "location", + Parameters: []string{"~", fmt.Sprintf(`.*\.(%s)$`, strings.Join(exts, "|"))}, + } + + newBlock := &components.Block{} + newBlock.Directives = make([]components.IDirective, 0) + if req.Cache { + newBlock.Directives = append(newBlock.Directives, &components.Directive{ + Name: "expires", + Parameters: []string{strconv.Itoa(req.CacheTime) + req.CacheUint}, + }) + } + newBlock.Directives = append(newBlock.Directives, &components.Directive{ + Name: "log_not_found", + Parameters: []string{"off"}, + }) + validDir := &components.Directive{ + Name: "valid_referers", + Parameters: []string{}, + } + if req.NoneRef { + validDir.Parameters = append(validDir.Parameters, "none") + } + if len(req.ServerNames) > 0 { + validDir.Parameters = append(validDir.Parameters, "server_names", strings.Join(req.ServerNames, " ")) + } + newBlock.Directives = append(newBlock.Directives, validDir) + + ifDir := &components.Directive{ + Name: "if", + Parameters: []string{"($invalid_referer)"}, + } + ifDir.Block = &components.Block{ + Directives: []components.IDirective{ + &components.Directive{ + Name: "return", + Parameters: []string{req.Return}, + }, + &components.Directive{ + Name: "access_log", + Parameters: []string{"off"}, + }, + }, + } + newBlock.Directives = append(newBlock.Directives, ifDir) + newDirective.Block = newBlock + block.Directives = append(block.Directives, &newDirective) + } + + if err = nginx.WriteConfig(nginxFull.SiteConfig.Config, nginx.IndentedStyle); err != nil { + return + } + if err = updateNginxConfig(constant.NginxScopeServer, nil, &website); err != nil { + _ = fileOp.WriteFile(nginxFull.SiteConfig.Config.FilePath, bytes.NewReader(backpContent), 0755) + return + } + return +} + +func (w WebsiteService) GetAntiLeech(id uint) (*response.NginxAntiLeechRes, error) { + website, err := websiteRepo.GetFirst(commonRepo.WithByID(id)) + if err != nil { + return nil, err + } + nginxFull, err := getNginxFull(&website) + if err != nil { + return nil, err + } + res := &response.NginxAntiLeechRes{ + LogEnable: true, + ServerNames: []string{}, + } + block := nginxFull.SiteConfig.Config.FindServers()[0] + locations := block.FindDirectives("location") + for _, location := range locations { + loParams := location.GetParameters() + if len(loParams) > 1 || loParams[0] == "~" { + extendStr := loParams[1] + if strings.HasPrefix(extendStr, `.*\.(`) && strings.HasSuffix(extendStr, `)$`) { + str1 := strings.TrimPrefix(extendStr, `.*\.(`) + str2 := strings.TrimSuffix(str1, ")$") + res.Extends = strings.Join(strings.Split(str2, "|"), ",") + } + } + lDirectives := location.GetBlock().GetDirectives() + for _, lDir := range lDirectives { + if lDir.GetName() == "valid_referers" { + res.Enable = true + params := lDir.GetParameters() + serverIndex := 0 + serverNameExist := false + for i, param := range params { + if param == "none" { + res.NoneRef = true + } + if param == "blocked" { + res.Blocked = true + } + if param == "server_names" { + serverIndex = i + serverNameExist = true + } + } + if serverNameExist { + serverNames := params[serverIndex+1:] + res.ServerNames = serverNames + } + } + if lDir.GetName() == "if" && lDir.GetParameters()[0] == "($invalid_referer)" { + directives := lDir.GetBlock().GetDirectives() + for _, dir := range directives { + if dir.GetName() == "return" { + res.Return = strings.Join(dir.GetParameters(), " ") + } + if dir.GetName() == "access_log" { + if strings.Join(dir.GetParameters(), "") == "off" { + res.LogEnable = false + } + } + } + } + if lDir.GetName() == "expires" { + res.Cache = true + re := regexp.MustCompile(`^(\d+)(\w+)$`) + matches := re.FindStringSubmatch(lDir.GetParameters()[0]) + if matches == nil { + continue + } + cacheTime, err := strconv.Atoi(matches[1]) + if err != nil { + continue + } + unit := matches[2] + res.CacheUint = unit + res.CacheTime = cacheTime + } + } + } + return res, nil +} diff --git a/backend/router/ro_website.go b/backend/router/ro_website.go index fb39d2501..1fbba67a7 100644 --- a/backend/router/ro_website.go +++ b/backend/router/ro_website.go @@ -58,5 +58,8 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) { groupRouter.POST("/auths", baseApi.GetAuthConfig) groupRouter.POST("/auths/update", baseApi.UpdateAuthConfig) + + groupRouter.POST("/leech", baseApi.GetAntiLeech) + groupRouter.POST("/leech/update", baseApi.UpdateAntiLeech) } } diff --git a/frontend/src/api/interface/website.ts b/frontend/src/api/interface/website.ts index 78a19db1f..0ae59f045 100644 --- a/frontend/src/api/interface/website.ts +++ b/frontend/src/api/interface/website.ts @@ -365,4 +365,22 @@ export namespace Website { password: string; remark: string; } + + export interface LeechConfig { + enable: boolean; + cache: boolean; + cacheTime: number; + cacheUint: string; + extends: string; + return: string; + serverNames: string[]; + noneRef: boolean; + logEnable: boolean; + blocked: boolean; + websiteID?: number; + } + + export interface LeechReq { + websiteID: number; + } } diff --git a/frontend/src/api/modules/website.ts b/frontend/src/api/modules/website.ts index b3b629802..672e68b9b 100644 --- a/frontend/src/api/modules/website.ts +++ b/frontend/src/api/modules/website.ts @@ -206,3 +206,11 @@ export const GetAuthConfig = (req: Website.AuthReq) => { export const OperateAuthConfig = (req: Website.NginxAuthConfig) => { return http.post(`/websites/auths/update`, req); }; + +export const GetAntiLeech = (req: Website.LeechReq) => { + return http.post(`/websites/leech`, req); +}; + +export const UpdateAntiLeech = (req: Website.LeechConfig) => { + return http.post(`/websites/leech/update`, req); +}; diff --git a/frontend/src/global/form-rules.ts b/frontend/src/global/form-rules.ts index a6e331d63..3a0bebe9a 100644 --- a/frontend/src/global/form-rules.ts +++ b/frontend/src/global/form-rules.ts @@ -290,6 +290,19 @@ const checkDisableFunctions = (rule: any, value: any, callback: any) => { } }; +const checkLeechExts = (rule: any, value: any, callback: any) => { + if (value === '' || typeof value === 'undefined' || value == null) { + callback(new Error(i18n.global.t('commons.rule.leechExts'))); + } else { + const reg = /^[a-zA-Z0-9,]+$/; + if (!reg.test(value) && value !== '') { + callback(new Error(i18n.global.t('commons.rule.leechExts'))); + } else { + callback(); + } + } +}; + interface CommonRule { requiredInput: FormItemRule; requiredSelect: FormItemRule; @@ -314,6 +327,7 @@ interface CommonRule { appName: FormItemRule; containerName: FormItemRule; disabledFunctions: FormItemRule; + leechExts: FormItemRule; paramCommon: FormItemRule; paramComplexity: FormItemRule; @@ -465,4 +479,9 @@ export const Rules: CommonRule = { trigger: 'blur', validator: checkDisableFunctions, }, + leechExts: { + required: true, + trigger: 'blur', + validator: checkLeechExts, + }, }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index cce28ae84..e3aeb3559 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -160,6 +160,7 @@ const message = { appName: 'Support English, numbers, - and _, length 2-30, and cannot start and end with -_', conatinerName: 'Supports letters, numbers, underscores, hyphens and dots, cannot end with hyphen- or dot.', disableFunction: 'Only support letters and,', + leechExts: 'Only support letters, numbers and,', }, res: { paramError: 'The request failed, please try again later!', @@ -1375,6 +1376,16 @@ const message = { editBasicAuthHelper: 'The password is asymmetrically encrypted and cannot be echoed. Editing needs to reset the password', createPassword: 'Generate password', + antiLeech: 'Anti-leech', + extends: 'Extension', + browserCache: 'browser cache', + leechLog: 'Record anti-leech log', + accessDomain: 'Allowed domain names', + leechReturn: 'Response resource', + noneRef: 'Allow the source to be empty', + disable: 'not enabled', + disableLeechHelper: 'Whether to disable the anti-leech', + disableLeech: 'Disable anti-leech', }, php: { short_open_tag: 'Short tag support', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index b5e4e0421..294bf54e7 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -163,6 +163,7 @@ const message = { appName: '支持英文、数字、-和_,长度2-30,并且不能以-_开头和结尾', conatinerName: '支持字母、数字、下划线、连字符和点,不能以连字符-或点.结尾', disableFunction: '仅支持字母和,', + leechExts: '件支持字母数字和,', }, res: { paramError: '请求失败,请稍后重试!', @@ -1355,6 +1356,16 @@ const message = { basicAuth: '密码访问', editBasicAuthHelper: '密码为非对称加密,无法回显,编辑需要重新设置密码', createPassword: '生成密码', + antiLeech: '防盗链', + extends: '扩展名', + browserCache: '浏览器缓存', + leechLog: '记录防盗链日志', + accessDomain: '允许的域名', + leechReturn: '响应资源', + noneRef: '允许来源为空', + disable: '未启用', + disableLeechHelper: '是否禁用防盗链', + disableLeech: '禁用防盗链', }, php: { short_open_tag: '短标签支持', diff --git a/frontend/src/views/website/website/config/basic/anti-Leech/index.vue b/frontend/src/views/website/website/config/basic/anti-Leech/index.vue new file mode 100644 index 000000000..ac4aac8d7 --- /dev/null +++ b/frontend/src/views/website/website/config/basic/anti-Leech/index.vue @@ -0,0 +1,189 @@ + + + diff --git a/frontend/src/views/website/website/config/basic/https/index.vue b/frontend/src/views/website/website/config/basic/https/index.vue index fe75007ed..5e77fa0e4 100644 --- a/frontend/src/views/website/website/config/basic/https/index.vue +++ b/frontend/src/views/website/website/config/basic/https/index.vue @@ -129,7 +129,7 @@ const id = computed(() => { return props.id; }); const httpsForm = ref(); -let form = reactive({ +const form = reactive({ enable: false, websiteId: id.value, websiteSSLId: undefined, @@ -141,10 +141,10 @@ let form = reactive({ 'EECDH+CHACHA20:EECDH+CHACHA20-draft:EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5', SSLProtocol: ['TLSv1.3', 'TLSv1.2', 'TLSv1.1', 'TLSv1'], }); -let loading = ref(false); +const loading = ref(false); const ssls = ref(); -let websiteSSL = ref(); -let rules = ref({ +const websiteSSL = ref(); +const rules = ref({ type: [Rules.requiredSelect], privateKey: [Rules.requiredInput], certificate: [Rules.requiredInput], diff --git a/frontend/src/views/website/website/config/basic/index.vue b/frontend/src/views/website/website/config/basic/index.vue index abf47b7ae..f76836c6d 100644 --- a/frontend/src/views/website/website/config/basic/index.vue +++ b/frontend/src/views/website/website/config/basic/index.vue @@ -24,8 +24,11 @@ + + + - + @@ -42,6 +45,7 @@ import SitePath from './site-folder/index.vue'; import Rewrite from './rewrite/index.vue'; import Proxy from './proxy/index.vue'; import AuthBasic from './auth-basic/index.vue'; +import AntiLeech from './anti-Leech/index.vue'; const props = defineProps({ id: {