2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 09:13:49 +08:00

Add redirect and advanced settings tabs to website editor (#1267)

* Initial plan

* Add redirect tab to website editor with drag-and-drop support

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* Add advanced settings tab with rate limiting and basic auth

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* Add htpasswd file support for basic auth (nginx and apache compatible)

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* Fix code review issues: prevent duplicate usernames in basic auth

Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>

* fix: bug

* fix: bug

* feat: 支持real ip设置

* feat: 支持real ip设置

* fix: lint

* fix: lint

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: devhaozi <115467771+devhaozi@users.noreply.github.com>
Co-authored-by: 耗子 <haozi@loli.email>
This commit is contained in:
Copilot
2026-01-22 06:32:13 +08:00
committed by GitHub
parent 0e958cbd78
commit a386dc3a2a
13 changed files with 821 additions and 109 deletions

View File

@@ -47,6 +47,14 @@ type WebsiteSetting struct {
Upstreams []types.Upstream `json:"upstreams"`
Proxies []types.Proxy `json:"proxies"`
// 重定向
Redirects []types.Redirect `json:"redirects"`
// 高级设置
RateLimit *types.RateLimit `json:"rate_limit"` // 限流限速配置
RealIP *types.RealIP `json:"real_ip"` // 真实 IP 配置
BasicAuth map[string]string `json:"basic_auth"` // 基本认证配置
// 自定义配置
CustomConfigs []WebsiteCustomConfig `json:"custom_configs"`
}

View File

@@ -78,11 +78,21 @@ func parseRedirectFile(filePath string) (*types.Redirect, error) {
redirectMatchPattern := regexp.MustCompile(`RedirectMatch\s+(\d+)\s+(\S+)\s+(\S+)`)
if matches := redirectMatchPattern.FindStringSubmatch(contentStr); matches != nil {
statusCode, _ := strconv.Atoi(matches[1])
to := matches[3]
keepURI := strings.Contains(to, "$1")
if keepURI {
to = strings.TrimSuffix(to, "$1")
}
// 还原 from 为简单路径格式
from := matches[2]
from = strings.TrimPrefix(from, "^")
from = strings.TrimSuffix(from, "(.*)$")
from = strings.TrimSuffix(from, "$")
return &types.Redirect{
Type: types.RedirectTypeURL,
From: matches[2],
To: matches[3],
KeepURI: strings.Contains(matches[3], "$1"),
From: from,
To: to,
KeepURI: keepURI,
StatusCode: statusCode,
}, nil
}
@@ -94,11 +104,16 @@ func parseRedirectFile(filePath string) (*types.Redirect, error) {
if matches := hostRewritePattern.FindStringSubmatch(contentStr); matches != nil {
statusCode, _ := strconv.Atoi(matches[3])
host := strings.ReplaceAll(matches[1], `\.`, ".")
to := matches[2]
keepURI := strings.Contains(to, "$1")
if keepURI {
to = strings.TrimSuffix(to, "$1")
}
return &types.Redirect{
Type: types.RedirectTypeHost,
From: host,
To: matches[2],
KeepURI: strings.Contains(matches[2], "$1"),
To: to,
KeepURI: keepURI,
StatusCode: statusCode,
}, nil
}

View File

@@ -541,24 +541,25 @@ func (v *baseVhost) RateLimit() *types.RateLimit {
return nil
}
rateLimit := &types.RateLimit{
Zone: make(map[string]string),
}
rateLimit := &types.RateLimit{}
// 获取速率限制值
rateValue := v.vhost.GetDirectiveValue("SetEnv")
if rateValue != "" {
rateLimit.Rate = rateValue
// 获取速率限制值 (SetEnv rate-limit 512)
args := v.vhost.GetDirectiveValues("SetEnv")
if len(args) >= 2 && args[0] == "rate-limit" {
_, _ = fmt.Sscanf(args[1], "%d", &rateLimit.Rate)
}
return rateLimit
}
func (v *baseVhost) SetRateLimit(limit *types.RateLimit) error {
// 设置 mod_ratelimit
v.vhost.SetDirective("SetOutputFilter", "RATE_LIMIT")
if limit.Rate != "" {
v.vhost.SetDirective("SetEnv", "rate-limit", limit.Rate)
// Apache mod_ratelimit 只支持流量限制,不支持并发连接限制
if limit.Rate > 0 {
v.vhost.SetDirective("SetOutputFilter", "RATE_LIMIT")
v.vhost.SetDirective("SetEnv", "rate-limit", fmt.Sprintf("%d", limit.Rate))
} else {
v.vhost.RemoveDirective("SetOutputFilter")
v.vhost.RemoveDirectives("SetEnv")
}
return nil
@@ -606,6 +607,58 @@ func (v *baseVhost) ClearBasicAuth() error {
return nil
}
func (v *baseVhost) RealIP() *types.RealIP {
// Apache 使用 mod_remoteip
// RemoteIPHeader X-Forwarded-For
// RemoteIPTrustedProxy 127.0.0.1
header := v.vhost.GetDirectiveValue("RemoteIPHeader")
if header == "" {
return nil
}
var from []string
for _, dir := range v.vhost.GetDirectives("RemoteIPTrustedProxy") {
if len(dir.Args) > 0 {
from = append(from, dir.Args[0])
}
}
return &types.RealIP{
From: from,
Header: header,
}
}
func (v *baseVhost) SetRealIP(realIP *types.RealIP) error {
// 清除现有配置
v.vhost.RemoveDirective("RemoteIPHeader")
v.vhost.RemoveDirectives("RemoteIPTrustedProxy")
if realIP == nil || (len(realIP.From) == 0 && realIP.Header == "") {
return nil
}
// 设置 RemoteIPHeader
if realIP.Header != "" {
v.vhost.SetDirective("RemoteIPHeader", realIP.Header)
}
// 设置 RemoteIPTrustedProxy
for _, ip := range realIP.From {
if ip != "" {
v.vhost.AddDirective("RemoteIPTrustedProxy", ip)
}
}
return nil
}
func (v *baseVhost) ClearRealIP() error {
v.vhost.RemoveDirective("RemoteIPHeader")
v.vhost.RemoveDirectives("RemoteIPTrustedProxy")
return nil
}
func (v *baseVhost) Redirects() []types.Redirect {
siteDir := filepath.Join(v.configDir, "site")
redirects, _ := parseRedirectFiles(siteDir)

View File

@@ -237,12 +237,13 @@ func (s *VhostTestSuite) TestRateLimit() {
s.Nil(s.vhost.RateLimit())
limit := &types.RateLimit{
Rate: "512",
Rate: 512,
}
s.NoError(s.vhost.SetRateLimit(limit))
got := s.vhost.RateLimit()
s.NotNil(got)
s.Equal(512, got.Rate)
s.NoError(s.vhost.ClearRateLimit())
s.Nil(s.vhost.RateLimit())

View File

@@ -40,15 +40,19 @@ var order = map[string]int{
"client_max_body_size": 100,
"client_body_buffer_size": 101,
"limit_except": 102,
"limit_req_zone": 103,
"limit_req": 104,
"limit_conn_zone": 105,
"limit_conn": 106,
"allow": 107,
"deny": 108,
"auth_basic": 109,
"auth_basic_user_file": 110,
"limit_rate": 102,
"limit_except": 103,
"limit_req_zone": 104,
"limit_req": 105,
"limit_conn_zone": 106,
"limit_conn": 107,
"allow": 108,
"deny": 109,
"auth_basic": 110,
"auth_basic_user_file": 111,
"set_real_ip_from": 112,
"real_ip_header": 113,
"real_ip_recursive": 114,
"ssl": 200,
"ssl_certificate": 201,

View File

@@ -66,11 +66,16 @@ func parseRedirectFile(filePath string) (*types.Redirect, error) {
urlPattern := regexp.MustCompile(`location\s*=\s*(\S+)\s*\{[^}]*return\s+(\d+)\s+([^;]+);`)
if matches := urlPattern.FindStringSubmatch(contentStr); matches != nil {
statusCode, _ := strconv.Atoi(matches[2])
to := strings.TrimSpace(matches[3])
keepURI := strings.Contains(to, "$request_uri")
if keepURI {
to = strings.TrimSuffix(to, "$request_uri")
}
return &types.Redirect{
Type: types.RedirectTypeURL,
From: matches[1],
To: strings.TrimSpace(matches[3]),
KeepURI: strings.Contains(matches[3], "$request_uri"),
To: to,
KeepURI: keepURI,
StatusCode: statusCode,
}, nil
}
@@ -79,11 +84,16 @@ func parseRedirectFile(filePath string) (*types.Redirect, error) {
hostPattern := regexp.MustCompile(`if\s*\(\s*\$host\s*=\s*"?([^")\s]+)"?\s*\)\s*\{[^}]*return\s+(\d+)\s+([^;]+);`)
if matches := hostPattern.FindStringSubmatch(contentStr); matches != nil {
statusCode, _ := strconv.Atoi(matches[2])
to := strings.TrimSpace(matches[3])
keepURI := strings.Contains(to, "$request_uri")
if keepURI {
to = strings.TrimSuffix(to, "$request_uri")
}
return &types.Redirect{
Type: types.RedirectTypeHost,
From: matches[1],
To: strings.TrimSpace(matches[3]),
KeepURI: strings.Contains(matches[3], "$request_uri"),
To: to,
KeepURI: keepURI,
StatusCode: statusCode,
}, nil
}
@@ -92,11 +102,16 @@ func parseRedirectFile(filePath string) (*types.Redirect, error) {
errorPattern := regexp.MustCompile(`error_page\s+404\s*=\s*@redirect_404;[^@]*location\s+@redirect_404\s*\{[^}]*return\s+(\d+)\s+([^;]+);`)
if matches := errorPattern.FindStringSubmatch(contentStr); matches != nil {
statusCode, _ := strconv.Atoi(matches[1])
to := strings.TrimSpace(matches[2])
keepURI := strings.Contains(to, "$request_uri")
if keepURI {
to = strings.TrimSuffix(to, "$request_uri")
}
return &types.Redirect{
Type: types.RedirectType404,
From: "",
To: strings.TrimSpace(matches[2]),
KeepURI: strings.Contains(matches[2], "$request_uri"),
To: to,
KeepURI: keepURI,
StatusCode: statusCode,
}, nil
}

View File

@@ -576,70 +576,84 @@ func (v *baseVhost) ClearSSL() error {
}
func (v *baseVhost) RateLimit() *types.RateLimit {
rate := ""
var perServer, perIP, rate int
// 解析 limit_rate 配置
directive, err := v.parser.FindOne("server.limit_rate")
if err == nil {
if len(v.parser.parameters2Slices(directive.GetParameters())) != 0 {
rate = directive.GetParameters()[0].GetValue()
params := v.parser.parameters2Slices(directive.GetParameters())
if len(params) > 0 {
// 解析 limit_rate 值,如 "512k" -> 512
rateStr := params[0]
rateStr = strings.TrimSuffix(rateStr, "k")
rateStr = strings.TrimSuffix(rateStr, "K")
_, _ = fmt.Sscanf(rateStr, "%d", &rate)
}
}
directives, _ := v.parser.Find("server.limit_conn")
var limitConn [][]string
for _, dir := range directives {
limitConn = append(limitConn, v.parser.parameters2Slices(dir.GetParameters()))
}
if rate == "" && len(limitConn) == 0 {
return nil
}
rateLimit := &types.RateLimit{
Rate: rate,
Zone: make(map[string]string),
}
// 解析 limit_conn 配置
for _, limit := range limitConn {
if len(limit) >= 2 {
// limit_conn zone connections
// 例如: limit_conn perip 10
rateLimit.Zone[limit[0]] = limit[1]
directives, _ := v.parser.Find("server.limit_conn")
for _, dir := range directives {
params := v.parser.parameters2Slices(dir.GetParameters())
if len(params) >= 2 {
var val int
_, _ = fmt.Sscanf(params[1], "%d", &val)
switch params[0] {
case "perserver":
perServer = val
case "perip":
perIP = val
}
}
}
return rateLimit
if perServer == 0 && perIP == 0 && rate == 0 {
return nil
}
return &types.RateLimit{
PerServer: perServer,
PerIP: perIP,
Rate: rate,
}
}
func (v *baseVhost) SetRateLimit(limit *types.RateLimit) error {
var limitConns [][]string
for zone, connections := range limit.Zone {
limitConns = append(limitConns, []string{zone, connections})
}
// 设置限速
// 设置限速 limit_rate
_ = v.parser.Clear("server.limit_rate")
if err := v.parser.Set("server", []*config.Directive{
{
Name: "limit_rate",
Parameters: []config.Parameter{{Value: limit.Rate}},
},
}); err != nil {
return err
}
// 设置并发连接数限制
_ = v.parser.Clear("server.limit_conn")
var directives []*config.Directive
for _, lim := range limitConns {
if len(lim) >= 2 {
directives = append(directives, &config.Directive{
Name: "limit_conn",
Parameters: v.parser.slices2Parameters(lim),
})
if limit.Rate > 0 {
if err := v.parser.Set("server", []*config.Directive{
{
Name: "limit_rate",
Parameters: []config.Parameter{{Value: fmt.Sprintf("%dk", limit.Rate)}},
},
}); err != nil {
return err
}
}
return v.parser.Set("server", directives)
// 设置并发连接数限制 limit_conn
_ = v.parser.Clear("server.limit_conn")
var directives []*config.Directive
if limit.PerServer > 0 {
directives = append(directives, &config.Directive{
Name: "limit_conn",
Parameters: []config.Parameter{{Value: "perserver"}, {Value: fmt.Sprintf("%d", limit.PerServer)}},
})
}
if limit.PerIP > 0 {
directives = append(directives, &config.Directive{
Name: "limit_conn",
Parameters: []config.Parameter{{Value: "perip"}, {Value: fmt.Sprintf("%d", limit.PerIP)}},
})
}
if len(directives) > 0 {
if err := v.parser.Set("server", directives); err != nil {
return err
}
}
return nil
}
func (v *baseVhost) ClearRateLimit() error {
@@ -706,6 +720,100 @@ func (v *baseVhost) ClearBasicAuth() error {
return nil
}
func (v *baseVhost) RealIP() *types.RealIP {
// 解析 set_real_ip_from 配置
var from []string
directives, _ := v.parser.Find("server.set_real_ip_from")
for _, dir := range directives {
params := v.parser.parameters2Slices(dir.GetParameters())
if len(params) > 0 {
from = append(from, params[0])
}
}
// 解析 real_ip_header 配置
header := ""
directive, err := v.parser.FindOne("server.real_ip_header")
if err == nil {
params := v.parser.parameters2Slices(directive.GetParameters())
if len(params) > 0 {
header = params[0]
}
}
// 解析 real_ip_recursive 配置
recursive := false
recursiveDir, err := v.parser.FindOne("server.real_ip_recursive")
if err == nil {
params := v.parser.parameters2Slices(recursiveDir.GetParameters())
if len(params) > 0 && params[0] == "on" {
recursive = true
}
}
if len(from) == 0 && header == "" {
return nil
}
return &types.RealIP{
From: from,
Header: header,
Recursive: recursive,
}
}
func (v *baseVhost) SetRealIP(realIP *types.RealIP) error {
// 清除现有配置
_ = v.parser.Clear("server.set_real_ip_from")
_ = v.parser.Clear("server.real_ip_header")
_ = v.parser.Clear("server.real_ip_recursive")
if realIP == nil || (len(realIP.From) == 0 && realIP.Header == "") {
return nil
}
var directives []*config.Directive
// 添加 set_real_ip_from 配置
for _, ip := range realIP.From {
if ip != "" {
directives = append(directives, &config.Directive{
Name: "set_real_ip_from",
Parameters: []config.Parameter{{Value: ip}},
})
}
}
// 添加 real_ip_header 配置
if realIP.Header != "" {
directives = append(directives, &config.Directive{
Name: "real_ip_header",
Parameters: []config.Parameter{{Value: realIP.Header}},
})
}
// 添加 real_ip_recursive 配置
if realIP.Recursive {
directives = append(directives, &config.Directive{
Name: "real_ip_recursive",
Parameters: []config.Parameter{{Value: "on"}},
})
}
if len(directives) > 0 {
return v.parser.Set("server", directives)
}
return nil
}
func (v *baseVhost) ClearRealIP() error {
_ = v.parser.Clear("server.set_real_ip_from")
_ = v.parser.Clear("server.real_ip_header")
_ = v.parser.Clear("server.real_ip_recursive")
return nil
}
func (v *baseVhost) Redirects() []types.Redirect {
siteDir := filepath.Join(v.configDir, "site")
redirects, _ := parseRedirectFiles(siteDir)

View File

@@ -249,16 +249,17 @@ func (s *VhostTestSuite) TestRateLimit() {
s.Nil(s.vhost.RateLimit())
limit := &types.RateLimit{
Rate: "512k",
Zone: map[string]string{
"perip": "10",
},
PerServer: 300,
PerIP: 25,
Rate: 512,
}
s.NoError(s.vhost.SetRateLimit(limit))
got := s.vhost.RateLimit()
s.NotNil(got)
s.Equal("512k", got.Rate)
s.Equal(300, got.PerServer)
s.Equal(25, got.PerIP)
s.Equal(512, got.Rate)
s.NoError(s.vhost.ClearRateLimit())
s.Nil(s.vhost.RateLimit())

View File

@@ -76,6 +76,13 @@ type Vhost interface {
// ClearBasicAuth 清除基本认证
ClearBasicAuth() error
// RealIP 取真实 IP 配置
RealIP() *RealIP
// SetRealIP 设置真实 IP 配置
SetRealIP(realIP *RealIP) error
// ClearRealIP 清除真实 IP 配置
ClearRealIP() error
// Config 取指定名称的配置内容
// type 可选值: "site", "shared"
Config(name string, typ string) string
@@ -162,9 +169,16 @@ type SSLConfig struct {
// RateLimit 限流限速配置
type RateLimit struct {
Rate string `json:"rate"` // 速率限制,如: "512k", "10r/s"
Concurrent int `json:"concurrent"` // 并发连接数限制
Zone map[string]string `json:"zone"` // 条件配置,如: map["perip"] = "10"
PerServer int `json:"per_server"` // 站点最大并发数 (limit_conn perserver X)
PerIP int `json:"per_ip"` // 单 IP 最大并发数 (limit_conn perip X)
Rate int `json:"rate"` // 流量限制,单位 KB (limit_rate Xk)
}
// RealIP 真实 IP 配置
type RealIP struct {
From []string `json:"from"` // 可信 IP 来源列表 (set_real_ip_from)
Header string `json:"header"` // 真实 IP 头 (real_ip_header),如: X-Real-IP, X-Forwarded-For
Recursive bool `json:"recursive"` // 递归搜索 (real_ip_recursive)
}
// IncludeFile 包含文件配置