feat: 网站增加防盗链设置 (#1151)

Refs https://github.com/1Panel-dev/1Panel/issues/1149
This commit is contained in:
zhengkunwang223 2023-05-26 10:40:21 +08:00 committed by GitHub
parent d62f566bb3
commit e70ec0e978
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 513 additions and 5 deletions

View file

@ -758,3 +758,46 @@ func (b *BaseApi) UpdateAuthConfig(c *gin.Context) {
} }
helper.SuccessWithOutData(c) 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)
}

View file

@ -48,3 +48,21 @@ type NginxAuthUpdate struct {
type NginxAuthReq struct { type NginxAuthReq struct {
WebsiteID uint `json:"websiteID" validate:"required"` 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"`
}

View file

@ -21,3 +21,16 @@ type NginxAuthRes struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Items []dto.NginxAuth `json:"items"` 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"`
}

View file

@ -76,6 +76,8 @@ type IWebsiteService interface {
UpdateProxyFile(req request.NginxProxyUpdate) (err error) UpdateProxyFile(req request.NginxProxyUpdate) (err error)
GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error) GetAuthBasics(req request.NginxAuthReq) (res response.NginxAuthRes, err error)
UpdateAuthBasic(req request.NginxAuthUpdate) (err error) UpdateAuthBasic(req request.NginxAuthUpdate) (err error)
GetAntiLeech(id uint) (*response.NginxAntiLeechRes, error)
UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err error)
} }
func NewIWebsiteService() IWebsiteService { func NewIWebsiteService() IWebsiteService {
@ -1645,3 +1647,172 @@ func (w WebsiteService) GetAuthBasics(req request.NginxAuthReq) (res response.Ng
} }
return 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
}

View file

@ -58,5 +58,8 @@ func (a *WebsiteRouter) InitWebsiteRouter(Router *gin.RouterGroup) {
groupRouter.POST("/auths", baseApi.GetAuthConfig) groupRouter.POST("/auths", baseApi.GetAuthConfig)
groupRouter.POST("/auths/update", baseApi.UpdateAuthConfig) groupRouter.POST("/auths/update", baseApi.UpdateAuthConfig)
groupRouter.POST("/leech", baseApi.GetAntiLeech)
groupRouter.POST("/leech/update", baseApi.UpdateAntiLeech)
} }
} }

View file

@ -365,4 +365,22 @@ export namespace Website {
password: string; password: string;
remark: 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;
}
} }

View file

