2
0
mirror of https://github.com/acepanel/panel.git synced 2026-02-04 03:07:20 +08:00

feat: 反向代理支持更多设置

This commit is contained in:
2026-01-29 20:06:44 +08:00
parent 6458957650
commit 6a73f6e333
3 changed files with 812 additions and 12 deletions

View File

@@ -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()

View File

@@ -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 上游服务器配置

View File

@@ -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>