feat: cap gateway client concurrency
This commit is contained in:
parent
2b18ad8dd4
commit
d4539da9c5
@ -86,7 +86,8 @@ Proxy Pool 用 Controller 协调这些变化,并让 Gateway 数据面只消费
|
|||||||
Distribution 提取权限可按命中凭据分别收敛。Distribution 凭据还可限制单次
|
Distribution 提取权限可按命中凭据分别收敛。Distribution 凭据还可限制单次
|
||||||
提取数量、可访问 Upstream 与地区,Gateway 凭据可限制可访问 Routing;未配置时
|
提取数量、可访问 Upstream 与地区,Gateway 凭据可限制可访问 Routing;未配置时
|
||||||
保持既有全范围行为。Gateway 凭据还可在每个 Worker 内限制每分钟请求数,
|
保持既有全范围行为。Gateway 凭据还可在每个 Worker 内限制每分钟请求数,
|
||||||
不把该热路径计数写入 Redis 或 PostgreSQL。
|
同时限制 HTTP 请求和 CONNECT 隧道的并发数;热路径计数不写入 Redis 或
|
||||||
|
PostgreSQL。
|
||||||
|
|
||||||
## 架构概览
|
## 架构概览
|
||||||
|
|
||||||
|
|||||||
@ -181,11 +181,12 @@ distribution:
|
|||||||
|
|
||||||
### 3.4 Gateway Client 路由约束
|
### 3.4 Gateway Client 路由约束
|
||||||
|
|
||||||
Gateway 的 auth.client 支持 allowedRoutings 和 requestsPerMinute。认证成功后,
|
Gateway 的 auth.client 支持 allowedRoutings、requestsPerMinute 和
|
||||||
|
maxConcurrentConnections。认证成功后,
|
||||||
Gateway 先完成目标地址策略和本地 Routing 匹配,再于本地 Dispatcher 前检查
|
Gateway 先完成目标地址策略和本地 Routing 匹配,再于本地 Dispatcher 前检查
|
||||||
Routing 集合;不匹配返回 403,不会尝试选择或预留 Proxy。速率限制以认证 Client
|
Routing 集合;不匹配返回 403,不会尝试选择或预留 Proxy。速率和并发限制均以
|
||||||
为计数键,在每个 Gateway Worker 内执行,超过限制返回 429。两项检查都只读取
|
认证 Client 为计数键,在每个 Gateway Worker 内执行,超过限制返回 429。三项
|
||||||
请求上下文和当前本地状态,不访问 Redis、PostgreSQL 或 Provider。
|
检查都只读取请求上下文和当前本地状态,不访问 Redis、PostgreSQL 或 Provider。
|
||||||
|
|
||||||
allowedRoutings 中的每个名称必须对应一个启用的 Gateway Routing。Gateway 上的
|
allowedRoutings 中的每个名称必须对应一个启用的 Gateway Routing。Gateway 上的
|
||||||
maxExtractCount、allowedUpstreams 和 allowedRegions,以及 Distribution 上的
|
maxExtractCount、allowedUpstreams 和 allowedRegions,以及 Distribution 上的
|
||||||
@ -202,6 +203,7 @@ gateway:
|
|||||||
client:
|
client:
|
||||||
allowedRoutings: [checkout]
|
allowedRoutings: [checkout]
|
||||||
requestsPerMinute: 600
|
requestsPerMinute: 600
|
||||||
|
maxConcurrentConnections: 20
|
||||||
~~~
|
~~~
|
||||||
|
|
||||||
Gateway、Distribution、Admin 与 Provider API 是独立认证边界。改变其中一套
|
Gateway、Distribution、Admin 与 Provider API 是独立认证边界。改变其中一套
|
||||||
|
|||||||
@ -28,6 +28,8 @@
|
|||||||
代理可用期或转发失败时失效。
|
代理可用期或转发失败时失效。
|
||||||
- Gateway 凭据的 requestsPerMinute 限制按认证 Client 在本 Worker 内执行,不把
|
- Gateway 凭据的 requestsPerMinute 限制按认证 Client 在本 Worker 内执行,不把
|
||||||
认证身份、会话或请求计数写入 PostgreSQL、Redis 或 Prometheus 标签。
|
认证身份、会话或请求计数写入 PostgreSQL、Redis 或 Prometheus 标签。
|
||||||
|
- Gateway 凭据的 maxConcurrentConnections 在请求或 CONNECT 隧道的完整生命周期
|
||||||
|
内保留一个本地租约;无论转发成功、失败还是服务排空,租约都会释放。
|
||||||
|
|
||||||
## 3. 目标地址策略
|
## 3. 目标地址策略
|
||||||
|
|
||||||
|
|||||||
@ -413,10 +413,15 @@ func TestValidateDistributionCredentialClientPolicy(t *testing.T) {
|
|||||||
t.Fatalf("Validate(gateway request rate policy) error = %v", err)
|
t.Fatalf("Validate(gateway request rate policy) error = %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg.Gateway.Auth.ClientPolicy = clientpolicy.Policy{MaxConcurrentConnections: 3}
|
||||||
|
if err := Validate(cfg); err != nil {
|
||||||
|
t.Fatalf("Validate(gateway concurrency policy) error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
cfg.Gateway.Auth.ClientPolicy = clientpolicy.Policy{}
|
cfg.Gateway.Auth.ClientPolicy = clientpolicy.Policy{}
|
||||||
cfg.Distribution.Auth.ClientPolicy = clientpolicy.Policy{RequestsPerMinute: 60}
|
cfg.Distribution.Auth.ClientPolicy = clientpolicy.Policy{MaxConcurrentConnections: 3}
|
||||||
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "only supported on gateway") {
|
if err := Validate(cfg); err == nil || !strings.Contains(err.Error(), "only supported on gateway") {
|
||||||
t.Fatalf("Validate(distribution request rate policy) error = %v", err)
|
t.Fatalf("Validate(distribution gateway limit policy) error = %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -552,8 +552,8 @@ func validateDistributionClientPolicies(auth Auth, upstreams map[string]Upstream
|
|||||||
}
|
}
|
||||||
|
|
||||||
func validateDistributionClientPolicy(scope string, policy clientpolicy.Policy, upstreams map[string]Upstream) error {
|
func validateDistributionClientPolicy(scope string, policy clientpolicy.Policy, upstreams map[string]Upstream) error {
|
||||||
if len(policy.AllowedRoutings) != 0 || policy.RequestsPerMinute != 0 {
|
if len(policy.AllowedRoutings) != 0 || policy.RequestsPerMinute != 0 || policy.MaxConcurrentConnections != 0 {
|
||||||
return fmt.Errorf("validate %s: routing access and request rate limits are only supported on gateway", scope)
|
return fmt.Errorf("validate %s: routing access and Gateway limits are only supported on gateway", scope)
|
||||||
}
|
}
|
||||||
for _, upstream := range policy.AllowedUpstreams {
|
for _, upstream := range policy.AllowedUpstreams {
|
||||||
if _, exists := upstreams[upstream]; !exists {
|
if _, exists := upstreams[upstream]; !exists {
|
||||||
@ -584,6 +584,9 @@ func validateGatewayClientPolicy(scope string, policy clientpolicy.Policy, routi
|
|||||||
if int64(policy.RequestsPerMinute) > MaximumExactCounter {
|
if int64(policy.RequestsPerMinute) > MaximumExactCounter {
|
||||||
return fmt.Errorf("validate %s.requestsPerMinute: exceeds exact counter range", scope)
|
return fmt.Errorf("validate %s.requestsPerMinute: exceeds exact counter range", scope)
|
||||||
}
|
}
|
||||||
|
if int64(policy.MaxConcurrentConnections) > MaximumExactCounter {
|
||||||
|
return fmt.Errorf("validate %s.maxConcurrentConnections: exceeds exact counter range", scope)
|
||||||
|
}
|
||||||
for _, routing := range policy.AllowedRoutings {
|
for _, routing := range policy.AllowedRoutings {
|
||||||
if _, exists := routings[routing]; !exists {
|
if _, exists := routings[routing]; !exists {
|
||||||
return fmt.Errorf("validate %s.allowedRoutings: routing %q does not exist or is not an enabled gateway routing", scope, routing)
|
return fmt.Errorf("validate %s.allowedRoutings: routing %q does not exist or is not an enabled gateway routing", scope, routing)
|
||||||
|
|||||||
@ -12,20 +12,21 @@ var ErrInvalidPolicy = errors.New("invalid client policy")
|
|||||||
// Policy limits an authenticated client's permitted Distribution extraction or
|
// Policy limits an authenticated client's permitted Distribution extraction or
|
||||||
// Gateway routing. Zero values preserve the existing unrestricted behavior.
|
// Gateway routing. Zero values preserve the existing unrestricted behavior.
|
||||||
type Policy struct {
|
type Policy struct {
|
||||||
MaxExtractCount int `yaml:"maxExtractCount"`
|
MaxExtractCount int `yaml:"maxExtractCount"`
|
||||||
RequestsPerMinute int `yaml:"requestsPerMinute"`
|
RequestsPerMinute int `yaml:"requestsPerMinute"`
|
||||||
AllowedUpstreams []string `yaml:"allowedUpstreams"`
|
MaxConcurrentConnections int `yaml:"maxConcurrentConnections"`
|
||||||
AllowedRegions []string `yaml:"allowedRegions"`
|
AllowedUpstreams []string `yaml:"allowedUpstreams"`
|
||||||
AllowedRoutings []string `yaml:"allowedRoutings"`
|
AllowedRegions []string `yaml:"allowedRegions"`
|
||||||
|
AllowedRoutings []string `yaml:"allowedRoutings"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (policy Policy) IsZero() bool {
|
func (policy Policy) IsZero() bool {
|
||||||
return policy.MaxExtractCount == 0 && policy.RequestsPerMinute == 0 && len(policy.AllowedUpstreams) == 0 &&
|
return policy.MaxExtractCount == 0 && policy.RequestsPerMinute == 0 && policy.MaxConcurrentConnections == 0 && len(policy.AllowedUpstreams) == 0 &&
|
||||||
len(policy.AllowedRegions) == 0 && len(policy.AllowedRoutings) == 0
|
len(policy.AllowedRegions) == 0 && len(policy.AllowedRoutings) == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
func (policy Policy) Validate() error {
|
func (policy Policy) Validate() error {
|
||||||
if policy.MaxExtractCount < 0 || policy.RequestsPerMinute < 0 ||
|
if policy.MaxExtractCount < 0 || policy.RequestsPerMinute < 0 || policy.MaxConcurrentConnections < 0 ||
|
||||||
!validUniqueValues(policy.AllowedUpstreams) ||
|
!validUniqueValues(policy.AllowedUpstreams) ||
|
||||||
!validUniqueValues(policy.AllowedRegions) ||
|
!validUniqueValues(policy.AllowedRegions) ||
|
||||||
!validUniqueValues(policy.AllowedRoutings) {
|
!validUniqueValues(policy.AllowedRoutings) {
|
||||||
|
|||||||
@ -16,14 +16,16 @@ func TestValidate(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{name: "empty policy", policy: Policy{}},
|
{name: "empty policy", policy: Policy{}},
|
||||||
{name: "bounded policy", policy: Policy{
|
{name: "bounded policy", policy: Policy{
|
||||||
MaxExtractCount: 10,
|
MaxExtractCount: 10,
|
||||||
RequestsPerMinute: 600,
|
RequestsPerMinute: 600,
|
||||||
AllowedUpstreams: []string{"provider-a", "provider-b"},
|
MaxConcurrentConnections: 3,
|
||||||
AllowedRegions: []string{"shanghai", "beijing"},
|
AllowedUpstreams: []string{"provider-a", "provider-b"},
|
||||||
AllowedRoutings: []string{"checkout", "catalog"},
|
AllowedRegions: []string{"shanghai", "beijing"},
|
||||||
|
AllowedRoutings: []string{"checkout", "catalog"},
|
||||||
}},
|
}},
|
||||||
{name: "negative max count", policy: Policy{MaxExtractCount: -1}, wantErr: true},
|
{name: "negative max count", policy: Policy{MaxExtractCount: -1}, wantErr: true},
|
||||||
{name: "negative request rate", policy: Policy{RequestsPerMinute: -1}, wantErr: true},
|
{name: "negative request rate", policy: Policy{RequestsPerMinute: -1}, wantErr: true},
|
||||||
|
{name: "negative concurrency", policy: Policy{MaxConcurrentConnections: -1}, wantErr: true},
|
||||||
{name: "empty upstream", policy: Policy{AllowedUpstreams: []string{""}}, wantErr: true},
|
{name: "empty upstream", policy: Policy{AllowedUpstreams: []string{""}}, wantErr: true},
|
||||||
{name: "whitespace upstream", policy: Policy{AllowedUpstreams: []string{" provider-a"}}, wantErr: true},
|
{name: "whitespace upstream", policy: Policy{AllowedUpstreams: []string{" provider-a"}}, wantErr: true},
|
||||||
{name: "duplicate upstream", policy: Policy{AllowedUpstreams: []string{"provider-a", "provider-a"}}, wantErr: true},
|
{name: "duplicate upstream", policy: Policy{AllowedUpstreams: []string{"provider-a", "provider-a"}}, wantErr: true},
|
||||||
|
|||||||
@ -52,14 +52,18 @@ func BuildProtection(listener config.Listener) (Protection, error) {
|
|||||||
return Protection{Auth: auth, Access: access, Admission: chainGuards(admissionGuards...), ClientIPs: clientIPs}, nil
|
return Protection{Auth: auth, Access: access, Admission: chainGuards(admissionGuards...), ClientIPs: clientIPs}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildCredentialAdmission(authentication config.Auth) (*CredentialAdmissionGuard, error) {
|
func buildCredentialAdmission(authentication config.Auth) (Guard, error) {
|
||||||
limits := make(map[int]Admitter)
|
rateLimits := make(map[int]Admitter)
|
||||||
|
concurrencyLimits := make(map[int]struct{})
|
||||||
for _, policy := range credentialPolicies(authentication) {
|
for _, policy := range credentialPolicies(authentication) {
|
||||||
|
if policy.MaxConcurrentConnections > 0 {
|
||||||
|
concurrencyLimits[policy.MaxConcurrentConnections] = struct{}{}
|
||||||
|
}
|
||||||
limit := policy.RequestsPerMinute
|
limit := policy.RequestsPerMinute
|
||||||
if limit == 0 {
|
if limit == 0 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, exists := limits[limit]; exists {
|
if _, exists := rateLimits[limit]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
limiter, err := platformAdmission.NewFixedWindow(platformAdmission.FixedWindowConfig{
|
limiter, err := platformAdmission.NewFixedWindow(platformAdmission.FixedWindowConfig{
|
||||||
@ -69,9 +73,12 @@ func buildCredentialAdmission(authentication config.Auth) (*CredentialAdmissionG
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("build gateway credential admission: %w", err)
|
return nil, fmt.Errorf("build gateway credential admission: %w", err)
|
||||||
}
|
}
|
||||||
limits[limit] = limiter
|
rateLimits[limit] = limiter
|
||||||
}
|
}
|
||||||
return NewCredentialAdmissionGuard(limits), nil
|
return chainGuards(
|
||||||
|
NewCredentialAdmissionGuard(rateLimits),
|
||||||
|
NewCredentialConcurrencyGuard(concurrencyLimits),
|
||||||
|
), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func credentialPolicies(authentication config.Auth) []clientpolicy.Policy {
|
func credentialPolicies(authentication config.Auth) []clientpolicy.Policy {
|
||||||
|
|||||||
@ -99,6 +99,46 @@ func TestBuildProtectionAppliesCredentialRequestRateLimit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildProtectionAppliesCredentialConcurrentConnectionLimit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
protection, err := BuildProtection(config.Listener{
|
||||||
|
Auth: config.Auth{
|
||||||
|
Mode: "bearer", Token: "gateway-token",
|
||||||
|
ClientPolicy: clientpolicy.Policy{MaxConcurrentConnections: 1},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildProtection() error = %v", err)
|
||||||
|
}
|
||||||
|
newRequest := func() *http.Request {
|
||||||
|
request := httptest.NewRequest(http.MethodGet, "http://example.test", nil)
|
||||||
|
request.RemoteAddr = "198.51.100.8:1234"
|
||||||
|
request.Header.Set("Proxy-Authorization", "Bearer gateway-token")
|
||||||
|
if err := protection.Auth.Check(context.Background(), request); err != nil {
|
||||||
|
t.Fatalf("Auth.Check() error = %v", err)
|
||||||
|
}
|
||||||
|
return request
|
||||||
|
}
|
||||||
|
first := newRequest()
|
||||||
|
if err := protection.Admission.Check(context.Background(), first); err != nil {
|
||||||
|
t.Fatalf("Admission.Check(first) error = %v", err)
|
||||||
|
}
|
||||||
|
second := newRequest()
|
||||||
|
err = protection.Admission.Check(context.Background(), second)
|
||||||
|
var admissionError *HTTPError
|
||||||
|
if !errors.As(err, &admissionError) || admissionError.StatusCode != http.StatusTooManyRequests {
|
||||||
|
t.Fatalf("Admission.Check(second) error = %T %v, want 429", err, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
releaseCredentialReservation(first)
|
||||||
|
third := newRequest()
|
||||||
|
if err := protection.Admission.Check(context.Background(), third); err != nil {
|
||||||
|
t.Fatalf("Admission.Check(after release) error = %v", err)
|
||||||
|
}
|
||||||
|
releaseCredentialReservation(third)
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildProtectionAnyPreservesIPWhitelistRejectionIndependentOfOrder(t *testing.T) {
|
func TestBuildProtectionAnyPreservesIPWhitelistRejectionIndependentOfOrder(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
methods := [][]config.AuthMethod{
|
methods := [][]config.AuthMethod{
|
||||||
|
|||||||
@ -187,6 +187,7 @@ func (handler *Handler) ServeHTTP(writer http.ResponseWriter, request *http.Requ
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
defer releaseCredentialReservation(request)
|
||||||
|
|
||||||
if request.Method == http.MethodConnect {
|
if request.Method == http.MethodConnect {
|
||||||
target, err := handler.targets.EvaluateConnectAuthority(request.Context(), request.Host)
|
target, err := handler.targets.EvaluateConnectAuthority(request.Context(), request.Host)
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"proxy-pool/internal/config"
|
||||||
"proxy-pool/internal/domain/clientpolicy"
|
"proxy-pool/internal/domain/clientpolicy"
|
||||||
outcomeDomain "proxy-pool/internal/domain/outcome"
|
outcomeDomain "proxy-pool/internal/domain/outcome"
|
||||||
proxyDomain "proxy-pool/internal/domain/proxy"
|
proxyDomain "proxy-pool/internal/domain/proxy"
|
||||||
@ -111,6 +112,71 @@ func TestHandlerRejectsGatewayRoutingOutsideCredentialPolicy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandlerReleasesCredentialConcurrencyAfterRequest(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
protection, err := BuildProtection(config.Listener{
|
||||||
|
Auth: config.Auth{Mode: "bearer", Token: "gateway-token",
|
||||||
|
ClientPolicy: clientpolicy.Policy{MaxConcurrentConnections: 1}},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("BuildProtection() error = %v", err)
|
||||||
|
}
|
||||||
|
dispatcher, view := dispatcherWithProxies(t, "proxy-a")
|
||||||
|
started := make(chan struct{})
|
||||||
|
release := make(chan struct{})
|
||||||
|
transport := &fakeTransport{roundTrip: func(
|
||||||
|
_ context.Context, _ proxyDomain.Proxy, _ *http.Request, commit ...func() error,
|
||||||
|
) (*http.Response, error) {
|
||||||
|
if err := commit[0](); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
select {
|
||||||
|
case started <- struct{}{}:
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
<-release
|
||||||
|
return &http.Response{StatusCode: http.StatusNoContent, Header: make(http.Header), Body: http.NoBody}, nil
|
||||||
|
}}
|
||||||
|
handler, err := New(Config{}, Dependencies{
|
||||||
|
Auth: protection.Auth, Admission: protection.Admission, Targets: fakeTargets{},
|
||||||
|
Router: RouteFunc(func(*http.Request) (dispatch.Request, error) {
|
||||||
|
return dispatch.Request{Upstreams: []string{"provider-a"}}, nil
|
||||||
|
}),
|
||||||
|
Dispatcher: dispatcher, Transport: transport,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("New() error = %v", err)
|
||||||
|
}
|
||||||
|
request := func() *http.Request {
|
||||||
|
result := httptest.NewRequest(http.MethodGet, "http://example.test/resource", nil)
|
||||||
|
result.Header.Set("Proxy-Authorization", "Bearer gateway-token")
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
first := httptest.NewRecorder()
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
handler.ServeHTTP(first, request())
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
<-started
|
||||||
|
|
||||||
|
second := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(second, request())
|
||||||
|
if second.Code != http.StatusTooManyRequests {
|
||||||
|
t.Fatalf("second status = %d, want 429", second.Code)
|
||||||
|
}
|
||||||
|
close(release)
|
||||||
|
<-done
|
||||||
|
|
||||||
|
third := httptest.NewRecorder()
|
||||||
|
handler.ServeHTTP(third, request())
|
||||||
|
if third.Code != http.StatusNoContent {
|
||||||
|
t.Fatalf("third status = %d, want 204", third.Code)
|
||||||
|
}
|
||||||
|
assertNoLeakedCapacity(t, view)
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandlerPinsAuthenticatedSessionToCommittedProxyAndStripsHeader(t *testing.T) {
|
func TestHandlerPinsAuthenticatedSessionToCommittedProxyAndStripsHeader(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"proxy-pool/internal/gateway/policy"
|
"proxy-pool/internal/gateway/policy"
|
||||||
"proxy-pool/internal/platform/httpsecurity"
|
"proxy-pool/internal/platform/httpsecurity"
|
||||||
@ -91,6 +92,85 @@ type CredentialAdmissionGuard struct {
|
|||||||
limiters map[int]Admitter
|
limiters map[int]Admitter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type credentialReservationContextKey struct{}
|
||||||
|
|
||||||
|
type clientConcurrency struct {
|
||||||
|
max int
|
||||||
|
mu sync.Mutex
|
||||||
|
use map[string]int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (limiter *clientConcurrency) acquire(clientID string) (func(), bool) {
|
||||||
|
limiter.mu.Lock()
|
||||||
|
defer limiter.mu.Unlock()
|
||||||
|
if limiter.use[clientID] >= limiter.max {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
limiter.use[clientID]++
|
||||||
|
var once sync.Once
|
||||||
|
return func() {
|
||||||
|
once.Do(func() {
|
||||||
|
limiter.mu.Lock()
|
||||||
|
defer limiter.mu.Unlock()
|
||||||
|
if limiter.use[clientID] <= 1 {
|
||||||
|
delete(limiter.use, clientID)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
limiter.use[clientID]--
|
||||||
|
})
|
||||||
|
}, true
|
||||||
|
}
|
||||||
|
|
||||||
|
type CredentialConcurrencyGuard struct {
|
||||||
|
limiters map[int]*clientConcurrency
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCredentialConcurrencyGuard(limits map[int]struct{}) *CredentialConcurrencyGuard {
|
||||||
|
if len(limits) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
limiters := make(map[int]*clientConcurrency, len(limits))
|
||||||
|
for limit := range limits {
|
||||||
|
if limit > 0 {
|
||||||
|
limiters[limit] = &clientConcurrency{max: limit, use: make(map[string]int)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(limiters) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &CredentialConcurrencyGuard{limiters: limiters}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (guard *CredentialConcurrencyGuard) Check(_ context.Context, request *http.Request) error {
|
||||||
|
if guard == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
identity, authenticated := httpsecurity.IdentityFromRequest(request)
|
||||||
|
if !authenticated || identity.ClientPolicy.MaxConcurrentConnections == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
limiter := guard.limiters[identity.ClientPolicy.MaxConcurrentConnections]
|
||||||
|
if limiter == nil || identity.ClientID == "" {
|
||||||
|
return &HTTPError{StatusCode: http.StatusInternalServerError, Cause: errors.New("gateway credential concurrency is not configured")}
|
||||||
|
}
|
||||||
|
release, acquired := limiter.acquire(identity.ClientID)
|
||||||
|
if !acquired {
|
||||||
|
return &HTTPError{StatusCode: http.StatusTooManyRequests, Cause: errors.New("gateway credential concurrency limit exceeded")}
|
||||||
|
}
|
||||||
|
*request = *request.WithContext(context.WithValue(request.Context(), credentialReservationContextKey{}, release))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func releaseCredentialReservation(request *http.Request) {
|
||||||
|
if request == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
release, found := request.Context().Value(credentialReservationContextKey{}).(func())
|
||||||
|
if found && release != nil {
|
||||||
|
release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func NewCredentialAdmissionGuard(limiters map[int]Admitter) *CredentialAdmissionGuard {
|
func NewCredentialAdmissionGuard(limiters map[int]Admitter) *CredentialAdmissionGuard {
|
||||||
if len(limiters) == 0 {
|
if len(limiters) == 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user