feat: 网站增加真实 IP 配置 (#6488)
Some checks failed
sync2gitee / repo-sync (push) Failing after -9m1s

Refs https://github.com/1Panel-dev/1Panel/issues/1028
This commit is contained in:
zhengkunwang 2024-09-13 18:42:27 +08:00 committed by GitHub
parent 68433c922b
commit 417ad81aa4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 337 additions and 21 deletions

View file

@ -946,3 +946,47 @@ func (b *BaseApi) GetProxyCache(c *gin.Context) {
}
helper.SuccessWithData(c, res)
}
// @Tags Website
// @Summary Set Real IP
// @Description 设置真实IP
// @Accept json
// @Param request body request.WebsiteRealIP true "request"
// @Success 200
// @Security ApiKeyAuth
// @Router /websites/realip [post]
// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"修改 [domain] 网站真实IP配置 ","formatEN":"Modify the real IP configuration of [domain] website"}
func (b *BaseApi) SetRealIPConfig(c *gin.Context) {
var req request.WebsiteRealIP
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteService.SetRealIPConfig(req); err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithOutData(c)
}
// 写一个调用 GetRealIPConfig 的接口
// @Tags Website
// @Summary Get Real IP Config
// @Description 获取真实 IP 配置
// @Accept json
// @Param id path int true "id"
// @Success 200 {object} response.WebsiteRealIP
// @Security ApiKeyAuth
// @Router /websites/realip/config/{id} [get]
func (b *BaseApi) GetRealIPConfig(c *gin.Context) {
id, err := helper.GetParamID(c)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrBadRequest, constant.ErrTypeInternalServer, nil)
return
}
res, err := websiteService.GetRealIPConfig(id)
if err != nil {
helper.ErrorWithDetail(c, constant.CodeErrInternalServer, constant.ErrTypeInternalServer, err)
return
}
helper.SuccessWithData(c, res)
}

View file

@ -82,3 +82,5 @@ type NginxUpstreamServer struct {
}
var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}

View file

@ -266,3 +266,11 @@ type WebsiteLBUpdateFile struct {
Name string `json:"name" validate:"required"`
Content string `json:"content" validate:"required"`
}
type WebsiteRealIP struct {
WebsiteID uint `json:"websiteID" validate:"required"`
Open bool `json:"open"`
IPFrom string `json:"ipFrom"`
IPHeader string `json:"ipHeader"`
IPOther string `json:"ipOther"`
}

View file

