mirror of
https://github.com/acepanel/panel.git
synced 2026-02-04 03:07:20 +08:00
feat: 反向代理支持更多设置
This commit is contained in:
@@ -17,6 +17,66 @@ import (
|
||||
// proxyFilePattern 匹配代理配置文件名 (200-299)
|
||||
var proxyFilePattern = regexp.MustCompile(`^(\d{3})-proxy\.conf$`)
|
||||
|
||||
// parseDurationFromNginx 从 Nginx 时间格式解析为 time.Duration
|
||||
func parseDurationFromNginx(valueStr, unit string) time.Duration {
|
||||
value, _ := strconv.Atoi(valueStr)
|
||||
switch unit {
|
||||
case "m":
|
||||
return time.Duration(value) * time.Minute
|
||||
case "h":
|
||||
return time.Duration(value) * time.Hour
|
||||
default:
|
||||
return time.Duration(value) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
// parseSizeToBytes 解析大小字符串为字节数
|
||||
func parseSizeToBytes(valueStr, unit string) int64 {
|
||||
value, _ := strconv.ParseInt(valueStr, 10, 64)
|
||||
switch strings.ToLower(unit) {
|
||||
case "k":
|
||||
return value * 1024
|
||||
case "m":
|
||||
return value * 1024 * 1024
|
||||
case "g":
|
||||
return value * 1024 * 1024 * 1024
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// formatBytesToNginx 格式化字节数为 Nginx 大小格式
|
||||
func formatBytesToNginx(bytes int64) string {
|
||||
if bytes == 0 {
|
||||
return "0"
|
||||
}
|
||||
if bytes%(1024*1024*1024) == 0 {
|
||||
return fmt.Sprintf("%dg", bytes/(1024*1024*1024))
|
||||
}
|
||||
if bytes%(1024*1024) == 0 {
|
||||
return fmt.Sprintf("%dm", bytes/(1024*1024))
|
||||
}
|
||||
if bytes%1024 == 0 {
|
||||
return fmt.Sprintf("%dk", bytes/1024)
|
||||
}
|
||||
return fmt.Sprintf("%d", bytes)
|
||||
}
|
||||
|
||||
// formatDurationToNginx 格式化 time.Duration 为 Nginx 时间格式
|
||||
func formatDurationToNginx(d time.Duration) string {
|
||||
if d == 0 {
|
||||
return "0s"
|
||||
}
|
||||
seconds := int(d.Seconds())
|
||||
if seconds%3600 == 0 {
|
||||
return fmt.Sprintf("%dh", seconds/3600)
|
||||
}
|
||||
if seconds%60 == 0 {
|
||||
return fmt.Sprintf("%dm", seconds/60)
|
||||
}
|
||||
return fmt.Sprintf("%ds", seconds)
|
||||
}
|
||||
|
||||
// parseProxyFiles 从 site 目录解析所有代理配置
|
||||
func parseProxyFiles(siteDir string) ([]types.Proxy, error) {
|
||||
entries, err := os.ReadDir(siteDir)
|
||||
@@ -227,6 +287,146 @@ func parseProxyFile(filePath string) (*types.Proxy, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 proxy_http_version
|
||||
httpVersionPattern := regexp.MustCompile(`proxy_http_version\s+([\d.]+);`)
|
||||
if hvm := httpVersionPattern.FindStringSubmatch(blockContent); hvm != nil {
|
||||
proxy.HTTPVersion = strings.TrimSpace(hvm[1])
|
||||
}
|
||||
|
||||
// 解析超时配置
|
||||
connectTimeoutPattern := regexp.MustCompile(`proxy_connect_timeout\s+(\d+)([smh]?);`)
|
||||
readTimeoutPattern := regexp.MustCompile(`proxy_read_timeout\s+(\d+)([smh]?);`)
|
||||
sendTimeoutPattern := regexp.MustCompile(`proxy_send_timeout\s+(\d+)([smh]?);`)
|
||||
|
||||
var timeout types.TimeoutConfig
|
||||
hasTimeout := false
|
||||
|
||||
if ctm := connectTimeoutPattern.FindStringSubmatch(blockContent); ctm != nil {
|
||||
timeout.Connect = parseDurationFromNginx(ctm[1], ctm[2])
|
||||
hasTimeout = true
|
||||
}
|
||||
if rtm := readTimeoutPattern.FindStringSubmatch(blockContent); rtm != nil {
|
||||
timeout.Read = parseDurationFromNginx(rtm[1], rtm[2])
|
||||
hasTimeout = true
|
||||
}
|
||||
if stm := sendTimeoutPattern.FindStringSubmatch(blockContent); stm != nil {
|
||||
timeout.Send = parseDurationFromNginx(stm[1], stm[2])
|
||||
hasTimeout = true
|
||||
}
|
||||
if hasTimeout {
|
||||
proxy.Timeout = &timeout
|
||||
}
|
||||
|
||||
// 解析重试配置
|
||||
nextUpstreamPattern := regexp.MustCompile(`proxy_next_upstream\s+([^;]+);`)
|
||||
nextUpstreamTriesPattern := regexp.MustCompile(`proxy_next_upstream_tries\s+(\d+);`)
|
||||
nextUpstreamTimeoutPattern := regexp.MustCompile(`proxy_next_upstream_timeout\s+(\d+)([smh]?);`)
|
||||
|
||||
var retry types.RetryConfig
|
||||
hasRetry := false
|
||||
|
||||
if num := nextUpstreamPattern.FindStringSubmatch(blockContent); num != nil {
|
||||
retry.Conditions = strings.Fields(num[1])
|
||||
hasRetry = true
|
||||
}
|
||||
if nutm := nextUpstreamTriesPattern.FindStringSubmatch(blockContent); nutm != nil {
|
||||
retry.Tries, _ = strconv.Atoi(nutm[1])
|
||||
hasRetry = true
|
||||
}
|
||||
if nutom := nextUpstreamTimeoutPattern.FindStringSubmatch(blockContent); nutom != nil {
|
||||
retry.Timeout = parseDurationFromNginx(nutom[1], nutom[2])
|
||||
hasRetry = true
|
||||
}
|
||||
if hasRetry {
|
||||
proxy.Retry = &retry
|
||||
}
|
||||
|
||||
// 解析 client_max_body_size
|
||||
clientMaxBodySizePattern := regexp.MustCompile(`client_max_body_size\s+(\d+)([kmgKMG]?);`)
|
||||
if cmbsm := clientMaxBodySizePattern.FindStringSubmatch(blockContent); cmbsm != nil {
|
||||
proxy.ClientMaxBodySize = parseSizeToBytes(cmbsm[1], cmbsm[2])
|
||||
}
|
||||
|
||||
// 解析 SSL 后端验证配置
|
||||
sslVerifyPattern := regexp.MustCompile(`proxy_ssl_verify\s+(on|off);`)
|
||||
sslTrustedCertPattern := regexp.MustCompile(`proxy_ssl_trusted_certificate\s+([^;]+);`)
|
||||
sslVerifyDepthPattern := regexp.MustCompile(`proxy_ssl_verify_depth\s+(\d+);`)
|
||||
|
||||
var sslBackend types.SSLBackendConfig
|
||||
hasSSLBackend := false
|
||||
|
||||
if svm := sslVerifyPattern.FindStringSubmatch(blockContent); svm != nil {
|
||||
sslBackend.Verify = svm[1] == "on"
|
||||
hasSSLBackend = true
|
||||
}
|
||||
if stcm := sslTrustedCertPattern.FindStringSubmatch(blockContent); stcm != nil {
|
||||
sslBackend.TrustedCertificate = strings.TrimSpace(stcm[1])
|
||||
hasSSLBackend = true
|
||||
}
|
||||
if svdm := sslVerifyDepthPattern.FindStringSubmatch(blockContent); svdm != nil {
|
||||
sslBackend.VerifyDepth, _ = strconv.Atoi(svdm[1])
|
||||
hasSSLBackend = true
|
||||
}
|
||||
if hasSSLBackend {
|
||||
proxy.SSLBackend = &sslBackend
|
||||
}
|
||||
|
||||
// 解析响应头配置
|
||||
hideHeaderPattern := regexp.MustCompile(`proxy_hide_header\s+([^;]+);`)
|
||||
addHeaderPattern := regexp.MustCompile(`add_header\s+(\S+)\s+"?([^";]+)"?(?:\s+always)?;`)
|
||||
|
||||
var responseHeaders types.ResponseHeaderConfig
|
||||
hasResponseHeaders := false
|
||||
|
||||
hideHeaderMatches := hideHeaderPattern.FindAllStringSubmatch(blockContent, -1)
|
||||
if len(hideHeaderMatches) > 0 {
|
||||
responseHeaders.Hide = []string{}
|
||||
for _, hhm := range hideHeaderMatches {
|
||||
responseHeaders.Hide = append(responseHeaders.Hide, strings.TrimSpace(hhm[1]))
|
||||
}
|
||||
hasResponseHeaders = true
|
||||
}
|
||||
|
||||
addHeaderMatches := addHeaderPattern.FindAllStringSubmatch(blockContent, -1)
|
||||
if len(addHeaderMatches) > 0 {
|
||||
responseHeaders.Add = make(map[string]string)
|
||||
for _, ahm := range addHeaderMatches {
|
||||
responseHeaders.Add[strings.TrimSpace(ahm[1])] = strings.TrimSpace(ahm[2])
|
||||
}
|
||||
hasResponseHeaders = true
|
||||
}
|
||||
if hasResponseHeaders {
|
||||
proxy.ResponseHeaders = &responseHeaders
|
||||
}
|
||||
|
||||
// 解析 IP 访问控制
|
||||
allowPattern := regexp.MustCompile(`allow\s+([^;]+);`)
|
||||
denyPattern := regexp.MustCompile(`deny\s+([^;]+);`)
|
||||
|
||||
var accessControl types.AccessControlConfig
|
||||
hasAccessControl := false
|
||||
|
||||
allowMatches := allowPattern.FindAllStringSubmatch(blockContent, -1)
|
||||
if len(allowMatches) > 0 {
|
||||
accessControl.Allow = []string{}
|
||||
for _, am := range allowMatches {
|
||||
accessControl.Allow = append(accessControl.Allow, strings.TrimSpace(am[1]))
|
||||
}
|
||||
hasAccessControl = true
|
||||
}
|
||||
|
||||
denyMatches := denyPattern.FindAllStringSubmatch(blockContent, -1)
|
||||
if len(denyMatches) > 0 {
|
||||
accessControl.Deny = []string{}
|
||||
for _, dm := range denyMatches {
|
||||
accessControl.Deny = append(accessControl.Deny, strings.TrimSpace(dm[1]))
|
||||
}
|
||||
hasAccessControl = true
|
||||
}
|
||||
if hasAccessControl {
|
||||
proxy.AccessControl = &accessControl
|
||||
}
|
||||
|
||||
return proxy, nil
|
||||
}
|
||||
|
||||
@@ -298,6 +498,21 @@ func generateProxyConfig(proxy types.Proxy) string {
|
||||
sb.WriteString(fmt.Sprintf("# Reverse proxy: %s -> %s\n", location, proxy.Pass))
|
||||
sb.WriteString(fmt.Sprintf("location %s {\n", location))
|
||||
|
||||
// IP 访问控制
|
||||
if proxy.AccessControl != nil {
|
||||
for _, ip := range proxy.AccessControl.Allow {
|
||||
sb.WriteString(fmt.Sprintf(" allow %s;\n", ip))
|
||||
}
|
||||
for _, ip := range proxy.AccessControl.Deny {
|
||||
sb.WriteString(fmt.Sprintf(" deny %s;\n", ip))
|
||||
}
|
||||
}
|
||||
|
||||
// 请求体大小限制
|
||||
if proxy.ClientMaxBodySize > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" client_max_body_size %s;\n", formatBytesToNginx(proxy.ClientMaxBodySize)))
|
||||
}
|
||||
|
||||
// resolver 配置
|
||||
if len(proxy.Resolver) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" resolver %s;\n", strings.Join(proxy.Resolver, " ")))
|
||||
@@ -307,7 +522,10 @@ func generateProxyConfig(proxy types.Proxy) string {
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf(" proxy_pass %s;\n", proxy.Pass))
|
||||
sb.WriteString(" proxy_http_version 1.1;\n")
|
||||
|
||||
// HTTP 协议版本
|
||||
httpVersion := lo.If(proxy.HTTPVersion != "", proxy.HTTPVersion).Else("1.1")
|
||||
sb.WriteString(fmt.Sprintf(" proxy_http_version %s;\n", httpVersion))
|
||||
|
||||
// Host 头
|
||||
host := lo.If(proxy.Host == "" || proxy.Host == "$proxy_host", "$proxy_host").ElseF(func() string {
|
||||
@@ -329,6 +547,43 @@ func generateProxyConfig(proxy types.Proxy) string {
|
||||
sb.WriteString(" proxy_ssl_session_reuse off;\n")
|
||||
sb.WriteString(" proxy_ssl_server_name on;\n")
|
||||
sb.WriteString(fmt.Sprintf(" proxy_ssl_name %s;\n", lo.If(proxy.SNI != "", proxy.SNI).Else("$proxy_host")))
|
||||
|
||||
// SSL 后端验证
|
||||
if proxy.SSLBackend != nil && proxy.SSLBackend.Verify {
|
||||
sb.WriteString(" proxy_ssl_verify on;\n")
|
||||
if proxy.SSLBackend.VerifyDepth > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_ssl_verify_depth %d;\n", proxy.SSLBackend.VerifyDepth))
|
||||
}
|
||||
if proxy.SSLBackend.TrustedCertificate != "" {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_ssl_trusted_certificate %s;\n", proxy.SSLBackend.TrustedCertificate))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 超时配置
|
||||
if proxy.Timeout != nil {
|
||||
if proxy.Timeout.Connect > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_connect_timeout %s;\n", formatDurationToNginx(proxy.Timeout.Connect)))
|
||||
}
|
||||
if proxy.Timeout.Read > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_read_timeout %s;\n", formatDurationToNginx(proxy.Timeout.Read)))
|
||||
}
|
||||
if proxy.Timeout.Send > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_send_timeout %s;\n", formatDurationToNginx(proxy.Timeout.Send)))
|
||||
}
|
||||
}
|
||||
|
||||
// 重试配置
|
||||
if proxy.Retry != nil {
|
||||
if len(proxy.Retry.Conditions) > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_next_upstream %s;\n", strings.Join(proxy.Retry.Conditions, " ")))
|
||||
}
|
||||
if proxy.Retry.Tries > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_next_upstream_tries %d;\n", proxy.Retry.Tries))
|
||||
}
|
||||
if proxy.Retry.Timeout > 0 {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_next_upstream_timeout %s;\n", formatDurationToNginx(proxy.Retry.Timeout)))
|
||||
}
|
||||
}
|
||||
|
||||
// Buffering 配置
|
||||
@@ -405,6 +660,19 @@ func generateProxyConfig(proxy types.Proxy) string {
|
||||
}
|
||||
}
|
||||
|
||||
// 响应头修改
|
||||
if proxy.ResponseHeaders != nil {
|
||||
// 隐藏响应头
|
||||
for _, header := range proxy.ResponseHeaders.Hide {
|
||||
sb.WriteString(fmt.Sprintf(" proxy_hide_header %s;\n", header))
|
||||
}
|
||||
// 添加响应头
|
||||
for name, value := range proxy.ResponseHeaders.Add {
|
||||
formattedValue := lo.If(strings.HasPrefix(value, "$"), value).Else("\"" + value + "\"")
|
||||
sb.WriteString(fmt.Sprintf(" add_header %s %s always;\n", name, formattedValue))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("}\n")
|
||||
|
||||
return sb.String()
|
||||
|
||||
@@ -31,18 +31,71 @@ type CacheConfig struct {
|
||||
Key string `form:"key" json:"key"`
|
||||
}
|
||||
|
||||
// TimeoutConfig 超时配置
|
||||
type TimeoutConfig struct {
|
||||
Connect time.Duration `form:"connect" json:"connect"` // proxy_connect_timeout,默认 60s
|
||||
Read time.Duration `form:"read" json:"read"` // proxy_read_timeout,默认 60s
|
||||
Send time.Duration `form:"send" json:"send"` // proxy_send_timeout,默认 60s
|
||||
}
|
||||
|
||||
// RetryConfig 重试配置
|
||||
type RetryConfig struct {
|
||||
// 触发重试的条件 (proxy_next_upstream)
|
||||
// 可选值: "error", "timeout", "invalid_header", "http_500", "http_502", "http_503", "http_504", "http_429", "non_idempotent", "off"
|
||||
Conditions []string `form:"conditions" json:"conditions"`
|
||||
|
||||
// 最大重试次数 (proxy_next_upstream_tries),0 表示不限制
|
||||
Tries int `form:"tries" json:"tries"`
|
||||
|
||||
// 重试超时时间 (proxy_next_upstream_timeout),0 表示不限制
|
||||
Timeout time.Duration `form:"timeout" json:"timeout"`
|
||||
}
|
||||
|
||||
// SSLBackendConfig SSL 后端验证配置
|
||||
type SSLBackendConfig struct {
|
||||
Verify bool `form:"verify" json:"verify"` // proxy_ssl_verify on/off
|
||||
TrustedCertificate string `form:"trusted_certificate" json:"trusted_certificate"` // proxy_ssl_trusted_certificate 路径
|
||||
VerifyDepth int `form:"verify_depth" json:"verify_depth"` // proxy_ssl_verify_depth,默认 1
|
||||
}
|
||||
|
||||
// ResponseHeaderConfig 响应头修改配置
|
||||
type ResponseHeaderConfig struct {
|
||||
// 隐藏的响应头 (proxy_hide_header)
|
||||
Hide []string `form:"hide" json:"hide"`
|
||||
|
||||
// 添加的响应头 (add_header),key -> value
|
||||
// 值可以包含变量,如 $upstream_cache_status
|
||||
Add map[string]string `form:"add" json:"add"`
|
||||
}
|
||||
|
||||
// AccessControlConfig IP 访问控制配置
|
||||
type AccessControlConfig struct {
|
||||
// 允许的 IP/CIDR 列表 (allow)
|
||||
Allow []string `form:"allow" json:"allow"`
|
||||
|
||||
// 拒绝的 IP/CIDR 列表 (deny)
|
||||
Deny []string `form:"deny" json:"deny"`
|
||||
}
|
||||
|
||||
// Proxy 反向代理配置
|
||||
type Proxy struct {
|
||||
Location string `form:"location" json:"location" validate:"required"` // 匹配路径,如: "/", "/api", "~ ^/api/v[0-9]+/"
|
||||
Pass string `form:"pass" json:"pass" validate:"required"` // 代理地址,如: "http://example.com", "http://backend"
|
||||
Host string `form:"host" json:"host"` // 代理 Host,如: "example.com"
|
||||
SNI string `form:"sni" json:"sni"` // 代理 SNI,如: "example.com"
|
||||
Cache *CacheConfig `form:"cache" json:"cache"` // 缓存配置,nil 表示禁用缓存
|
||||
Buffering bool `form:"buffering" json:"buffering"` // 是否启用缓冲
|
||||
Resolver []string `form:"resolver" json:"resolver"` // 自定义 DNS 解析器配置,如: ["8.8.8.8", "ipv6=off"]
|
||||
ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间,如: 5 * time.Second
|
||||
Headers map[string]string `form:"headers" json:"headers"` // 自定义请求头,如: map["X-Custom-Header"] = "value"
|
||||
Replaces map[string]string `form:"replaces" json:"replaces"` // 响应内容替换,如: map["/old"] = "/new"
|
||||
Location string `form:"location" json:"location" validate:"required"` // 匹配路径,如: "/", "/api", "~ ^/api/v[0-9]+/"
|
||||
Pass string `form:"pass" json:"pass" validate:"required"` // 代理地址,如: "http://example.com", "http://backend"
|
||||
Host string `form:"host" json:"host"` // 代理 Host,如: "example.com"
|
||||
SNI string `form:"sni" json:"sni"` // 代理 SNI,如: "example.com"
|
||||
Cache *CacheConfig `form:"cache" json:"cache"` // 缓存配置,nil 表示禁用缓存
|
||||
Buffering bool `form:"buffering" json:"buffering"` // 是否启用缓冲
|
||||
Resolver []string `form:"resolver" json:"resolver"` // 自定义 DNS 解析器配置,如: ["8.8.8.8", "ipv6=off"]
|
||||
ResolverTimeout time.Duration `form:"resolver_timeout" json:"resolver_timeout"` // DNS 解析超时时间,如: 5 * time.Second
|
||||
Headers map[string]string `form:"headers" json:"headers"` // 自定义请求头,如: map["X-Custom-Header"] = "value"
|
||||
Replaces map[string]string `form:"replaces" json:"replaces"` // 响应内容替换,如: map["/old"] = "/new"
|
||||
HTTPVersion string `form:"http_version" json:"http_version"` // HTTP 协议版本 (proxy_http_version),可选: "1.0", "1.1", "2"
|
||||
Timeout *TimeoutConfig `form:"timeout" json:"timeout"` // 超时配置
|
||||
Retry *RetryConfig `form:"retry" json:"retry"` // 重试配置
|
||||
ClientMaxBodySize int64 `form:"client_max_body_size" json:"client_max_body_size"` // 请求体大小限制 (client_max_body_size),单位字节,0 表示使用全局配置
|
||||
SSLBackend *SSLBackendConfig `form:"ssl_backend" json:"ssl_backend"` // SSL 后端验证配置
|
||||
ResponseHeaders *ResponseHeaderConfig `form:"response_headers" json:"response_headers"` // 响应头修改配置
|
||||
AccessControl *AccessControlConfig `form:"access_control" json:"access_control"` // IP 访问控制配置
|
||||
}
|
||||
|
||||
// Upstream 上游服务器配置
|
||||
|
||||
@@ -335,7 +335,14 @@ const addProxy = () => {
|
||||
resolver: [],
|
||||
resolver_timeout: 5 * 1000000000, // 5秒,以纳秒为单位
|
||||
headers: {},
|
||||
replaces: {}
|
||||
replaces: {},
|
||||
http_version: '1.1',
|
||||
timeout: null,
|
||||
retry: null,
|
||||
client_max_body_size: 0,
|
||||
ssl_backend: null,
|
||||
response_headers: null,
|
||||
access_control: null
|
||||
})
|
||||
}
|
||||
|
||||
@@ -408,6 +415,200 @@ const cacheMethodOptions = [
|
||||
{ label: 'POST', value: 'POST' }
|
||||
]
|
||||
|
||||
// HTTP 协议版本选项
|
||||
const httpVersionOptions = [
|
||||
{ label: 'HTTP/1.0', value: '1.0' },
|
||||
{ label: 'HTTP/1.1', value: '1.1' },
|
||||
{ label: 'HTTP/2', value: '2' }
|
||||
]
|
||||
|
||||
// 大小单位选项
|
||||
const sizeUnitOptions = [
|
||||
{ label: 'KB', value: 'k' },
|
||||
{ label: 'MB', value: 'm' },
|
||||
{ label: 'GB', value: 'g' }
|
||||
]
|
||||
|
||||
// 从字节解析为 {value, unit} 格式
|
||||
const parseSize = (bytes: number): { value: number; unit: string } => {
|
||||
if (!bytes || bytes <= 0) return { value: 0, unit: 'm' }
|
||||
|
||||
if (bytes >= 1024 * 1024 * 1024 && bytes % (1024 * 1024 * 1024) === 0) {
|
||||
return { value: bytes / (1024 * 1024 * 1024), unit: 'g' }
|
||||
}
|
||||
if (bytes >= 1024 * 1024 && bytes % (1024 * 1024) === 0) {
|
||||
return { value: bytes / (1024 * 1024), unit: 'm' }
|
||||
}
|
||||
if (bytes >= 1024 && bytes % 1024 === 0) {
|
||||
return { value: bytes / 1024, unit: 'k' }
|
||||
}
|
||||
return { value: bytes, unit: '' }
|
||||
}
|
||||
|
||||
// 将 {value, unit} 转换为字节
|
||||
const buildSize = (value: number, unit: string): number => {
|
||||
if (!value || value <= 0) return 0
|
||||
switch (unit) {
|
||||
case 'g':
|
||||
return value * 1024 * 1024 * 1024
|
||||
case 'm':
|
||||
return value * 1024 * 1024
|
||||
case 'k':
|
||||
return value * 1024
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// 重试条件选项
|
||||
const retryConditionOptions = [
|
||||
{ label: 'error', value: 'error' },
|
||||
{ label: 'timeout', value: 'timeout' },
|
||||
{ label: 'invalid_header', value: 'invalid_header' },
|
||||
{ label: 'http_500', value: 'http_500' },
|
||||
{ label: 'http_502', value: 'http_502' },
|
||||
{ label: 'http_503', value: 'http_503' },
|
||||
{ label: 'http_504', value: 'http_504' },
|
||||
{ label: 'http_429', value: 'http_429' },
|
||||
{ label: 'non_idempotent', value: 'non_idempotent' },
|
||||
{ label: 'off', value: 'off' }
|
||||
]
|
||||
|
||||
// 常见隐藏响应头选项
|
||||
const hideHeaderOptions = [
|
||||
{ label: 'X-Powered-By', value: 'X-Powered-By' },
|
||||
{ label: 'Server', value: 'Server' },
|
||||
{ label: 'X-AspNet-Version', value: 'X-AspNet-Version' },
|
||||
{ label: 'X-AspNetMvc-Version', value: 'X-AspNetMvc-Version' },
|
||||
{ label: 'X-Runtime', value: 'X-Runtime' },
|
||||
{ label: 'X-Version', value: 'X-Version' }
|
||||
]
|
||||
|
||||
// 创建默认超时配置
|
||||
const createDefaultTimeoutConfig = () => ({
|
||||
connect: 60 * SECOND,
|
||||
read: 60 * SECOND,
|
||||
send: 60 * SECOND
|
||||
})
|
||||
|
||||
// 创建默认重试配置
|
||||
const createDefaultRetryConfig = () => ({
|
||||
conditions: ['error', 'timeout'],
|
||||
tries: 0,
|
||||
timeout: 0
|
||||
})
|
||||
|
||||
// 创建默认 SSL 后端配置
|
||||
const createDefaultSSLBackendConfig = () => ({
|
||||
verify: false,
|
||||
trusted_certificate: '',
|
||||
verify_depth: 1
|
||||
})
|
||||
|
||||
// 创建默认响应头配置
|
||||
const createDefaultResponseHeadersConfig = () => ({
|
||||
hide: [],
|
||||
add: {}
|
||||
})
|
||||
|
||||
// 创建默认访问控制配置
|
||||
const createDefaultAccessControlConfig = () => ({
|
||||
allow: [],
|
||||
deny: []
|
||||
})
|
||||
|
||||
// 切换超时配置启用状态
|
||||
const toggleProxyTimeout = (proxy: any, enabled: boolean) => {
|
||||
if (enabled) {
|
||||
proxy.timeout = createDefaultTimeoutConfig()
|
||||
} else {
|
||||
proxy.timeout = null
|
||||
}
|
||||
}
|
||||
|
||||
// 切换重试配置启用状态
|
||||
const toggleProxyRetry = (proxy: any, enabled: boolean) => {
|
||||
if (enabled) {
|
||||
proxy.retry = createDefaultRetryConfig()
|
||||
} else {
|
||||
proxy.retry = null
|
||||
}
|
||||
}
|
||||
|
||||
// 切换 SSL 后端验证启用状态
|
||||
const toggleProxySSLBackend = (proxy: any, enabled: boolean) => {
|
||||
if (enabled) {
|
||||
proxy.ssl_backend = createDefaultSSLBackendConfig()
|
||||
} else {
|
||||
proxy.ssl_backend = null
|
||||
}
|
||||
}
|
||||
|
||||
// 切换响应头配置启用状态
|
||||
const toggleProxyResponseHeaders = (proxy: any, enabled: boolean) => {
|
||||
if (enabled) {
|
||||
proxy.response_headers = createDefaultResponseHeadersConfig()
|
||||
} else {
|
||||
proxy.response_headers = null
|
||||
}
|
||||
}
|
||||
|
||||
// 切换访问控制启用状态
|
||||
const toggleProxyAccessControl = (proxy: any, enabled: boolean) => {
|
||||
if (enabled) {
|
||||
proxy.access_control = createDefaultAccessControlConfig()
|
||||
} else {
|
||||
proxy.access_control = null
|
||||
}
|
||||
}
|
||||
|
||||
// 更新超时时间值
|
||||
const updateProxyTimeoutValue = (proxy: any, field: string, value: number) => {
|
||||
if (!proxy.timeout) return
|
||||
const parsed = parseDuration(proxy.timeout[field])
|
||||
proxy.timeout[field] = buildDuration(value, parsed.unit)
|
||||
}
|
||||
|
||||
// 更新超时时间单位
|
||||
const updateProxyTimeoutUnit = (proxy: any, field: string, unit: string) => {
|
||||
if (!proxy.timeout) return
|
||||
const parsed = parseDuration(proxy.timeout[field])
|
||||
proxy.timeout[field] = buildDuration(parsed.value, unit)
|
||||
}
|
||||
|
||||
// 更新请求体大小值
|
||||
const updateClientMaxBodySizeValue = (proxy: any, value: number) => {
|
||||
const parsed = parseSize(proxy.client_max_body_size)
|
||||
proxy.client_max_body_size = buildSize(value, parsed.unit || 'm')
|
||||
}
|
||||
|
||||
// 更新请求体大小单位
|
||||
const updateClientMaxBodySizeUnit = (proxy: any, unit: string) => {
|
||||
const parsed = parseSize(proxy.client_max_body_size)
|
||||
proxy.client_max_body_size = buildSize(parsed.value || 0, unit)
|
||||
}
|
||||
|
||||
// 更新重试超时值
|
||||
const updateRetryTimeoutValue = (proxy: any, value: number) => {
|
||||
if (!proxy.retry) return
|
||||
const parsed = parseDuration(proxy.retry.timeout)
|
||||
proxy.retry.timeout = buildDuration(value, parsed.unit)
|
||||
}
|
||||
|
||||
// 更新重试超时单位
|
||||
const updateRetryTimeoutUnit = (proxy: any, unit: string) => {
|
||||
if (!proxy.retry) return
|
||||
const parsed = parseDuration(proxy.retry.timeout)
|
||||
proxy.retry.timeout = buildDuration(parsed.value, unit)
|
||||
}
|
||||
|
||||
// 添加响应头
|
||||
const addResponseHeader = (proxy: any) => {
|
||||
if (!proxy.response_headers) return
|
||||
if (!proxy.response_headers.add) proxy.response_headers.add = {}
|
||||
proxy.response_headers.add['X-Custom-Header'] = 'value'
|
||||
}
|
||||
|
||||
// 删除代理
|
||||
const removeProxy = (index: number) => {
|
||||
if (setting.value.proxies) {
|
||||
@@ -1182,6 +1383,284 @@ const removeCustomConfig = (index: number) => {
|
||||
{{ $gettext('Add Replacement Rule') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
|
||||
<!-- 高级配置(仅 Nginx) -->
|
||||
<template v-if="isNginx">
|
||||
<n-divider>{{ $gettext('Advanced Settings') }}</n-divider>
|
||||
<n-grid :cols="24" :x-gap="16">
|
||||
<!-- HTTP 协议版本 -->
|
||||
<n-form-item-gi :span="8" :label="$gettext('HTTP Version')">
|
||||
<n-select
|
||||
v-model:value="proxy.http_version"
|
||||
:options="httpVersionOptions"
|
||||
:placeholder="$gettext('Select HTTP version')"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 请求体大小限制 -->
|
||||
<n-form-item-gi :span="8" :label="$gettext('Max Body Size')">
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
:value="parseSize(proxy.client_max_body_size).value"
|
||||
:min="0"
|
||||
flex-1
|
||||
:placeholder="$gettext('0 = global')"
|
||||
@update:value="(v: number) => updateClientMaxBodySizeValue(proxy, v)"
|
||||
/>
|
||||
<n-select
|
||||
:value="parseSize(proxy.client_max_body_size).unit || 'm'"
|
||||
:options="sizeUnitOptions"
|
||||
style="width: 80px"
|
||||
@update:value="(v: string) => updateClientMaxBodySizeUnit(proxy, v)"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 超时设置开关 -->
|
||||
<n-form-item-gi :span="8" :label="$gettext('Timeout Settings')">
|
||||
<n-switch
|
||||
:value="proxy.timeout !== null"
|
||||
@update:value="(v: boolean) => toggleProxyTimeout(proxy, v)"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<!-- 超时配置详情 -->
|
||||
<template v-if="proxy.timeout">
|
||||
<n-grid :cols="24" :x-gap="16">
|
||||
<n-form-item-gi :span="8" :label="$gettext('Connect Timeout')">
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
:value="parseDuration(proxy.timeout.connect).value"
|
||||
:min="1"
|
||||
flex-1
|
||||
@update:value="(v: number) => updateProxyTimeoutValue(proxy, 'connect', v)"
|
||||
/>
|
||||
<n-select
|
||||
:value="parseDuration(proxy.timeout.connect).unit"
|
||||
:options="timeUnitOptions"
|
||||
style="width: 100px"
|
||||
@update:value="(v: string) => updateProxyTimeoutUnit(proxy, 'connect', v)"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="8" :label="$gettext('Read Timeout')">
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
:value="parseDuration(proxy.timeout.read).value"
|
||||
:min="1"
|
||||
flex-1
|
||||
@update:value="(v: number) => updateProxyTimeoutValue(proxy, 'read', v)"
|
||||
/>
|
||||
<n-select
|
||||
:value="parseDuration(proxy.timeout.read).unit"
|
||||
:options="timeUnitOptions"
|
||||
style="width: 100px"
|
||||
@update:value="(v: string) => updateProxyTimeoutUnit(proxy, 'read', v)"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="8" :label="$gettext('Send Timeout')">
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
:value="parseDuration(proxy.timeout.send).value"
|
||||
:min="1"
|
||||
flex-1
|
||||
@update:value="(v: number) => updateProxyTimeoutValue(proxy, 'send', v)"
|
||||
/>
|
||||
<n-select
|
||||
:value="parseDuration(proxy.timeout.send).unit"
|
||||
:options="timeUnitOptions"
|
||||
style="width: 100px"
|
||||
@update:value="(v: string) => updateProxyTimeoutUnit(proxy, 'send', v)"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<n-grid :cols="24" :x-gap="16">
|
||||
<!-- 重试配置开关 -->
|
||||
<n-form-item-gi :span="8" :label="$gettext('Retry Settings')">
|
||||
<n-switch
|
||||
:value="proxy.retry !== null"
|
||||
@update:value="(v: boolean) => toggleProxyRetry(proxy, v)"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- SSL 后端验证开关(仅 https) -->
|
||||
<n-form-item-gi
|
||||
v-if="proxy.pass?.startsWith('https')"
|
||||
:span="8"
|
||||
:label="$gettext('SSL Backend Verify')"
|
||||
>
|
||||
<n-switch
|
||||
:value="proxy.ssl_backend !== null"
|
||||
@update:value="(v: boolean) => toggleProxySSLBackend(proxy, v)"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
|
||||
<!-- 响应头修改开关 -->
|
||||
<n-form-item-gi :span="8" :label="$gettext('Response Headers')">
|
||||
<n-switch
|
||||
:value="proxy.response_headers !== null"
|
||||
@update:value="(v: boolean) => toggleProxyResponseHeaders(proxy, v)"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<!-- 重试配置详情 -->
|
||||
<template v-if="proxy.retry">
|
||||
<n-grid :cols="24" :x-gap="16">
|
||||
<n-form-item-gi :span="12" :label="$gettext('Retry Conditions')">
|
||||
<n-select
|
||||
v-model:value="proxy.retry.conditions"
|
||||
:options="retryConditionOptions"
|
||||
multiple
|
||||
:placeholder="$gettext('Select retry conditions')"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" :label="$gettext('Max Tries')">
|
||||
<n-input-number
|
||||
v-model:value="proxy.retry.tries"
|
||||
:min="0"
|
||||
:placeholder="$gettext('0 = unlimited')"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" :label="$gettext('Retry Timeout')">
|
||||
<n-input-group>
|
||||
<n-input-number
|
||||
:value="parseDuration(proxy.retry.timeout).value"
|
||||
:min="0"
|
||||
flex-1
|
||||
:placeholder="$gettext('0 = unlimited')"
|
||||
@update:value="(v: number) => updateRetryTimeoutValue(proxy, v)"
|
||||
/>
|
||||
<n-select
|
||||
:value="parseDuration(proxy.retry.timeout).unit"
|
||||
:options="timeUnitOptions"
|
||||
style="width: 100px"
|
||||
@update:value="(v: string) => updateRetryTimeoutUnit(proxy, v)"
|
||||
/>
|
||||
</n-input-group>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<!-- SSL 后端验证详情 -->
|
||||
<template v-if="proxy.ssl_backend && proxy.pass?.startsWith('https')">
|
||||
<n-grid :cols="24" :x-gap="16">
|
||||
<n-form-item-gi :span="6" :label="$gettext('Enable Verify')">
|
||||
<n-switch v-model:value="proxy.ssl_backend.verify" />
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="6" :label="$gettext('Verify Depth')">
|
||||
<n-input-number
|
||||
v-model:value="proxy.ssl_backend.verify_depth"
|
||||
:min="1"
|
||||
:max="10"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" :label="$gettext('Trusted Certificate')">
|
||||
<n-input
|
||||
v-model:value="proxy.ssl_backend.trusted_certificate"
|
||||
:placeholder="$gettext('CA certificate path, e.g. /etc/ssl/certs/ca-certificates.crt')"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<!-- 响应头修改详情 -->
|
||||
<template v-if="proxy.response_headers">
|
||||
<n-grid :cols="24" :x-gap="16">
|
||||
<n-form-item-gi :span="12" :label="$gettext('Hide Headers')">
|
||||
<n-select
|
||||
v-model:value="proxy.response_headers.hide"
|
||||
:options="hideHeaderOptions"
|
||||
multiple
|
||||
filterable
|
||||
tag
|
||||
:placeholder="$gettext('Select or input headers to hide')"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" :label="$gettext('Add Headers')">
|
||||
<n-flex vertical :size="8" w-full>
|
||||
<n-flex
|
||||
v-for="(headerValue, headerName) in proxy.response_headers.add"
|
||||
:key="String(headerName)"
|
||||
:size="8"
|
||||
align="center"
|
||||
>
|
||||
<n-input
|
||||
:value="String(headerName)"
|
||||
:placeholder="$gettext('Header name')"
|
||||
flex-1
|
||||
@blur="
|
||||
(e: FocusEvent) => {
|
||||
const newName = (e.target as HTMLInputElement).value
|
||||
const oldName = String(headerName)
|
||||
if (newName && newName !== oldName) {
|
||||
proxy.response_headers.add[newName] =
|
||||
proxy.response_headers.add[oldName]
|
||||
delete proxy.response_headers.add[oldName]
|
||||
}
|
||||
}
|
||||
"
|
||||
/>
|
||||
<span flex-shrink-0>=</span>
|
||||
<n-input
|
||||
:value="String(headerValue)"
|
||||
:placeholder="$gettext('Header value')"
|
||||
flex-1
|
||||
@update:value="
|
||||
(v: string) => (proxy.response_headers.add[String(headerName)] = v)
|
||||
"
|
||||
/>
|
||||
<n-button
|
||||
type="error"
|
||||
secondary
|
||||
size="small"
|
||||
flex-shrink-0
|
||||
@click="delete proxy.response_headers.add[String(headerName)]"
|
||||
>
|
||||
{{ $gettext('Remove') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
<n-button dashed size="small" @click="addResponseHeader(proxy)">
|
||||
{{ $gettext('Add Response Header') }}
|
||||
</n-button>
|
||||
</n-flex>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
|
||||
<n-grid :cols="24" :x-gap="16">
|
||||
<!-- IP 访问控制开关 -->
|
||||
<n-form-item-gi :span="8" :label="$gettext('IP Access Control')">
|
||||
<n-switch
|
||||
:value="proxy.access_control !== null"
|
||||
@update:value="(v: boolean) => toggleProxyAccessControl(proxy, v)"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
|
||||
<!-- IP 访问控制详情 -->
|
||||
<template v-if="proxy.access_control">
|
||||
<n-grid :cols="24" :x-gap="16">
|
||||
<n-form-item-gi :span="12" :label="$gettext('Allow IPs')">
|
||||
<n-dynamic-tags
|
||||
v-model:value="proxy.access_control.allow"
|
||||
:placeholder="$gettext('IP or CIDR, e.g. 192.168.1.0/24')"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
<n-form-item-gi :span="12" :label="$gettext('Deny IPs')">
|
||||
<n-dynamic-tags
|
||||
v-model:value="proxy.access_control.deny"
|
||||
:placeholder="$gettext('IP or CIDR, e.g. all')"
|
||||
/>
|
||||
</n-form-item-gi>
|
||||
</n-grid>
|
||||
</template>
|
||||
</template>
|
||||
</n-form>
|
||||
</n-card>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user