mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-06 15:46:37 +08:00
feat(rate-limit): add default request rate limit configuration
- Support openresty_default_limit_req_per_ip in system_configs. - Add limit_req and limit_req_status 429 directive generation in openresty renderer. - Implement route-level limit_req_per_ip override and explicit disable. - Add frontend UI inputs and validation in rate limits tab config. - Update swagger API docs and changelog for v3.4.3-beta.3.
This commit is contained in:
@@ -22,6 +22,12 @@ sidebar: false
|
|||||||
|
|
||||||
## [unreleased]
|
## [unreleased]
|
||||||
|
|
||||||
|
## [v3.4.3-beta.3] - 2026-07-20
|
||||||
|
|
||||||
|
### 新增
|
||||||
|
|
||||||
|
- 安全性限流页「配置」页签新增请求频率限制配置:支持在全局配置单 IP 的请求频率限制(例如 10r/s、100r/m),触发限制时边缘节点返回 429 状态码;同时支持站点级别单独配置或通过 `-1` 显式禁用请求频率限制。
|
||||||
|
|
||||||
### 改进
|
### 改进
|
||||||
|
|
||||||
- 限流页请求压力图的 RPS 纵轴按可见时间窗口最高值的 1.5 倍动态缩放,拖动底部时间范围条时同步更新。
|
- 限流页请求压力图的 RPS 纵轴按可见时间窗口最高值的 1.5 倍动态缩放,拖动底部时间范围条时同步更新。
|
||||||
|
|||||||
+7
-1
@@ -5571,7 +5571,7 @@ const docTemplate = `{
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "趋势桶分钟数(1、5 或 60,默认 60)",
|
"description": "趋势桶分钟数(1、3、5 或 60,默认 60)",
|
||||||
"name": "bucket_minutes",
|
"name": "bucket_minutes",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
@@ -18272,6 +18272,9 @@ const docTemplate = `{
|
|||||||
"limit_rate": {
|
"limit_rate": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"limit_req_per_ip": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"origin_address": {
|
"origin_address": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -18388,6 +18391,9 @@ const docTemplate = `{
|
|||||||
"limit_rate": {
|
"limit_rate": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"limit_req_per_ip": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"origin_host": {
|
"origin_host": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+7
-1
@@ -5564,7 +5564,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"description": "趋势桶分钟数(1、5 或 60,默认 60)",
|
"description": "趋势桶分钟数(1、3、5 或 60,默认 60)",
|
||||||
"name": "bucket_minutes",
|
"name": "bucket_minutes",
|
||||||
"in": "query"
|
"in": "query"
|
||||||
}
|
}
|
||||||
@@ -18265,6 +18265,9 @@
|
|||||||
"limit_rate": {
|
"limit_rate": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"limit_req_per_ip": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"origin_address": {
|
"origin_address": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -18381,6 +18384,9 @@
|
|||||||
"limit_rate": {
|
"limit_rate": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"limit_req_per_ip": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"origin_host": {
|
"origin_host": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
|||||||
+5
-1
@@ -2928,6 +2928,8 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
limit_rate:
|
limit_rate:
|
||||||
type: string
|
type: string
|
||||||
|
limit_req_per_ip:
|
||||||
|
type: string
|
||||||
origin_address:
|
origin_address:
|
||||||
type: string
|
type: string
|
||||||
origin_host:
|
origin_host:
|
||||||
@@ -3005,6 +3007,8 @@ definitions:
|
|||||||
type: integer
|
type: integer
|
||||||
limit_rate:
|
limit_rate:
|
||||||
type: string
|
type: string
|
||||||
|
limit_req_per_ip:
|
||||||
|
type: string
|
||||||
origin_host:
|
origin_host:
|
||||||
type: string
|
type: string
|
||||||
origin_id:
|
origin_id:
|
||||||
@@ -7522,7 +7526,7 @@ paths:
|
|||||||
in: query
|
in: query
|
||||||
name: hours
|
name: hours
|
||||||
type: integer
|
type: integer
|
||||||
- description: 趋势桶分钟数(1、5 或 60,默认 60)
|
- description: 趋势桶分钟数(1、3、5 或 60,默认 60)
|
||||||
in: query
|
in: query
|
||||||
name: bucket_minutes
|
name: bucket_minutes
|
||||||
type: integer
|
type: integer
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const optionsQueryKey = ['openflare', 'options'] as const;
|
|||||||
const KEY_CONN_PER_SERVER = 'openresty_default_limit_conn_per_server';
|
const KEY_CONN_PER_SERVER = 'openresty_default_limit_conn_per_server';
|
||||||
const KEY_CONN_PER_IP = 'openresty_default_limit_conn_per_ip';
|
const KEY_CONN_PER_IP = 'openresty_default_limit_conn_per_ip';
|
||||||
const KEY_LIMIT_RATE = 'openresty_default_limit_rate';
|
const KEY_LIMIT_RATE = 'openresty_default_limit_rate';
|
||||||
|
const KEY_LIMIT_REQ_PER_IP = 'openresty_default_limit_req_per_ip';
|
||||||
|
|
||||||
const limitRatePattern = /^\d+(?:[kKmM])?$/;
|
const limitRatePattern = /^\d+(?:[kKmM])?$/;
|
||||||
|
|
||||||
@@ -32,12 +33,14 @@ type RateLimitFields = {
|
|||||||
openresty_default_limit_conn_per_server: string;
|
openresty_default_limit_conn_per_server: string;
|
||||||
openresty_default_limit_conn_per_ip: string;
|
openresty_default_limit_conn_per_ip: string;
|
||||||
openresty_default_limit_rate: string;
|
openresty_default_limit_rate: string;
|
||||||
|
openresty_default_limit_req_per_ip: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultFields: RateLimitFields = {
|
const defaultFields: RateLimitFields = {
|
||||||
openresty_default_limit_conn_per_server: '0',
|
openresty_default_limit_conn_per_server: '0',
|
||||||
openresty_default_limit_conn_per_ip: '0',
|
openresty_default_limit_conn_per_ip: '0',
|
||||||
openresty_default_limit_rate: '',
|
openresty_default_limit_rate: '',
|
||||||
|
openresty_default_limit_req_per_ip: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
function optionsToMap(options: Array<{ key: string; value: string }>) {
|
function optionsToMap(options: Array<{ key: string; value: string }>) {
|
||||||
@@ -55,6 +58,7 @@ function mapOptionsToFields(
|
|||||||
optionMap[KEY_CONN_PER_SERVER] ?? '0',
|
optionMap[KEY_CONN_PER_SERVER] ?? '0',
|
||||||
openresty_default_limit_conn_per_ip: optionMap[KEY_CONN_PER_IP] ?? '0',
|
openresty_default_limit_conn_per_ip: optionMap[KEY_CONN_PER_IP] ?? '0',
|
||||||
openresty_default_limit_rate: optionMap[KEY_LIMIT_RATE] ?? '',
|
openresty_default_limit_rate: optionMap[KEY_LIMIT_RATE] ?? '',
|
||||||
|
openresty_default_limit_req_per_ip: optionMap[KEY_LIMIT_REQ_PER_IP] ?? '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,6 +75,13 @@ function validateFields(fields: RateLimitFields) {
|
|||||||
if (rate && rate !== '0' && !limitRatePattern.test(rate)) {
|
if (rate && rate !== '0' && !limitRatePattern.test(rate)) {
|
||||||
throw new Error('限速格式不合法,请使用 512k、1m、纯数字,或留空关闭');
|
throw new Error('限速格式不合法,请使用 512k、1m、纯数字,或留空关闭');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const reqRate = fields.openresty_default_limit_req_per_ip.trim();
|
||||||
|
if (reqRate && reqRate !== '0' && !/^\d+r\/[sm]$/i.test(reqRate)) {
|
||||||
|
throw new Error(
|
||||||
|
'请求频率限制格式不合法,请使用类似 10r/s、100r/m,或留空关闭',
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeConnValue(value: string) {
|
function normalizeConnValue(value: string) {
|
||||||
@@ -119,6 +130,10 @@ export function ConfigTab() {
|
|||||||
key: KEY_LIMIT_RATE,
|
key: KEY_LIMIT_RATE,
|
||||||
value: normalizeRateValue(fields.openresty_default_limit_rate),
|
value: normalizeRateValue(fields.openresty_default_limit_rate),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: KEY_LIMIT_REQ_PER_IP,
|
||||||
|
value: normalizeRateValue(fields.openresty_default_limit_req_per_ip),
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
},
|
},
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
@@ -256,6 +271,25 @@ export function ConfigTab() {
|
|||||||
单请求带宽默认值,例如 512k 或 1m
|
单请求带宽默认值,例如 512k 或 1m
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div className='space-y-1.5'>
|
||||||
|
<Label className='text-xs text-muted-foreground'>
|
||||||
|
默认单 IP 请求频率限制
|
||||||
|
</Label>
|
||||||
|
<Input
|
||||||
|
value={fields.openresty_default_limit_req_per_ip}
|
||||||
|
placeholder='10r/s / 100r/m'
|
||||||
|
onChange={(e) =>
|
||||||
|
updateField(
|
||||||
|
'openresty_default_limit_req_per_ip',
|
||||||
|
e.target.value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className='h-9 text-xs'
|
||||||
|
/>
|
||||||
|
<p className='text-xs text-muted-foreground'>
|
||||||
|
单个 IP 请求频率默认值,例如 10r/s 或 100r/m,留空关闭
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -531,6 +531,7 @@ func diffOpenRestyOptionDetails(left openRestyConfigSnapshot, right openRestyCon
|
|||||||
appendIfChanged("OpenRestyDefaultLimitConnPerServer", fmt.Sprintf("%d", left.DefaultLimitConnPerServer), fmt.Sprintf("%d", right.DefaultLimitConnPerServer))
|
appendIfChanged("OpenRestyDefaultLimitConnPerServer", fmt.Sprintf("%d", left.DefaultLimitConnPerServer), fmt.Sprintf("%d", right.DefaultLimitConnPerServer))
|
||||||
appendIfChanged("OpenRestyDefaultLimitConnPerIP", fmt.Sprintf("%d", left.DefaultLimitConnPerIP), fmt.Sprintf("%d", right.DefaultLimitConnPerIP))
|
appendIfChanged("OpenRestyDefaultLimitConnPerIP", fmt.Sprintf("%d", left.DefaultLimitConnPerIP), fmt.Sprintf("%d", right.DefaultLimitConnPerIP))
|
||||||
appendIfChanged("OpenRestyDefaultLimitRate", left.DefaultLimitRate, right.DefaultLimitRate)
|
appendIfChanged("OpenRestyDefaultLimitRate", left.DefaultLimitRate, right.DefaultLimitRate)
|
||||||
|
appendIfChanged("OpenRestyDefaultLimitReqPerIP", left.DefaultLimitReqPerIP, right.DefaultLimitReqPerIP)
|
||||||
return changes
|
return changes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -582,5 +583,6 @@ func openRestyOptionKeys() []string {
|
|||||||
"OpenRestyDefaultLimitConnPerServer",
|
"OpenRestyDefaultLimitConnPerServer",
|
||||||
"OpenRestyDefaultLimitConnPerIP",
|
"OpenRestyDefaultLimitConnPerIP",
|
||||||
"OpenRestyDefaultLimitRate",
|
"OpenRestyDefaultLimitRate",
|
||||||
|
"OpenRestyDefaultLimitReqPerIP",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ type snapshotRoute struct {
|
|||||||
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
||||||
LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"`
|
LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"`
|
||||||
LimitRate string `json:"limit_rate,omitempty"`
|
LimitRate string `json:"limit_rate,omitempty"`
|
||||||
|
LimitReqPerIP string `json:"limit_req_per_ip,omitempty"`
|
||||||
CacheEnabled bool `json:"cache_enabled"`
|
CacheEnabled bool `json:"cache_enabled"`
|
||||||
CachePolicy string `json:"cache_policy,omitempty"`
|
CachePolicy string `json:"cache_policy,omitempty"`
|
||||||
CacheRules []string `json:"cache_rules,omitempty"`
|
CacheRules []string `json:"cache_rules,omitempty"`
|
||||||
@@ -138,6 +139,7 @@ type openRestyConfigSnapshot struct {
|
|||||||
DefaultLimitConnPerServer int `json:"default_limit_conn_per_server,omitempty"`
|
DefaultLimitConnPerServer int `json:"default_limit_conn_per_server,omitempty"`
|
||||||
DefaultLimitConnPerIP int `json:"default_limit_conn_per_ip,omitempty"`
|
DefaultLimitConnPerIP int `json:"default_limit_conn_per_ip,omitempty"`
|
||||||
DefaultLimitRate string `json:"default_limit_rate,omitempty"`
|
DefaultLimitRate string `json:"default_limit_rate,omitempty"`
|
||||||
|
DefaultLimitReqPerIP string `json:"default_limit_req_per_ip,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type snapshotDocument struct {
|
type snapshotDocument struct {
|
||||||
@@ -285,6 +287,7 @@ func buildSnapshotRoutes(ctx context.Context, routes []*model.ProxyRoute) ([]sna
|
|||||||
LimitConnPerServer: route.LimitConnPerServer,
|
LimitConnPerServer: route.LimitConnPerServer,
|
||||||
LimitConnPerIP: route.LimitConnPerIP,
|
LimitConnPerIP: route.LimitConnPerIP,
|
||||||
LimitRate: route.LimitRate,
|
LimitRate: route.LimitRate,
|
||||||
|
LimitReqPerIP: route.LimitReqPerIP,
|
||||||
CacheEnabled: route.CacheEnabled,
|
CacheEnabled: route.CacheEnabled,
|
||||||
CachePolicy: route.CachePolicy,
|
CachePolicy: route.CachePolicy,
|
||||||
CacheRules: cacheRules,
|
CacheRules: cacheRules,
|
||||||
@@ -548,10 +551,14 @@ func buildOpenRestyConfigSnapshot(ctx context.Context) openRestyConfigSnapshot {
|
|||||||
DefaultLimitConnPerServer: getNonNegIntConfig(model.ConfigKeyOpenRestyDefaultLimitConnPerServer, 0),
|
DefaultLimitConnPerServer: getNonNegIntConfig(model.ConfigKeyOpenRestyDefaultLimitConnPerServer, 0),
|
||||||
DefaultLimitConnPerIP: getNonNegIntConfig(model.ConfigKeyOpenRestyDefaultLimitConnPerIP, 0),
|
DefaultLimitConnPerIP: getNonNegIntConfig(model.ConfigKeyOpenRestyDefaultLimitConnPerIP, 0),
|
||||||
DefaultLimitRate: strings.ToLower(strings.TrimSpace(getStringConfig(model.ConfigKeyOpenRestyDefaultLimitRate, ""))),
|
DefaultLimitRate: strings.ToLower(strings.TrimSpace(getStringConfig(model.ConfigKeyOpenRestyDefaultLimitRate, ""))),
|
||||||
|
DefaultLimitReqPerIP: strings.ToLower(strings.TrimSpace(getStringConfig(model.ConfigKeyOpenRestyDefaultLimitReqPerIP, ""))),
|
||||||
}
|
}
|
||||||
if snapshot.DefaultLimitRate == "0" {
|
if snapshot.DefaultLimitRate == "0" {
|
||||||
snapshot.DefaultLimitRate = ""
|
snapshot.DefaultLimitRate = ""
|
||||||
}
|
}
|
||||||
|
if snapshot.DefaultLimitReqPerIP == "0" {
|
||||||
|
snapshot.DefaultLimitReqPerIP = ""
|
||||||
|
}
|
||||||
snapshot.CachePath = normalizeProxyCachePathForSnapshot(snapshot.CacheEnabled, snapshot.CachePath)
|
snapshot.CachePath = normalizeProxyCachePathForSnapshot(snapshot.CacheEnabled, snapshot.CachePath)
|
||||||
return snapshot
|
return snapshot
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ var openRestyOptionValidators = map[string]func(key, value string) error{
|
|||||||
model.ConfigKeyOpenRestyDefaultLimitConnPerServer: validateNonNegativeIntegerOption,
|
model.ConfigKeyOpenRestyDefaultLimitConnPerServer: validateNonNegativeIntegerOption,
|
||||||
model.ConfigKeyOpenRestyDefaultLimitConnPerIP: validateNonNegativeIntegerOption,
|
model.ConfigKeyOpenRestyDefaultLimitConnPerIP: validateNonNegativeIntegerOption,
|
||||||
model.ConfigKeyOpenRestyDefaultLimitRate: validateOpenRestyDefaultLimitRate,
|
model.ConfigKeyOpenRestyDefaultLimitRate: validateOpenRestyDefaultLimitRate,
|
||||||
|
model.ConfigKeyOpenRestyDefaultLimitReqPerIP: validateOpenRestyDefaultLimitReqPerIP,
|
||||||
}
|
}
|
||||||
|
|
||||||
var openRestyDefaultLimitRatePattern = regexp.MustCompile(`^\d+[kKmM]?$`)
|
var openRestyDefaultLimitRatePattern = regexp.MustCompile(`^\d+[kKmM]?$`)
|
||||||
@@ -194,3 +195,15 @@ func validateOpenRestyDefaultLimitRate(key, trimmed string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var openRestyDefaultLimitReqPerIPPattern = regexp.MustCompile(`^\d+r/[sm]$`)
|
||||||
|
|
||||||
|
func validateOpenRestyDefaultLimitReqPerIP(key, trimmed string) error {
|
||||||
|
if trimmed == "" || trimmed == "0" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !openRestyDefaultLimitReqPerIPPattern.MatchString(strings.ToLower(trimmed)) {
|
||||||
|
return fmt.Errorf("%s 格式不合法,请输入类似 10r/s、100r/m,或留空关闭", key)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func populateProxyRouteFields(
|
|||||||
upstreams []string,
|
upstreams []string,
|
||||||
originHost, cachePolicy string,
|
originHost, cachePolicy string,
|
||||||
limitConnPerServer, limitConnPerIP int,
|
limitConnPerServer, limitConnPerIP int,
|
||||||
limitRate, upstreamType string,
|
limitRate, limitReqPerIP, upstreamType string,
|
||||||
) {
|
) {
|
||||||
route.SiteName = siteName
|
route.SiteName = siteName
|
||||||
route.OriginID = originID
|
route.OriginID = originID
|
||||||
@@ -103,6 +103,7 @@ func populateProxyRouteFields(
|
|||||||
route.LimitConnPerServer = limitConnPerServer
|
route.LimitConnPerServer = limitConnPerServer
|
||||||
route.LimitConnPerIP = limitConnPerIP
|
route.LimitConnPerIP = limitConnPerIP
|
||||||
route.LimitRate = limitRate
|
route.LimitRate = limitRate
|
||||||
|
route.LimitReqPerIP = limitReqPerIP
|
||||||
route.CacheEnabled = input.CacheEnabled
|
route.CacheEnabled = input.CacheEnabled
|
||||||
route.CachePolicy = normalizeCachePolicy(input.CacheEnabled, cachePolicy)
|
route.CachePolicy = normalizeCachePolicy(input.CacheEnabled, cachePolicy)
|
||||||
route.CacheRules = jsonFields.cacheRulesJSON
|
route.CacheRules = jsonFields.cacheRulesJSON
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
|
|
||||||
var proxyHeaderKeyPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
var proxyHeaderKeyPattern = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
|
||||||
var proxyRouteLimitRatePattern = regexp.MustCompile(`^\d+[kKmM]?$`)
|
var proxyRouteLimitRatePattern = regexp.MustCompile(`^\d+[kKmM]?$`)
|
||||||
|
var proxyRouteLimitReqPattern = regexp.MustCompile(`^\d+r/[sm]$`)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
proxyRouteCachePolicyStatic = "static"
|
proxyRouteCachePolicyStatic = "static"
|
||||||
@@ -349,6 +350,20 @@ func normalizeProxyRouteLimitRate(raw string) (string, error) {
|
|||||||
return normalized, nil
|
return normalized, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeProxyRouteLimitReqPerIP(raw string) (string, error) {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(raw))
|
||||||
|
if normalized == "" || normalized == "0" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
if normalized == "-1" {
|
||||||
|
return "-1", nil
|
||||||
|
}
|
||||||
|
if !proxyRouteLimitReqPattern.MatchString(normalized) {
|
||||||
|
return "", errors.New("请求频率格式不合法,请使用类似 10r/s、100r/m,或 -1 禁用")
|
||||||
|
}
|
||||||
|
return normalized, nil
|
||||||
|
}
|
||||||
|
|
||||||
func hasStructuredOriginInput(input Input) bool {
|
func hasStructuredOriginInput(input Input) bool {
|
||||||
return (input.OriginID != nil && *input.OriginID != 0) ||
|
return (input.OriginID != nil && *input.OriginID != 0) ||
|
||||||
strings.TrimSpace(input.OriginScheme) != "" ||
|
strings.TrimSpace(input.OriginScheme) != "" ||
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ type Input struct {
|
|||||||
LimitConnPerServer int `json:"limit_conn_per_server"`
|
LimitConnPerServer int `json:"limit_conn_per_server"`
|
||||||
LimitConnPerIP int `json:"limit_conn_per_ip"`
|
LimitConnPerIP int `json:"limit_conn_per_ip"`
|
||||||
LimitRate string `json:"limit_rate"`
|
LimitRate string `json:"limit_rate"`
|
||||||
|
LimitReqPerIP string `json:"limit_req_per_ip"`
|
||||||
CacheEnabled bool `json:"cache_enabled"`
|
CacheEnabled bool `json:"cache_enabled"`
|
||||||
CachePolicy string `json:"cache_policy"`
|
CachePolicy string `json:"cache_policy"`
|
||||||
CacheRules []string `json:"cache_rules"`
|
CacheRules []string `json:"cache_rules"`
|
||||||
@@ -72,6 +73,7 @@ type View struct {
|
|||||||
LimitConnPerServer int `json:"limit_conn_per_server"`
|
LimitConnPerServer int `json:"limit_conn_per_server"`
|
||||||
LimitConnPerIP int `json:"limit_conn_per_ip"`
|
LimitConnPerIP int `json:"limit_conn_per_ip"`
|
||||||
LimitRate string `json:"limit_rate"`
|
LimitRate string `json:"limit_rate"`
|
||||||
|
LimitReqPerIP string `json:"limit_req_per_ip"`
|
||||||
CacheEnabled bool `json:"cache_enabled"`
|
CacheEnabled bool `json:"cache_enabled"`
|
||||||
CachePolicy string `json:"cache_policy"`
|
CachePolicy string `json:"cache_policy"`
|
||||||
CacheRules string `json:"cache_rules"`
|
CacheRules string `json:"cache_rules"`
|
||||||
@@ -267,6 +269,10 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
limitReqPerIP, err := normalizeProxyRouteLimitReqPerIP(input.LimitReqPerIP)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
if err := validateProxyRouteZoneDomainCertificates(ctx, domains, input.EnableHTTPS); err != nil {
|
if err := validateProxyRouteZoneDomainCertificates(ctx, domains, input.EnableHTTPS); err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
@@ -307,6 +313,7 @@ func buildProxyRoute(ctx context.Context, route *model.ProxyRoute, input Input)
|
|||||||
limitConnPerServer,
|
limitConnPerServer,
|
||||||
limitConnPerIP,
|
limitConnPerIP,
|
||||||
limitRate,
|
limitRate,
|
||||||
|
limitReqPerIP,
|
||||||
upstreamType,
|
upstreamType,
|
||||||
)
|
)
|
||||||
if err := applyProxyRouteUpstreamType(ctx, route, upstreamType, input); err != nil {
|
if err := applyProxyRouteUpstreamType(ctx, route, upstreamType, input); err != nil {
|
||||||
@@ -369,6 +376,7 @@ func buildProxyRouteView(ctx context.Context, route *model.ProxyRoute) (*View, e
|
|||||||
LimitConnPerServer: route.LimitConnPerServer,
|
LimitConnPerServer: route.LimitConnPerServer,
|
||||||
LimitConnPerIP: route.LimitConnPerIP,
|
LimitConnPerIP: route.LimitConnPerIP,
|
||||||
LimitRate: route.LimitRate,
|
LimitRate: route.LimitRate,
|
||||||
|
LimitReqPerIP: route.LimitReqPerIP,
|
||||||
CacheEnabled: route.CacheEnabled,
|
CacheEnabled: route.CacheEnabled,
|
||||||
CachePolicy: displayCachePolicy(route.CacheEnabled, route.CachePolicy),
|
CachePolicy: displayCachePolicy(route.CacheEnabled, route.CachePolicy),
|
||||||
CacheRules: route.CacheRules,
|
CacheRules: route.CacheRules,
|
||||||
|
|||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
-- +goose Up
|
||||||
|
INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('openresty_default_limit_req_per_ip', '', 'business', 0, '默认单 IP 请求频率限制(空关闭,例如 10r/s、100r/m)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
ALTER TABLE w_of_proxy_routes ADD COLUMN limit_req_per_ip VARCHAR(32) NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE w_of_proxy_routes DROP COLUMN limit_req_per_ip;
|
||||||
|
|
||||||
|
DELETE FROM w_system_configs WHERE key = 'openresty_default_limit_req_per_ip';
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
-- +goose Up
|
||||||
|
INSERT INTO w_system_configs (key, value, type, visibility, description, created_at, updated_at)
|
||||||
|
VALUES
|
||||||
|
('openresty_default_limit_req_per_ip', '', 'business', 0, '默认单 IP 请求频率限制(空关闭,例如 10r/s、100r/m)', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT (key) DO NOTHING;
|
||||||
|
|
||||||
|
ALTER TABLE w_of_proxy_routes ADD COLUMN limit_req_per_ip VARCHAR(32) NOT NULL DEFAULT '';
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
ALTER TABLE w_of_proxy_routes DROP COLUMN limit_req_per_ip;
|
||||||
|
|
||||||
|
DELETE FROM w_system_configs WHERE key = 'openresty_default_limit_req_per_ip';
|
||||||
@@ -25,6 +25,7 @@ type ProxyRoute struct {
|
|||||||
LimitConnPerServer int `json:"limit_conn_per_server" gorm:"not null;default:0"`
|
LimitConnPerServer int `json:"limit_conn_per_server" gorm:"not null;default:0"`
|
||||||
LimitConnPerIP int `json:"limit_conn_per_ip" gorm:"not null;default:0"`
|
LimitConnPerIP int `json:"limit_conn_per_ip" gorm:"not null;default:0"`
|
||||||
LimitRate string `json:"limit_rate" gorm:"size:32;not null;default:''"`
|
LimitRate string `json:"limit_rate" gorm:"size:32;not null;default:''"`
|
||||||
|
LimitReqPerIP string `json:"limit_req_per_ip" gorm:"size:32;not null;default:''"`
|
||||||
CacheEnabled bool `json:"cache_enabled" gorm:"not null;default:false"`
|
CacheEnabled bool `json:"cache_enabled" gorm:"not null;default:false"`
|
||||||
CachePolicy string `json:"cache_policy" gorm:"size:32;not null;default:''"`
|
CachePolicy string `json:"cache_policy" gorm:"size:32;not null;default:''"`
|
||||||
CacheRules string `json:"cache_rules" gorm:"type:text;not null;default:'[]'"`
|
CacheRules string `json:"cache_rules" gorm:"type:text;not null;default:'[]'"`
|
||||||
@@ -84,6 +85,7 @@ func UpdateProxyRouteRecord(ctx context.Context, route *ProxyRoute) error {
|
|||||||
"limit_conn_per_server": route.LimitConnPerServer,
|
"limit_conn_per_server": route.LimitConnPerServer,
|
||||||
"limit_conn_per_ip": route.LimitConnPerIP,
|
"limit_conn_per_ip": route.LimitConnPerIP,
|
||||||
"limit_rate": route.LimitRate,
|
"limit_rate": route.LimitRate,
|
||||||
|
"limit_req_per_ip": route.LimitReqPerIP,
|
||||||
"cache_enabled": route.CacheEnabled,
|
"cache_enabled": route.CacheEnabled,
|
||||||
"cache_policy": route.CachePolicy,
|
"cache_policy": route.CachePolicy,
|
||||||
"cache_rules": route.CacheRules,
|
"cache_rules": route.CacheRules,
|
||||||
|
|||||||
@@ -108,6 +108,7 @@ const (
|
|||||||
ConfigKeyOpenRestyDefaultLimitConnPerServer = "openresty_default_limit_conn_per_server" // 默认站点并发连接
|
ConfigKeyOpenRestyDefaultLimitConnPerServer = "openresty_default_limit_conn_per_server" // 默认站点并发连接
|
||||||
ConfigKeyOpenRestyDefaultLimitConnPerIP = "openresty_default_limit_conn_per_ip" // 默认单 IP 并发连接
|
ConfigKeyOpenRestyDefaultLimitConnPerIP = "openresty_default_limit_conn_per_ip" // 默认单 IP 并发连接
|
||||||
ConfigKeyOpenRestyDefaultLimitRate = "openresty_default_limit_rate" // 默认单请求带宽
|
ConfigKeyOpenRestyDefaultLimitRate = "openresty_default_limit_rate" // 默认单请求带宽
|
||||||
|
ConfigKeyOpenRestyDefaultLimitReqPerIP = "openresty_default_limit_req_per_ip" // 默认单 IP 请求频率限制
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -201,7 +201,7 @@ func renderTemplateDirective(enabled bool, statement string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func renderOpenRestyCacheTemplateBlock(cfg ConfigSnapshot) string {
|
func renderOpenRestyCacheTemplateBlock(cfg ConfigSnapshot) string {
|
||||||
lines := []string{renderOpenRestyLimitZoneBlock()}
|
lines := []string{renderOpenRestyLimitZoneBlock(cfg)}
|
||||||
if !cfg.CacheEnabled {
|
if !cfg.CacheEnabled {
|
||||||
lines = append(lines, renderOpenRestyObservabilityTemplateBlock())
|
lines = append(lines, renderOpenRestyObservabilityTemplateBlock())
|
||||||
return strings.Join(lines, "")
|
return strings.Join(lines, "")
|
||||||
@@ -222,8 +222,14 @@ func renderOpenRestyCacheTemplateBlock(cfg ConfigSnapshot) string {
|
|||||||
return strings.Join(lines, "")
|
return strings.Join(lines, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderOpenRestyLimitZoneBlock() string {
|
func renderOpenRestyLimitZoneBlock(cfg ConfigSnapshot) string {
|
||||||
return " limit_conn_zone $server_name zone=openflare_conn_per_server:10m;\n limit_conn_zone $binary_remote_addr zone=openflare_conn_per_ip:10m;\n"
|
var builder strings.Builder
|
||||||
|
builder.WriteString(" limit_conn_zone $server_name zone=openflare_conn_per_server:10m;\n")
|
||||||
|
builder.WriteString(" limit_conn_zone $binary_remote_addr zone=openflare_conn_per_ip:10m;\n")
|
||||||
|
if strings.TrimSpace(cfg.DefaultLimitReqPerIP) != "" {
|
||||||
|
fmt.Fprintf(&builder, " limit_req_zone $binary_remote_addr zone=openflare_req_per_ip:10m rate=%s;\n", strings.TrimSpace(cfg.DefaultLimitReqPerIP))
|
||||||
|
}
|
||||||
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func renderOpenRestyObservabilityTemplateBlock() string {
|
func renderOpenRestyObservabilityTemplateBlock() string {
|
||||||
@@ -475,6 +481,11 @@ func renderRouteLimitBlock(limitConfig routeLimitConfig) string {
|
|||||||
if strings.TrimSpace(limitConfig.LimitRate) != "" {
|
if strings.TrimSpace(limitConfig.LimitRate) != "" {
|
||||||
fmt.Fprintf(&builder, " limit_rate %s;\n", limitConfig.LimitRate)
|
fmt.Fprintf(&builder, " limit_rate %s;\n", limitConfig.LimitRate)
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(limitConfig.LimitReqPerIP) != "" {
|
||||||
|
burst := calculateBurst(limitConfig.LimitReqPerIP)
|
||||||
|
fmt.Fprintf(&builder, " limit_req zone=openflare_req_per_ip burst=%d nodelay;\n", burst)
|
||||||
|
fmt.Fprintf(&builder, " limit_req_status 429;\n")
|
||||||
|
}
|
||||||
return builder.String()
|
return builder.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -483,6 +494,7 @@ func mergeRouteLimitConfig(route Route, cfg ConfigSnapshot) routeLimitConfig {
|
|||||||
LimitConnPerServer: mergeLimitConn(route.LimitConnPerServer, cfg.DefaultLimitConnPerServer),
|
LimitConnPerServer: mergeLimitConn(route.LimitConnPerServer, cfg.DefaultLimitConnPerServer),
|
||||||
LimitConnPerIP: mergeLimitConn(route.LimitConnPerIP, cfg.DefaultLimitConnPerIP),
|
LimitConnPerIP: mergeLimitConn(route.LimitConnPerIP, cfg.DefaultLimitConnPerIP),
|
||||||
LimitRate: mergeLimitRate(route.LimitRate, cfg.DefaultLimitRate),
|
LimitRate: mergeLimitRate(route.LimitRate, cfg.DefaultLimitRate),
|
||||||
|
LimitReqPerIP: mergeLimitRate(route.LimitReqPerIP, cfg.DefaultLimitReqPerIP),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -864,3 +876,29 @@ func buildPathExactMatchPattern(rules []string) string {
|
|||||||
}
|
}
|
||||||
return fmt.Sprintf("^(?:%s)$", strings.Join(parts, "|"))
|
return fmt.Sprintf("^(?:%s)$", strings.Join(parts, "|"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func calculateBurst(rateStr string) int {
|
||||||
|
rateStr = strings.ToLower(strings.TrimSpace(rateStr))
|
||||||
|
if rateStr == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
var val int
|
||||||
|
var unit string
|
||||||
|
_, err := fmt.Sscanf(rateStr, "%dr/%s", &val, &unit)
|
||||||
|
if err != nil {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
if val <= 0 {
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
if unit == "s" {
|
||||||
|
return val * 2
|
||||||
|
} else if unit == "m" {
|
||||||
|
b := val / 5
|
||||||
|
if b < 5 {
|
||||||
|
b = 5
|
||||||
|
}
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
return 5
|
||||||
|
}
|
||||||
|
|||||||
@@ -464,16 +464,18 @@ func TestMergeRouteLimitConfig(t *testing.T) {
|
|||||||
DefaultLimitConnPerServer: 100,
|
DefaultLimitConnPerServer: 100,
|
||||||
DefaultLimitConnPerIP: 10,
|
DefaultLimitConnPerIP: 10,
|
||||||
DefaultLimitRate: "512k",
|
DefaultLimitRate: "512k",
|
||||||
|
DefaultLimitReqPerIP: "10r/s",
|
||||||
},
|
},
|
||||||
want: routeLimitConfig{LimitConnPerServer: 100, LimitConnPerIP: 10, LimitRate: "512k"},
|
want: routeLimitConfig{LimitConnPerServer: 100, LimitConnPerIP: 10, LimitRate: "512k", LimitReqPerIP: "10r/s"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "explicit off ignores default",
|
name: "explicit off ignores default",
|
||||||
route: Route{LimitConnPerServer: -1, LimitConnPerIP: -1, LimitRate: "-1"},
|
route: Route{LimitConnPerServer: -1, LimitConnPerIP: -1, LimitRate: "-1", LimitReqPerIP: "-1"},
|
||||||
cfg: ConfigSnapshot{
|
cfg: ConfigSnapshot{
|
||||||
DefaultLimitConnPerServer: 100,
|
DefaultLimitConnPerServer: 100,
|
||||||
DefaultLimitConnPerIP: 10,
|
DefaultLimitConnPerIP: 10,
|
||||||
DefaultLimitRate: "512k",
|
DefaultLimitRate: "512k",
|
||||||
|
DefaultLimitReqPerIP: "10r/s",
|
||||||
},
|
},
|
||||||
want: routeLimitConfig{},
|
want: routeLimitConfig{},
|
||||||
},
|
},
|
||||||
@@ -484,8 +486,9 @@ func TestMergeRouteLimitConfig(t *testing.T) {
|
|||||||
DefaultLimitConnPerServer: 100,
|
DefaultLimitConnPerServer: 100,
|
||||||
DefaultLimitConnPerIP: 10,
|
DefaultLimitConnPerIP: 10,
|
||||||
DefaultLimitRate: "512k",
|
DefaultLimitRate: "512k",
|
||||||
|
DefaultLimitReqPerIP: "10r/s",
|
||||||
},
|
},
|
||||||
want: routeLimitConfig{LimitConnPerServer: 50, LimitConnPerIP: 5, LimitRate: "1m"},
|
want: routeLimitConfig{LimitConnPerServer: 50, LimitConnPerIP: 5, LimitRate: "1m", LimitReqPerIP: "10r/s"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "partial inherit",
|
name: "partial inherit",
|
||||||
@@ -494,8 +497,9 @@ func TestMergeRouteLimitConfig(t *testing.T) {
|
|||||||
DefaultLimitConnPerServer: 100,
|
DefaultLimitConnPerServer: 100,
|
||||||
DefaultLimitConnPerIP: 10,
|
DefaultLimitConnPerIP: 10,
|
||||||
DefaultLimitRate: "256k",
|
DefaultLimitRate: "256k",
|
||||||
|
DefaultLimitReqPerIP: "10r/s",
|
||||||
},
|
},
|
||||||
want: routeLimitConfig{LimitConnPerServer: 100, LimitConnPerIP: 0, LimitRate: "256k"},
|
want: routeLimitConfig{LimitConnPerServer: 100, LimitConnPerIP: 0, LimitRate: "256k", LimitReqPerIP: "10r/s"},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@@ -522,6 +526,7 @@ func TestRenderRouteConfigAppliesDefaultLimits(t *testing.T) {
|
|||||||
DefaultLimitConnPerServer: 120,
|
DefaultLimitConnPerServer: 120,
|
||||||
DefaultLimitConnPerIP: 12,
|
DefaultLimitConnPerIP: 12,
|
||||||
DefaultLimitRate: "512k",
|
DefaultLimitRate: "512k",
|
||||||
|
DefaultLimitReqPerIP: "10r/s",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rendered, err := RenderRouteConfig(doc, nil)
|
rendered, err := RenderRouteConfig(doc, nil)
|
||||||
@@ -532,6 +537,8 @@ func TestRenderRouteConfigAppliesDefaultLimits(t *testing.T) {
|
|||||||
"limit_conn openflare_conn_per_server 120;",
|
"limit_conn openflare_conn_per_server 120;",
|
||||||
"limit_conn openflare_conn_per_ip 12;",
|
"limit_conn openflare_conn_per_ip 12;",
|
||||||
"limit_rate 512k;",
|
"limit_rate 512k;",
|
||||||
|
"limit_req zone=openflare_req_per_ip burst=20 nodelay;",
|
||||||
|
"limit_req_status 429;",
|
||||||
} {
|
} {
|
||||||
if !strings.Contains(rendered, want) {
|
if !strings.Contains(rendered, want) {
|
||||||
t.Fatalf("expected %q in route config, got:\n%s", want, rendered)
|
t.Fatalf("expected %q in route config, got:\n%s", want, rendered)
|
||||||
@@ -550,18 +557,20 @@ func TestRenderRouteConfigExplicitOffSkipsDefaultLimits(t *testing.T) {
|
|||||||
LimitConnPerServer: -1,
|
LimitConnPerServer: -1,
|
||||||
LimitConnPerIP: -1,
|
LimitConnPerIP: -1,
|
||||||
LimitRate: "-1",
|
LimitRate: "-1",
|
||||||
|
LimitReqPerIP: "-1",
|
||||||
}},
|
}},
|
||||||
OpenRestyConfig: ConfigSnapshot{
|
OpenRestyConfig: ConfigSnapshot{
|
||||||
DefaultLimitConnPerServer: 120,
|
DefaultLimitConnPerServer: 120,
|
||||||
DefaultLimitConnPerIP: 12,
|
DefaultLimitConnPerIP: 12,
|
||||||
DefaultLimitRate: "512k",
|
DefaultLimitRate: "512k",
|
||||||
|
DefaultLimitReqPerIP: "10r/s",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
rendered, err := RenderRouteConfig(doc, nil)
|
rendered, err := RenderRouteConfig(doc, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("RenderRouteConfig() error = %v", err)
|
t.Fatalf("RenderRouteConfig() error = %v", err)
|
||||||
}
|
}
|
||||||
if strings.Contains(rendered, "limit_conn") || strings.Contains(rendered, "limit_rate") {
|
if strings.Contains(rendered, "limit_conn") || strings.Contains(rendered, "limit_rate") || strings.Contains(rendered, "limit_req") {
|
||||||
t.Fatalf("expected no limit directives, got:\n%s", rendered)
|
t.Fatalf("expected no limit directives, got:\n%s", rendered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -165,6 +165,7 @@ type Route struct {
|
|||||||
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
LimitConnPerServer int `json:"limit_conn_per_server,omitempty"`
|
||||||
LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"`
|
LimitConnPerIP int `json:"limit_conn_per_ip,omitempty"`
|
||||||
LimitRate string `json:"limit_rate,omitempty"`
|
LimitRate string `json:"limit_rate,omitempty"`
|
||||||
|
LimitReqPerIP string `json:"limit_req_per_ip,omitempty"`
|
||||||
CacheEnabled bool `json:"cache_enabled"`
|
CacheEnabled bool `json:"cache_enabled"`
|
||||||
CachePolicy string `json:"cache_policy,omitempty"`
|
CachePolicy string `json:"cache_policy,omitempty"`
|
||||||
CacheRules []string `json:"cache_rules,omitempty"`
|
CacheRules []string `json:"cache_rules,omitempty"`
|
||||||
@@ -312,6 +313,7 @@ type ConfigSnapshot struct {
|
|||||||
DefaultLimitConnPerServer int `json:"default_limit_conn_per_server,omitempty"`
|
DefaultLimitConnPerServer int `json:"default_limit_conn_per_server,omitempty"`
|
||||||
DefaultLimitConnPerIP int `json:"default_limit_conn_per_ip,omitempty"`
|
DefaultLimitConnPerIP int `json:"default_limit_conn_per_ip,omitempty"`
|
||||||
DefaultLimitRate string `json:"default_limit_rate,omitempty"`
|
DefaultLimitRate string `json:"default_limit_rate,omitempty"`
|
||||||
|
DefaultLimitReqPerIP string `json:"default_limit_req_per_ip,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Document is the top-level input structure for the OpenResty renderer,
|
// Document is the top-level input structure for the OpenResty renderer,
|
||||||
@@ -341,6 +343,7 @@ type routeLimitConfig struct {
|
|||||||
LimitConnPerServer int
|
LimitConnPerServer int
|
||||||
LimitConnPerIP int
|
LimitConnPerIP int
|
||||||
LimitRate string
|
LimitRate string
|
||||||
|
LimitReqPerIP string
|
||||||
}
|
}
|
||||||
|
|
||||||
type routeUpstreamConfig struct {
|
type routeUpstreamConfig struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user