@ -206,3 +206,11 @@ export const GetAuthConfig = (req: Website.AuthReq) => {
export const OperateAuthConfig = (req: Website.NginxAuthConfig) => { export const OperateAuthConfig = (req: Website.NginxAuthConfig) => {
return http.post<any>(`/websites/auths/update`, req); return http.post<any>(`/websites/auths/update`, req);
}; };
export const GetAntiLeech = (req: Website.LeechReq) => {
return http.post<Website.LeechConfig>(`/websites/leech`, req);
};
export const UpdateAntiLeech = (req: Website.LeechConfig) => {
return http.post<any>(`/websites/leech/update`, req);
};

View file

@ -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 { interface CommonRule {
requiredInput: FormItemRule; requiredInput: FormItemRule;
requiredSelect: FormItemRule; requiredSelect: FormItemRule;
@ -314,6 +327,7 @@ interface CommonRule {
appName: FormItemRule; appName: FormItemRule;
containerName: FormItemRule; containerName: FormItemRule;
disabledFunctions: FormItemRule; disabledFunctions: FormItemRule;
leechExts: FormItemRule;
paramCommon: FormItemRule; paramCommon: FormItemRule;
paramComplexity: FormItemRule; paramComplexity: FormItemRule;
@ -465,4 +479,9 @@ export const Rules: CommonRule = {
trigger: 'blur', trigger: 'blur',
validator: checkDisableFunctions, validator: checkDisableFunctions,
}, },
leechExts: {
required: true,
trigger: 'blur',
validator: checkLeechExts,
},
}; };

View file

@ -160,6 +160,7 @@ const message = {
appName: 'Support English, numbers, - and _, length 2-30, and cannot start and end with -_', 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.', conatinerName: 'Supports letters, numbers, underscores, hyphens and dots, cannot end with hyphen- or dot.',
disableFunction: 'Only support letters and,', disableFunction: 'Only support letters and,',
leechExts: 'Only support letters, numbers and,',
}, },
res: { res: {
paramError: 'The request failed, please try again later!', paramError: 'The request failed, please try again later!',
@ -1375,6 +1376,16 @@ const message = {
editBasicAuthHelper: editBasicAuthHelper:
'The password is asymmetrically encrypted and cannot be echoed. Editing needs to reset the password', 'The password is asymmetrically encrypted and cannot be echoed. Editing needs to reset the password',
createPassword: 'Generate 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: { php: {
short_open_tag: 'Short tag support', short_open_tag: 'Short tag support',

View file

@ -163,6 +163,7 @@ const message = {
appName: '支持英文数字-和_,长度2-30,并且不能以-_开头和结尾', appName: '支持英文数字-和_,长度2-30,并且不能以-_开头和结尾',
conatinerName: '支持字母数字下划线连字符和点,不能以连字符-或点.结尾', conatinerName: '支持字母数字下划线连字符和点,不能以连字符-或点.结尾',
disableFunction: '仅支持字母和,', disableFunction: '仅支持字母和,',
leechExts: '件支持字母数字和,',
}, },
res: { res: {
paramError: '请求失败,请稍后重试!', paramError: '请求失败,请稍后重试!',
@ -1355,6 +1356,16 @@ const message = {
basicAuth: '密码访问', basicAuth: '密码访问',
editBasicAuthHelper: '密码为非对称加密无法回显编辑需要重新设置密码', editBasicAuthHelper: '密码为非对称加密无法回显编辑需要重新设置密码',
createPassword: '生成密码', createPassword: '生成密码',
antiLeech: '防盗链',
extends: '扩展名',
browserCache: '浏览器缓存',
leechLog: '记录防盗链日志',
accessDomain: '允许的域名',
leechReturn: '响应资源',
noneRef: '允许来源为空',
disable: '未启用',
disableLeechHelper: '是否禁用防盗链',
disableLeech: '禁用防盗链',
}, },
php: { php: {
short_open_tag: '短标签支持', short_open_tag: '短标签支持',

View file

@ -0,0 +1,189 @@
<template>
<div v-loading="loading">
<el-row :gutter="20" v-loading="loading">
<el-col :xs="24" :sm="18" :md="8" :lg="8" :xl="8">
<el-form :model="form" :rules="rules" ref="leechRef" label-position="right" label-width="120px">
<el-form-item :label="$t('website.enableOrNot')">
<el-switch v-model="form.enable" @change="changeEnable"></el-switch>
</el-form-item>
<div v-if="form.enable">
<el-form-item :label="$t('website.extends')" prop="extends">
<el-input v-model="form.extends" type="text"></el-input>
</el-form-item>
<el-form-item :label="$t('website.browserCache')" prop="cache">
<el-switch v-model="form.cache" />
</el-form-item>
<el-form-item :label="$t('website.cacheTime')" prop="cacheTime" v-if="form.cache">
<el-input v-model.number="form.cacheTime" maxlength="15">
<template #append>
<el-select v-model="form.cacheUint" style="width: 100px">
<el-option
v-for="(unit, index) in Units"
:key="index"
:label="unit.label"
:value="unit.value"
></el-option>
</el-select>
</template>
</el-input>
</el-form-item>
<el-form-item :label="$t('website.noneRef')" prop="noneRef">
<el-switch v-model="form.noneRef" />
</el-form-item>
<el-form-item :label="$t('website.accessDomain')" prop="serverNames">
<el-input
v-model="form.domains"
type="textarea"
:autosize="{ minRows: 6, maxRows: 20 }"
></el-input>
</el-form-item>
<el-form-item :label="$t('website.leechReturn')" prop="return">
<el-input v-model="form.return" type="text" :maxlength="35"></el-input>
</el-form-item>
</div>
</el-form>
<el-button type="primary" @click="submit(leechRef, true)" :disabled="loading" v-if="form.enable">
{{ $t('commons.button.save') }}
</el-button>
</el-col>
</el-row>
</div>
</template>
<script setup lang="ts">
import { GetAntiLeech, ListDomains, UpdateAntiLeech } from '@/api/modules/website';
import { Rules, checkNumberRange } from '@/global/form-rules';
import { FormInstance } from 'element-plus';
import { computed, onMounted, reactive } from 'vue';
import { ref } from 'vue';
import { Units } from '@/global/mimetype';
import { MsgSuccess } from '@/utils/message';
import i18n from '@/lang';
const loading = ref(false);
const props = defineProps({
id: {
type: Number,
default: 0,
},
});
const id = computed(() => {
return props.id;
});
const leechRef = ref<FormInstance>();
const resData = ref({
enable: false,
});
const form = reactive({
enable: false,
cache: true,
cacheTime: 30,
cacheUint: 'd',
extends: 'js,css,png,jpg,jpeg,gif,ico,bmp,swf,eot,svg,ttf,woff,woff2',
return: '404',
domains: '',
noneRef: true,
logEnable: false,
blocked: true,
serverNames: [],
websiteID: 0,
});
const rules = ref({
extends: [Rules.requiredInput, Rules.leechExts],
cacheTime: [Rules.requiredInput, checkNumberRange(1, 65535)],
return: [Rules.requiredInput],
});
const changeEnable = (enable: boolean) => {
if (enable) {
ListDomains(id.value)
.then((res) => {
const domains = res.data || [];
let serverNameStr = '';
for (const param of domains) {
serverNameStr = serverNameStr + param.domain + '\n';
}
form.domains = serverNameStr;
})
.finally(() => {});
}
if (resData.value.enable && !enable) {
ElMessageBox.confirm(i18n.global.t('website.disableLeechHelper'), i18n.global.t('website.disableLeech'), {
confirmButtonText: i18n.global.t('commons.button.confirm'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'error',
closeOnClickModal: false,
beforeClose: async (action, instance, done) => {
if (action !== 'confirm') {
form.enable = true;
done();
} else {
instance.confirmButtonLoading = true;
update(enable);
done();
}
},
}).then(() => {});
}
};
const search = async () => {
loading.value = true;
const res = await GetAntiLeech({ websiteID: id.value });
loading.value = false;
if (!res.data.enable) {
return;
}
resData.value = res.data;
form.blocked = res.data.blocked;
form.cache = res.data.cache;
form.enable = res.data.enable;
if (res.data.cache) {
form.cacheTime = res.data.cacheTime;
form.cacheUint = res.data.cacheUint;
}
form.extends = res.data.extends;
form.return = res.data.return;
form.logEnable = res.data.enable;
form.noneRef = res.data.noneRef;
const serverNames = res.data.serverNames;
let serverNameStr = '';
for (const param of serverNames) {
serverNameStr = serverNameStr + param + '\n';
}
form.domains = serverNameStr;
};
const submit = async (formEl: FormInstance | undefined, enable: boolean) => {
if (!formEl) return;
await formEl.validate((valid) => {
if (!valid) {
return;
}
update(enable);
});
};
const update = async (enable: boolean) => {
if (enable) {
form.serverNames = form.domains.split('\n');
}
form.enable = enable;
loading.value = true;
form.websiteID = id.value;
await UpdateAntiLeech(form)
.then(() => {
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
search();
})
.finally(() => {
loading.value = false;
});
};
onMounted(() => {
search();
});
</script>

View file

@ -129,7 +129,7 @@ const id = computed(() => {
return props.id; return props.id;
}); });
const httpsForm = ref<FormInstance>(); const httpsForm = ref<FormInstance>();
let form = reactive({ const form = reactive({
enable: false, enable: false,
websiteId: id.value, websiteId: id.value,
websiteSSLId: undefined, 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', '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'], SSLProtocol: ['TLSv1.3', 'TLSv1.2', 'TLSv1.1', 'TLSv1'],
}); });
let loading = ref(false); const loading = ref(false);
const ssls = ref(); const ssls = ref();
let websiteSSL = ref(); const websiteSSL = ref();
let rules = ref({ const rules = ref({
type: [Rules.requiredSelect], type: [Rules.requiredSelect],
privateKey: [Rules.requiredInput], privateKey: [Rules.requiredInput],
certificate: [Rules.requiredInput], certificate: [Rules.requiredInput],

View file

@ -24,8 +24,11 @@
<el-tab-pane :label="$t('website.rewrite')"> <el-tab-pane :label="$t('website.rewrite')">
<Rewrite :id="id" v-if="tabIndex == '7'"></Rewrite> <Rewrite :id="id" v-if="tabIndex == '7'"></Rewrite>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('website.antiLeech')">
<AntiLeech :id="id" v-if="tabIndex == '8'"></AntiLeech>
</el-tab-pane>
<el-tab-pane :label="$t('website.other')"> <el-tab-pane :label="$t('website.other')">
<Other :id="id" v-if="tabIndex == '8'"></Other> <Other :id="id" v-if="tabIndex == '9'"></Other>
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</template> </template>
@ -42,6 +45,7 @@ import SitePath from './site-folder/index.vue';
import Rewrite from './rewrite/index.vue'; import Rewrite from './rewrite/index.vue';
import Proxy from './proxy/index.vue'; import Proxy from './proxy/index.vue';
import AuthBasic from './auth-basic/index.vue'; import AuthBasic from './auth-basic/index.vue';
import AntiLeech from './anti-Leech/index.vue';
const props = defineProps({ const props = defineProps({
id: { id: {