feat: Enhance anti-leech configuration with new options and improved UI (#10149)

* feat: enhance anti-leech configuration with new options and improved UI

* fix: remove redundant code
This commit is contained in:
KOMATA 2025-08-27 10:08:38 +08:00 committed by GitHub
parent 6f45b90520
commit e84aa49003
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 314 additions and 117 deletions

View file

@ -2230,7 +2230,7 @@ func (w WebsiteService) UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err e
}
}
}
if req.Enable {
if req.Enable || req.Cache {
exts := strings.Split(req.Extends, ",")
newDirective := components.Directive{
Name: "location",
@ -2240,49 +2240,84 @@ func (w WebsiteService) UpdateAntiLeech(req request.NginxAntiLeechUpdate) (err e
newBlock := &components.Block{}
newBlock.Directives = make([]components.IDirective, 0)
if req.Cache {
newBlock.Directives = append(newBlock.Directives, &components.Directive{
newBlock.AppendDirectives(&components.Directive{
Name: "expires",
Parameters: []string{strconv.Itoa(req.CacheTime) + req.CacheUint},
})
}
newBlock.Directives = append(newBlock.Directives, &components.Directive{
if !req.LogEnable {
newBlock.AppendDirectives(&components.Directive{
Name: "access_log",
Parameters: []string{"off"},
})
}
newBlock.AppendDirectives(&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, strings.Join(req.ServerNames, " "))
}
newBlock.Directives = append(newBlock.Directives, validDir)
if req.Enable {
validDir := &components.Directive{
Name: "valid_referers",
Parameters: []string{},
}
if req.NoneRef {
validDir.Parameters = append(validDir.Parameters, "none")
}
if req.Blocked {
validDir.Parameters = append(validDir.Parameters, "blocked")
}
if len(req.ServerNames) > 0 {
validDir.Parameters = append(validDir.Parameters, strings.Join(req.ServerNames, " "))
}
newBlock.AppendDirectives(validDir)
ifDir := &components.Directive{
Name: "if",
Parameters: []string{"($invalid_referer)"},
ifDir := &components.Directive{
Name: "if",
Parameters: []string{"($invalid_referer)"},
}
if !req.LogEnable {
ifDir.Block = &components.Block{
Directives: []components.IDirective{
&components.Directive{
Name: "access_log",
Parameters: []string{"off"},
},
&components.Directive{
Name: "return",
Parameters: []string{req.Return},
},
},
}
} else {
ifDir.Block = &components.Block{
Directives: []components.IDirective{
&components.Directive{
Name: "return",
Parameters: []string{req.Return},
},
},
}
}
newBlock.AppendDirectives(ifDir)
}
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)
if website.Type == constant.Deployment {
newBlock.Directives = append(newBlock.Directives, &components.Directive{
Name: "proxy_pass",
Parameters: []string{fmt.Sprintf("http://%s", website.Proxy)},
})
newBlock.AppendDirectives(
&components.Directive{
Name: "proxy_set_header",
Parameters: []string{"Host", "$host"},
},
&components.Directive{
Name: "proxy_set_header",
Parameters: []string{"X-Real-IP", "$remote_addr"},
},
&components.Directive{
Name: "proxy_set_header",
Parameters: []string{"X-Forwarded-For", "$proxy_add_x_forwarded_for"},
},
&components.Directive{
Name: "proxy_pass",
Parameters: []string{fmt.Sprintf("http://%s", website.Proxy)},
})
}
newDirective.Block = newBlock
index := -1
@ -2336,6 +2371,11 @@ func (w WebsiteService) GetAntiLeech(id uint) (*response.NginxAntiLeechRes, erro
}
lDirectives := location.GetBlock().GetDirectives()
for _, lDir := range lDirectives {
if lDir.GetName() == "access_log" {
if strings.Join(lDir.GetParameters(), "") == "off" {
res.LogEnable = false
}
}
if lDir.GetName() == "valid_referers" {
res.Enable = true
params := lDir.GetParameters()
@ -2360,11 +2400,6 @@ func (w WebsiteService) GetAntiLeech(id uint) (*response.NginxAntiLeechRes, erro
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" {

View file

@ -2416,6 +2416,16 @@ const message = {
disableLeech: 'Disable anti-leech',
ipv6: 'Listen IPv6',
leechReturnError: 'Please fill in the HTTP status code',
blockedRef: 'Allow non-standard Referer',
accessControl: 'Anti-leech control',
leechcacheControl: 'Cache control',
leechlogControl: 'Log control',
logEnableControl: 'Log static asset requests',
leechSpecialValidHelper:
"When 'Allow empty Referer' is enabled, requests without a Referer (direct access, etc.) are not blocked; enabling 'Allow non-standard Referer' allows any Referer that does not start with http/https (client requests, etc.).",
leechInvalidReturnHelper: 'HTTP status code returned after blocking hotlinking requests',
leechlogControlHelper:
'Logs static asset requests; usually disabled in production to avoid excessive, noisy logs',
selectAcme: 'Select Acme account',
imported: 'Created manually',
importType: 'Import type',

View file

@ -2329,6 +2329,16 @@ const message = {
disableLeech: '反リーチを無効にします',
ipv6: '緑',
leechReturnError: 'HTTPステータスコードを入力してください',
blockedRef: '非標準のリファラーを許可',
accessControl: '反リーチ制御',
leechcacheControl: 'キャッシュ制御',
leechlogControl: 'ログ制御',
logEnableControl: '静的アセットのリクエストを記録',
leechSpecialValidHelper:
'空のリファラーを許可を有効にするとリファラーのないリクエスト直接アクセス等はブロックされません非標準のリファラーを許可を有効にするとhttp/httpsで始まらないリファラークライアントからのリクエスト等をすべて許可します',
leechInvalidReturnHelper: 'ブロック後に返すHTTPステータスコード',
leechlogControlHelper:
'静的アセットのリクエストを記録します運用環境では過剰で無意味なログを避けるため通常は無効にします',
selectAcme: 'ACMEアカウントを選択します',
imported: '手動で作成されます',
importType: 'インポートタイプ',

View file

@ -2289,6 +2289,16 @@ const message = {
disableLeech: '링크 차단 비활성화',
ipv6: 'IPv6 수신 대기',
leechReturnError: 'HTTP 상태 코드를 입력하세요',
blockedRef: '비표준 참조 허용',
accessControl: '링크 차단 제어',
leechcacheControl: '캐시 제어',
leechlogControl: '로그 제어',
logEnableControl: '정적 리소스 요청 로그 기록',
leechSpecialValidHelper:
"'빈 참조 허용'을 활성화하면 리퍼러가 없는 요청(직접 접근 등)은 차단되지 않습니다. '비표준 참조 허용'을 활성화하면 http/https로 시작하지 않는 모든 리퍼러 요청(클라이언트 요청 등)을 허용합니다.",
leechInvalidReturnHelper: '차단된 요청에 대해 반환할 HTTP 상태 코드',
leechlogControlHelper:
'정적 리소스 요청을 기록합니다. 운영 환경에서는 과도하고 불필요한 로그를 피하기 위해 보통 비활성화합니다',
selectAcme: 'Acme 계정 선택',
imported: '수동으로 생성됨',
importType: '가져오기 유형',

View file

@ -2384,6 +2384,16 @@ const message = {
disableLeech: 'Matikan anti-leech',
ipv6: 'Dengar IPv6',
leechReturnError: 'Sila isikan kod status HTTP',
blockedRef: 'Benarkan referrer tidak standard',
accessControl: 'Kawalan anti-leech',
leechcacheControl: 'Kawalan cache',
leechlogControl: 'Kawalan log',
logEnableControl: 'Log permintaan aset statik',
leechSpecialValidHelper:
"Apabila 'Benarkan referrer kosong' didayakan, permintaan tanpa referrer (akses terus dan sebagainya) tidak akan disekat; mendayakan 'Benarkan referrer tidak standard' akan membenarkan mana-mana referrer yang tidak bermula dengan http/https (permintaan klien dan sebagainya).",
leechInvalidReturnHelper: 'Kod status HTTP yang dipulangkan selepas menyekat permintaan hotlink',
leechlogControlHelper:
'Merekod permintaan aset statik; biasanya dimatikan dalam produksi untuk mengelakkan log berlebihan yang tidak perlu',
selectAcme: 'Pilih akaun Acme',
imported: 'Dibuat secara manual',
importType: 'Jenis import',

View file

@ -2382,6 +2382,16 @@ const message = {
disableLeech: 'Desabilitar anti-leech',
ipv6: 'Ouvir IPv6',
leechReturnError: 'Por favor, preencha o código de status HTTP',
blockedRef: 'Permitir Referer não padrão',
accessControl: 'Controle anti-leech',
leechcacheControl: 'Controle de cache',
leechlogControl: 'Controle de log',
logEnableControl: 'Registrar solicitações de ativos estáticos',
leechSpecialValidHelper:
"Quando 'Permitir Referer vazio' estiver ativado, as solicitações sem Referer (acesso direto etc.) não serão bloqueadas; ao ativar 'Permitir Referer não padrão', qualquer Referer que não comece com http/https será permitido (solicitações de cliente etc.).",
leechInvalidReturnHelper: 'Código de status HTTP retornado após bloquear solicitações de hotlink',
leechlogControlHelper:
'Registra solicitações de ativos estáticos; geralmente desativado em produção para evitar logs excessivos e ruidosos',
selectAcme: 'Selecionar conta Acme',
imported: 'Criado manualmente',
importType: 'Tipo de importação',

View file

@ -2380,6 +2380,16 @@ const message = {
disableLeech: 'Отключить анти-лич',
ipv6: 'Прослушивать IPv6',
leechReturnError: 'Пожалуйста, заполните HTTP код статуса',
blockedRef: 'Разрешить нестандартный Referer',
accessControl: 'Управление анти-личем',
leechcacheControl: 'Управление кэшем',
leechlogControl: 'Управление журналом',
logEnableControl: 'Логировать запросы статических ресурсов',
leechSpecialValidHelper:
'При включённой опции «Разрешить пустой referrer» запросы без Referer (прямой доступ и т. п.) не блокируются; включение «Разрешить нестандартный Referer» пропускает любой Referer, не начинающийся с http/https (клиентские запросы и т. п.).',
leechInvalidReturnHelper: 'HTTPкод статуса, возвращаемый после блокировки хотлинкинга',
leechlogControlHelper:
'Записывает запросы к статическим ресурсам; в продакшене обычно отключают, чтобы избежать избыточных и шумных логов',
selectAcme: 'Выберите Acme аккаунт',
imported: 'Создан вручную',
importType: 'Тип импорта',

View file

@ -2444,6 +2444,16 @@ const message = {
disableLeech: 'Sömürü karşıtını devre dışı bırak',
ipv6: 'IPv6yı dinle',
leechReturnError: 'Lütfen HTTP durum kodunu doldurun',
blockedRef: 'Standart olmayan Referere izin ver',
accessControl: 'Sömürü karşıtı kontrol',
leechcacheControl: 'Önbellek kontrolü',
leechlogControl: 'Günlük kontrolü',
logEnableControl: 'Statik varlık isteklerini günlüğe al',
leechSpecialValidHelper:
"'Boş yönlendirme izni ver' etkinse yönlendiricisi olmayan istekler (doğrudan erişim vb.) engellenmez; 'Standart olmayan Referere izin ver' etkinse http/https ile başlamayan tüm Referer isteklerine (istemci istekleri vb.) izin verilir.",
leechInvalidReturnHelper: 'Hotlink isteklerini engelledikten sonra döndürülecek HTTP durum kodu',
leechlogControlHelper:
'Statik varlık isteklerini kaydeder; üretimde genellikle ırı ve gereksiz günlüklerden kaçınmak için kapatılır',
selectAcme: 'Acme hesabını seç',
imported: 'Manuel olarak oluşturuldu',
importType: 'İçe aktarma türü',

View file

@ -2255,6 +2255,15 @@ const message = {
disableLeech: '禁用防盜鏈',
ipv6: '監聽 IPV6',
leechReturnError: '請填寫 HTTP 狀態碼',
blockedRef: '允許非標準 Referer',
accessControl: '防盜鏈控制',
leechcacheControl: '快取控制',
leechlogControl: '日誌控制',
logEnableControl: '記錄靜態資源請求日誌',
leechSpecialValidHelper:
'啟用允許 Referer 為空不會阻止無 Referer 的請求直接訪問等啟用允許非標準 Referer會放行任何不以 http/https 開頭的 Referer 請求客戶端請求等',
leechInvalidReturnHelper: '攔截盜鏈請求後返回的 HTTP 狀態碼',
leechlogControlHelper: '記錄靜態資源的請求生產環境通常可關閉以避免過多無意義的日誌',
selectAcme: '選擇 Acme 賬號',
imported: '手動創建',
importType: '導入方式',

View file

@ -2239,12 +2239,21 @@ const message = {
leechLog: '记录防盗链日志',
accessDomain: '允许的域名',
leechReturn: '响应资源',
noneRef: '允许来源为空',
noneRef: '允许 Referer 为空',
disable: '未启用',
disableLeechHelper: '是否禁用防盗链',
disableLeech: '禁用防盗链',
ipv6: '监听 IPV6',
leechReturnError: '请填写 HTTP 状态码',
blockedRef: '允许非标准 Referer',
accessControl: '防盗链控制',
leechcacheControl: '缓存控制',
leechlogControl: '日志控制',
logEnableControl: '记录静态资源请求日志',
leechSpecialValidHelper:
'允许 Referer 为空启用时不会阻止无 Referer 的请求直接访问等启用非标准 Referer 时会放行任何不以 http/https 开头的 Referer 请求客户端请求等',
leechInvalidReturnHelper: '拦截盗链请求后返回的 HTTP 状态码',
leechlogControlHelper: '记录静态资源的请求生产环境通常可以关闭避免过多无意义的日志',
selectAcme: '选择 acme 账号',
imported: '手动创建',
importType: '导入方式',

View file

@ -1,63 +1,119 @@
<template>
<div v-loading="loading">
<el-row v-loading="loading">
<el-col :xs="24" :sm="18" :md="16" :lg="16" :xl="16">
<el-form
:model="form"
:rules="rules"
ref="leechRef"
label-position="right"
label-width="120px"
class="moblie-form"
>
<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"></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" class="w-s-button p-w-100">
<el-option
v-for="(unit, index) in Units"
<el-row :gutter="20" v-loading="loading">
<el-col :xs="24" :sm="18" :md="18" :lg="20" :xl="22">
<el-form :model="form" :rules="rules" ref="leechRef" label-position="right" class="moblie-form">
<el-form-item :label="$t('website.extends')" prop="extends" class="mt-2">
<el-input v-model="form.extends" class="p-w-600"></el-input>
</el-form-item>
<div class="mt-2">
<el-tabs type="border-card">
<el-tab-pane :label="$t('website.accessControl')">
<el-form-item :label="$t('website.enableOrNot')" prop="enable">
<el-switch v-model="form.enable" @change="changeEnable"></el-switch>
</el-form-item>
<template v-if="form.enable">
<el-divider />
<el-form-item :label="$t('website.accessDomain')" prop="domains">
<div class="domain-list-container">
<div
v-for="(_, index) in domainList"
:key="index"
:label="unit.label"
:value="unit.value"
class="flex items-center mb-2"
>
<el-input
v-model="domainList[index]"
@input="updateDomainsString"
class="flex-1 mr-2"
></el-input>
<el-button
type="danger"
size="small"
:icon="Delete"
@click="removeDomain(index)"
v-if="domainList.length > 1"
></el-button>
</div>
<el-button type="primary" size="small" :icon="Plus" @click="addDomain" plain>
{{ $t('commons.button.add') }}
</el-button>
</div>
</el-form-item>
<el-divider />
<el-row :gutter="15">
<el-col>
<el-form-item :label="$t('website.noneRef')" prop="noneRef">
<el-switch v-model="form.noneRef" />
</el-form-item>
</el-col>
<el-col>
<el-form-item :label="$t('website.blockedRef')" prop="blocked">
<el-switch v-model="form.blocked" />
</el-form-item>
</el-col>
<span class="input-help mt-1 mb-1">
{{ $t('website.leechSpecialValidHelper') }}
</span>
</el-row>
<el-divider />
<el-form-item :label="$t('website.leechReturn')" prop="return">
<el-select v-model="form.return" class="p-w-600">
<el-option
v-for="option in returnOptions"
:key="option.value"
:label="option.label"
:value="option.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="domains">
<el-input v-model="form.domains" type="textarea" :rows="6"></el-input>
</el-form-item>
<el-form-item :label="$t('website.leechReturn')" prop="return">
<el-input v-model="form.return" :maxlength="35"></el-input>
</el-form-item>
<el-form-item>
<el-button
type="primary"
@click="submit(leechRef, true)"
:disabled="loading"
v-if="form.enable"
>
{{ $t('commons.button.save') }}
</el-button>
</el-form-item>
</div>
</el-form>
</el-col>
</el-row>
</div>
</el-form-item>
<span class="input-help mt-1 mb-1">
{{ $t('website.leechInvalidReturnHelper') }}
</span>
</template>
</el-tab-pane>
<el-tab-pane :label="$t('website.leechcacheControl')">
<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" class="p-w-300">
<template #append>
<el-select v-model="form.cacheUint" class="w-s-button p-w-100">
<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>
<span class="input-help mt-2">{{ $t('website.browserCacheTimeHelper') }}</span>
</el-tab-pane>
<el-tab-pane :label="$t('website.leechlogControl')">
<el-form-item :label="$t('website.logEnableControl')" prop="logEnable">
<el-switch v-model="form.logEnable" />
</el-form-item>
<span class="input-help mt-2">{{ $t('website.leechlogControlHelper') }}</span>
</el-tab-pane>
</el-tabs>
</div>
<div class="flex items-center gap-4 mt-2">
<el-button type="primary" @click="submit(leechRef, form.enable)" :disabled="loading">
{{ $t('commons.button.save') }}
</el-button>
</div>
</el-form>
</el-col>
</el-row>
</template>
<script setup lang="ts">
@ -69,6 +125,7 @@ import { ref } from 'vue';
import { Units } from '@/global/mimetype';
import { MsgSuccess, MsgError } from '@/utils/message';
import i18n from '@/lang';
import { Plus, Delete } from '@element-plus/icons-vue';
const loading = ref(false);
const props = defineProps({
@ -84,17 +141,25 @@ const leechRef = ref<FormInstance>();
const resData = ref({
enable: false,
});
const returnOptions = [
{ label: '400 Bad Request', value: '400' },
{ label: '403 Forbidden', value: '403' },
{ label: '404 Not Found', value: '404' },
];
const domainList = ref(['']);
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',
extends: 'js,css,png,jpg,jpeg,gif,webp,webm,avif,ico,bmp,swf,eot,svg,ttf,woff,woff2',
return: '404',
domains: '',
noneRef: true,
logEnable: false,
blocked: true,
blocked: false,
serverNames: [],
websiteID: 0,
});
@ -106,6 +171,30 @@ const rules = ref({
domains: [Rules.requiredInput],
});
const addDomain = () => {
domainList.value.push('');
};
const removeDomain = (index: number) => {
domainList.value.splice(index, 1);
updateDomainsString();
};
const updateDomainsString = () => {
form.domains = domainList.value.filter((domain) => domain.trim() !== '').join('\n');
};
const initDomainList = (domainsStr: string) => {
if (domainsStr) {
domainList.value = domainsStr.split('\n').filter((domain) => domain.trim() !== '');
if (domainList.value.length === 0) {
domainList.value = [''];
}
} else {
domainList.value = [''];
}
};
const changeEnable = (enable: boolean) => {
if (enable) {
listDomains(id.value)
@ -116,27 +205,10 @@ const changeEnable = (enable: boolean) => {
serverNameStr = serverNameStr + param.domain + '\n';
}
form.domains = serverNameStr;
initDomainList(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 () => {
@ -156,7 +228,7 @@ const search = async () => {
}
form.extends = res.data.extends;
form.return = res.data.return;
form.logEnable = res.data.enable;
form.logEnable = res.data.logEnable;
form.noneRef = res.data.noneRef;
const serverNames = res.data.serverNames;
@ -165,6 +237,7 @@ const search = async () => {
serverNameStr = serverNameStr + param + '\n';
}
form.domains = serverNameStr;
initDomainList(serverNameStr);
};
const submit = async (formEl: FormInstance | undefined, enable: boolean) => {
@ -179,6 +252,7 @@ const submit = async (formEl: FormInstance | undefined, enable: boolean) => {
const update = async (enable: boolean) => {
if (enable) {
updateDomainsString();
form.serverNames = form.domains.split('\n');
}
if (!checkReturn()) {