@ -93,3 +93,11 @@ type WebsiteDirConfig struct {
type WebsiteHtmlRes struct {
Content string `json:"content"`
}
type WebsiteRealIP struct {
WebsiteID uint `json:"websiteID" validate:"required"`
Open bool `json:"open"`
IPFrom string `json:"ipFrom"`
IPHeader string `json:"ipHeader"`
IPOther string `json:"ipOther"`
}

View file

@ -10,6 +10,7 @@ import (
"encoding/pem"
"errors"
"fmt"
"net"
"os"
"path"
"reflect"
@ -108,6 +109,9 @@ type IWebsiteService interface {
UpdateLoadBalance(req request.WebsiteLBUpdate) error
UpdateLoadBalanceFile(req request.WebsiteLBUpdateFile) error
SetRealIPConfig(req request.WebsiteRealIP) error
GetRealIPConfig(websiteID uint) (*response.WebsiteRealIP, error)
ChangeGroup(group, newGroup uint) error
}
@ -1578,23 +1582,6 @@ func (w WebsiteService) OperateProxy(req request.WebsiteProxyConfig) (err error)
return updateNginxConfig(constant.NginxScopeServer, []dto.NginxParam{{Name: "include", Params: []string{nginxInclude}}}, &website)
}
func openProxyCache(website model.Website) error {
cacheDir := GetSitePath(website, SiteCacheDir)
fileOp := files.NewFileOp()
if !fileOp.Stat(cacheDir) {
_ = fileOp.CreateDir(cacheDir, 0755)
}
content, err := fileOp.GetContent(GetSitePath(website, SiteConf))
if err != nil {
return err
}
if strings.Contains(string(content), "proxy_cache_path") {
return nil
}
proxyCachePath := fmt.Sprintf("/www/sites/%s/cache levels=1:2 keys_zone=proxy_cache_zone_of_%s:5m max_size=1g inactive=24h", website.Alias, website.Alias)
return updateNginxConfig("", []dto.NginxParam{{Name: "proxy_cache_path", Params: []string{proxyCachePath}}}, &website)
}
func (w WebsiteService) UpdateProxyCache(req request.NginxProxyCacheUpdate) (err error) {
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
@ -2983,3 +2970,83 @@ func (w WebsiteService) UpdateLoadBalanceFile(req request.WebsiteLBUpdateFile) e
func (w WebsiteService) ChangeGroup(group, newGroup uint) error {
return websiteRepo.UpdateGroup(group, newGroup)
}
func (w WebsiteService) SetRealIPConfig(req request.WebsiteRealIP) error {
website, err := websiteRepo.GetFirst(commonRepo.WithByID(req.WebsiteID))
if err != nil {
return err
}
params := []dto.NginxParam{
{Name: "real_ip_recursive", Params: []string{"on"}},
{Name: "set_real_ip_from", Params: []string{}},
{Name: "real_ip_header", Params: []string{}},
}
if req.Open {
if err := deleteNginxConfig(constant.NginxScopeServer, params, &website); err != nil {
return err
}
params = []dto.NginxParam{
{Name: "real_ip_recursive", Params: []string{"on"}},
}
var ips []string
ipArray := strings.Split(req.IPFrom, "\n")
for _, ip := range ipArray {
if ip == "" {
continue
}
if parsedIP := net.ParseIP(ip); parsedIP == nil {
if _, _, err := net.ParseCIDR(ip); err != nil {
return buserr.New("ErrParseIP")
}
}
ips = append(ips, strings.TrimSpace(ip))
}
for _, ip := range ips {
params = append(params, dto.NginxParam{Name: "set_real_ip_from", Params: []string{ip}})
}
if req.IPHeader == "other" {
params = append(params, dto.NginxParam{Name: "real_ip_header", Params: []string{req.IPOther}})
} else {
params = append(params, dto.NginxParam{Name: "real_ip_header", Params: []string{req.IPHeader}})
}
return updateNginxConfig(constant.NginxScopeServer, params, &website)
}
return deleteNginxConfig(constant.NginxScopeServer, params, &website)
}
func (w WebsiteService) GetRealIPConfig(websiteID uint) (*response.WebsiteRealIP, error) {
website, err := websiteRepo.GetFirst(commonRepo.WithByID(websiteID))
if err != nil {
return nil, err
}
params, err := getNginxParamsByKeys(constant.NginxScopeServer, []string{"real_ip_recursive"}, &website)
if err != nil {
return nil, err
}
if len(params) == 0 || len(params[0].Params) == 0 {
return &response.WebsiteRealIP{Open: false}, nil
}
params, err = getNginxParamsByKeys(constant.NginxScopeServer, []string{"set_real_ip_from", "real_ip_header"}, &website)
if err != nil {
return nil, err
}
res := &response.WebsiteRealIP{
Open: true,
}
var ips []string
for _, param := range params {
if param.Name == "set_real_ip_from" {
ips = append(ips, param.Params...)
}
if param.Name == "real_ip_header" {
if _, ok := dto.RealIPKeys[param.Params[0]]; ok {
res.IPHeader = param.Params[0]
} else {
res.IPHeader = "other"
res.IPOther = param.Params[0]
}
}
}
res.IPFrom = strings.Join(ips, "\n")
return res, err
}

View file

@ -1199,3 +1199,20 @@ func GetSitePath(website model.Website, confType string) string {
}
return ""
}
func openProxyCache(website model.Website) error {
cacheDir := GetSitePath(website, SiteCacheDir)
fileOp := files.NewFileOp()
if !fileOp.Stat(cacheDir) {
_ = fileOp.CreateDir(cacheDir, 0755)
}
content, err := fileOp.GetContent(GetSitePath(website, SiteConf))
if err != nil {
return err
}
if strings.Contains(string(content), "proxy_cache_path") {
return nil
}
proxyCachePath := fmt.Sprintf("/www/sites/%s/cache levels=1:2 keys_zone=proxy_cache_zone_of_%s:5m max_size=1g inactive=24h", website.Alias, website.Alias)
return updateNginxConfig("", []dto.NginxParam{{Name: "proxy_cache_path", Params: []string{proxyCachePath}}}, &website)
}

View file

@ -74,5 +74,8 @@ func (a *WebsiteRouter) InitRouter(Router *gin.RouterGroup) {
websiteRouter.POST("/lbs/file", baseApi.UpdateLoadBalanceFile)
websiteRouter.POST("/php/version", baseApi.ChangePHPVersion)
websiteRouter.POST("/realip/config", baseApi.SetRealIPConfig)
websiteRouter.GET("/realip/config/:id", baseApi.GetRealIPConfig)
}
}

View file

@ -44,6 +44,7 @@ var repeatKeys = map[string]struct {
"include": {},
"sub_filter": {},
"add_header": {},
"set_real_ip_from": {},
}
func IsRepeatKey(key string) bool {

View file

@ -598,4 +598,11 @@ export namespace Website {
cacheExpire: number;
cacheExpireUnit: string;
}
export interface WebsiteRealIPConfig {
open: boolean;
ipFrom: string;
ipHeader: string;
ipOther: string;
}
}

View file

@ -311,3 +311,11 @@ export const UpdateCacheConfig = (req: Website.WebsiteCacheConfig) => {
export const GetCacheConfig = (id: number) => {
return http.get<Website.WebsiteCacheConfig>(`/websites/proxy/config/${id}`);
};
export const UpdateRealIPConfig = (req: Website.WebsiteRealIPConfig) => {
return http.post(`/websites/realip/config`, req);
};
export const GetRealIPConfig = (id: number) => {
return http.get<Website.WebsiteRealIPConfig>(`/websites/realip/config/${id}`);
};

View file

@ -2194,6 +2194,14 @@ const message = {
shareCaheHelper: 'Approximately 8000 cache objects can be stored per 1M of memory',
cacheLimitHelper: 'Old cache will be automatically deleted when the limit is exceeded',
cacheExpireJHelper: 'Cache will be deleted if it misses after the expiration time',
realIP: 'Real IP',
ipFrom: 'IP Source',
ipFromHelper:
"By configuring trusted IP sources, OpenResty will analyze IP information in HTTP headers, accurately identify and record visitors' real IP addresses, including in access logs",
ipFromExample1: "If the frontend is a tool like Frp, you can enter Frp's IP address, such as 127.0.0.1",
ipFromExample2: "If the frontend is a CDN, you can enter the CDN's IP address range",
ipFromExample3:
'If unsure, you can enter 0.0.0.0/0 (ipv4) ::/0 (ipv6) [Note: Allowing any source IP is not secure]',
},
php: {
short_open_tag: 'Short tag support',

View file

@ -2042,6 +2042,13 @@ const message = {
shareCaheHelper: '每1M內存可以存儲約8000個快取對象',
cacheLimitHelper: '超過限制會自動刪除舊的快取',
cacheExpireJHelper: '超出時間快取未命中將會被刪除',
realIP: '真實 IP',
ipFrom: 'IP 來源',
ipFromHelper:
'通過配置可信 IP 來源OpenResty 會分析 HTTP Header 中的 IP 資訊準確識別並記錄訪客的真實 IP 地址包括在存取日誌中',
ipFromExample1: '如果前端是 Frp 等工具可以填寫 Frp IP 地址類似 127.0.0.1',
ipFromExample2: '如果前端是 CDN可以填寫 CDN IP 地址段',
ipFromExample3: '如果不確定可以填 0.0.0.0/0ipv4 ::/0ipv6 [注意允許任意來源 IP 不安全]',
},
php: {
short_open_tag: '短標簽支持',

View file

@ -2043,6 +2043,13 @@ const message = {
shareCaheHelper: '每1M内存可以存储约8000个缓存对象',
cacheLimitHelper: '超过限制会自动删除旧的缓存',
cacheExpireJHelper: '超出时间缓存未命中将会被删除',
realIP: '真实 IP',
ipFrom: 'IP 来源',
ipFromHelper:
'通过配置可信 IP 来源OpenResty 会分析 HTTP Header 中的 IP 信息准确识别并记录访客的真实 IP 地址包括在访问日志中',
ipFromExample1: '如果前端是 Frp 等工具可以填写 Frp IP 地址类似 127.0.0.1',
ipFromExample2: '如果前端是 CDN可以填写 CDN IP 地址段',
ipFromExample3: '如果不确定可以填 0.0.0.0/0ipv4 ::/0ipv6 [注意允许任意来源 IP 不安全]',
},
php: {
short_open_tag: '短标签支持',

View file

@ -24,17 +24,20 @@
<el-tab-pane :label="'HTTPS'">
<HTTPS :id="id" v-if="tabIndex == '7'"></HTTPS>
</el-tab-pane>
<el-tab-pane :label="$t('website.realIP')">
<RealIP :id="id" v-if="tabIndex == '8'"></RealIP>
</el-tab-pane>
<el-tab-pane :label="$t('website.rewrite')">
<Rewrite :id="id" v-if="tabIndex == '8'"></Rewrite>
<Rewrite :id="id" v-if="tabIndex == '9'"></Rewrite>
</el-tab-pane>
<el-tab-pane :label="$t('website.antiLeech')">
<AntiLeech :id="id" v-if="tabIndex == '9'"></AntiLeech>
<AntiLeech :id="id" v-if="tabIndex == '10'"></AntiLeech>
</el-tab-pane>
<el-tab-pane :label="$t('website.redirect')">
<Redirect :id="id" v-if="tabIndex == '10'"></Redirect>
<Redirect :id="id" v-if="tabIndex == '11'"></Redirect>
</el-tab-pane>
<el-tab-pane :label="$t('website.other')">
<Other :id="id" v-if="tabIndex == '11'"></Other>
<Other :id="id" v-if="tabIndex == '12'"></Other>
</el-tab-pane>
<el-tab-pane
:label="'PHP'"
@ -61,6 +64,7 @@ import AntiLeech from './anti-Leech/index.vue';
import Redirect from './redirect/index.vue';
import LoadBalance from './load-balance/index.vue';
import PHP from './php/index.vue';
import RealIP from './real-ip/index.vue';
const props = defineProps({
website: {

View file

@ -0,0 +1,125 @@
<template>
<el-row :gutter="20" v-loading="loading">
<el-col :xs="24" :sm="14" :md="14" :lg="10" :xl="8">
<el-alert :closable="false">
{{ $t('website.ipFromHelper') }}
<div>
{{ $t('website.ipFromExample1') }}
</div>
<div>
{{ $t('website.ipFromExample2') }}
</div>
<div>
{{ $t('website.ipFromExample3') }}
</div>
</el-alert>
<el-form
v-loading="loading"
@submit.prevent
ref="realIPForm"
label-position="right"
label-width="100px"
:model="req"
:rules="rules"
:validate-on-rule-change="false"
>
<el-form-item :label="$t('commons.button.start')" prop="open">
<el-switch v-model="req.open"></el-switch>
</el-form-item>
<div v-if="req.open">
<el-form-item :label="$t('website.ipFrom')" prop="ipFrom">
<el-input
type="textarea"
:rows="10"
clearable
v-model="req.ipFrom"
:placeholder="$t('website.wafInputHelper')"
></el-input>
<span class="input-help">
{{ $t('website.wafInputHelper') }}
</span>
</el-form-item>
<el-form-item label="IP Header" prop="ipHeader">
<el-select v-model="req.ipHeader">
<el-option :label="$t('website.other')" key="other" value="other"></el-option>
<el-option
v-for="item in ['X-Forwarded-For', 'X-Real-IP', 'CF-Connecting-IP']"
:key="item"
:label="item"
:value="item"
/>
</el-select>
</el-form-item>
<el-form-item prop="ipOther" v-if="req.ipHeader === 'other'">
<el-input type="text" v-model.trim="req.ipOther" />
</el-form-item>
</div>
<el-form-item>
<el-button type="primary" @click="submit(realIPForm)" :loading="loading">
{{ $t('commons.button.save') }}
</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<script setup lang="ts">
import { GetRealIPConfig, UpdateRealIPConfig } from '@/api/modules/website';
import { Rules } from '@/global/form-rules';
import i18n from '@/lang';
import { MsgSuccess } from '@/utils/message';
import { FormInstance } from 'element-plus';
const loading = ref(false);
const realIPForm = ref<FormInstance>();
const props = defineProps({
id: {
type: Number,
default: 0,
},
});
const req = reactive({
websiteID: 0,
open: false,
ipFrom: '127.0.0.1',
ipHeader: 'X-Real-IP',
ipOther: '',
});
const rules = {
ipFrom: [Rules.requiredInput],
ipHeader: [Rules.requiredSelect],
ipOther: [Rules.requiredInput],
};
const get = () => {
GetRealIPConfig(props.id).then((res) => {
req.open = res.data.open;
if (res.data.open) {
req.ipFrom = res.data.ipFrom;
req.ipHeader = res.data.ipHeader;
req.ipOther = res.data.ipOther;
}
});
};
const submit = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate(async (valid) => {
if (!valid) {
return;
}
req.websiteID = props.id;
try {
await UpdateRealIPConfig(req);
MsgSuccess(i18n.global.t('commons.msg.updateSuccess'));
} catch (error) {}
});
};
onMounted(() => {
get();
});
</script>