diff --git a/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml b/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml index 544718e877..29b89844e8 100644 --- a/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml +++ b/config/crd/bases/k8s.nginx.org_virtualserverroutes.yaml @@ -878,6 +878,10 @@ spec: is enabled. The default is set in the proxy-busy-buffers-size ConfigMap key.' type: string + client-body-buffer-size: + description: Sets the size of the buffer used for reading the + client request body. + type: string client-max-body-size: description: Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap diff --git a/config/crd/bases/k8s.nginx.org_virtualservers.yaml b/config/crd/bases/k8s.nginx.org_virtualservers.yaml index ac5bdd456a..798948336b 100644 --- a/config/crd/bases/k8s.nginx.org_virtualservers.yaml +++ b/config/crd/bases/k8s.nginx.org_virtualservers.yaml @@ -1067,6 +1067,10 @@ spec: is enabled. The default is set in the proxy-busy-buffers-size ConfigMap key.' type: string + client-body-buffer-size: + description: Sets the size of the buffer used for reading the + client request body. + type: string client-max-body-size: description: Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap diff --git a/deploy/crds.yaml b/deploy/crds.yaml index fd080f2d59..d6aa0ce873 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -1916,6 +1916,10 @@ spec: is enabled. The default is set in the proxy-busy-buffers-size ConfigMap key.' type: string + client-body-buffer-size: + description: Sets the size of the buffer used for reading the + client request body. + type: string client-max-body-size: description: Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap @@ -3335,6 +3339,10 @@ spec: is enabled. The default is set in the proxy-busy-buffers-size ConfigMap key.' type: string + client-body-buffer-size: + description: Sets the size of the buffer used for reading the + client request body. + type: string client-max-body-size: description: Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap diff --git a/docs/crd/k8s.nginx.org_virtualserverroutes.md b/docs/crd/k8s.nginx.org_virtualserverroutes.md index 52426dacee..bd7c136f31 100644 --- a/docs/crd/k8s.nginx.org_virtualserverroutes.md +++ b/docs/crd/k8s.nginx.org_virtualserverroutes.md @@ -169,6 +169,7 @@ The `.spec` object supports the following fields: | `upstreams[].buffers.number` | `integer` | Configures the number of buffers. The default is set in the proxy-buffers ConfigMap key. | | `upstreams[].buffers.size` | `string` | Configures the size of a buffer. The default is set in the proxy-buffers ConfigMap key. | | `upstreams[].busy-buffers-size` | `string` | Sets the size of the buffers used for reading a response from the upstream server when the proxy_buffering is enabled. The default is set in the proxy-busy-buffers-size ConfigMap key.' | +| `upstreams[].client-body-buffer-size` | `string` | Sets the size of the buffer used for reading the client request body. | | `upstreams[].client-max-body-size` | `string` | Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap key. | | `upstreams[].connect-timeout` | `string` | The timeout for establishing a connection with an upstream server. The default is specified in the proxy-connect-timeout ConfigMap key. | | `upstreams[].fail-timeout` | `string` | The time during which the specified number of unsuccessful attempts to communicate with an upstream server should happen to consider the server unavailable. The default is set in the fail-timeout ConfigMap key. | diff --git a/docs/crd/k8s.nginx.org_virtualservers.md b/docs/crd/k8s.nginx.org_virtualservers.md index 280e1bb8b2..4366b8824b 100644 --- a/docs/crd/k8s.nginx.org_virtualservers.md +++ b/docs/crd/k8s.nginx.org_virtualservers.md @@ -204,6 +204,7 @@ The `.spec` object supports the following fields: | `upstreams[].buffers.number` | `integer` | Configures the number of buffers. The default is set in the proxy-buffers ConfigMap key. | | `upstreams[].buffers.size` | `string` | Configures the size of a buffer. The default is set in the proxy-buffers ConfigMap key. | | `upstreams[].busy-buffers-size` | `string` | Sets the size of the buffers used for reading a response from the upstream server when the proxy_buffering is enabled. The default is set in the proxy-busy-buffers-size ConfigMap key.' | +| `upstreams[].client-body-buffer-size` | `string` | Sets the size of the buffer used for reading the client request body. | | `upstreams[].client-max-body-size` | `string` | Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap key. | | `upstreams[].connect-timeout` | `string` | The timeout for establishing a connection with an upstream server. The default is specified in the proxy-connect-timeout ConfigMap key. | | `upstreams[].fail-timeout` | `string` | The time during which the specified number of unsuccessful attempts to communicate with an upstream server should happen to consider the server unavailable. The default is set in the fail-timeout ConfigMap key. | diff --git a/internal/configs/annotations.go b/internal/configs/annotations.go index 8e1dc373ce..cf1a37e5b3 100644 --- a/internal/configs/annotations.go +++ b/internal/configs/annotations.go @@ -247,6 +247,14 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool cfgParams.ClientMaxBodySize = clientMaxBodySize } + if clientBodyBufferSize, exists := ingEx.Ingress.Annotations["nginx.org/client-body-buffer-size"]; exists { + size, err := ParseSize(clientBodyBufferSize) + if err != nil { + nl.Errorf(l, "Ingress %s/%s: Invalid value nginx.org/client-body-buffer-size: got %q: %v", ingEx.Ingress.GetNamespace(), ingEx.Ingress.GetName(), clientBodyBufferSize, err) + } + cfgParams.ClientBodyBufferSize = size + } + if redirectToHTTPS, exists, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, "nginx.org/redirect-to-https", ingEx.Ingress); exists { if err != nil { nl.Error(l, err) diff --git a/internal/configs/annotations_test.go b/internal/configs/annotations_test.go index de1d511402..c8f41a0e5d 100644 --- a/internal/configs/annotations_test.go +++ b/internal/configs/annotations_test.go @@ -755,3 +755,175 @@ func TestGetRewriteTargetWithComplexValues(t *testing.T) { }) } } + +func TestClientBodyBufferSizeAnnotationValid(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + clientBodyBufferSize string + expected string + }{ + { + name: "valid size with k", + clientBodyBufferSize: "16k", + expected: "16k", + }, + { + name: "valid size with M", + clientBodyBufferSize: "4M", + expected: "4M", + }, + { + name: "valid size without suffix", + clientBodyBufferSize: "8192", + expected: "8192", + }, + { + name: "single digit", + clientBodyBufferSize: "1k", + expected: "1k", + }, + { + name: "zero value", + clientBodyBufferSize: "0", + expected: "0", + }, + { + name: "large buffer size", + clientBodyBufferSize: "512K", + expected: "512K", + }, + { + name: "megabyte buffer size", + clientBodyBufferSize: "64M", + expected: "64M", + }, + { + name: "empty annotation", + clientBodyBufferSize: "", + expected: "", + }, + { + name: "very large number", + clientBodyBufferSize: "999999999", + expected: "999999999", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ingress := &networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ingress", + Namespace: "default", + Annotations: map[string]string{ + "nginx.org/client-body-buffer-size": tt.clientBodyBufferSize, + }, + }, + } + + ingEx := &IngressEx{ + Ingress: ingress, + } + + baseCfgParams := NewDefaultConfigParams(context.Background(), false) + result := parseAnnotations(ingEx, baseCfgParams, false, false, false, false, false) + + if result.ClientBodyBufferSize != tt.expected { + t.Errorf("Test %q: expected ClientBodyBufferSize %q, got %q", tt.name, tt.expected, result.ClientBodyBufferSize) + } + + result2 := parseAnnotations(ingEx, baseCfgParams, false, false, false, false, false) + if result2.ClientBodyBufferSize != tt.expected { + t.Errorf("Test %q with other annotations: expected ClientBodyBufferSize %q, got %q", tt.name, tt.expected, result2.ClientBodyBufferSize) + } + }) + } +} + +func TestClientBodyBufferSizeAnnotationInvalid(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + annotations map[string]string + }{ + { + name: "annotation name case sensitivity", + annotations: map[string]string{ + "nginx.org/Client-Body-Buffer-Size": "16k", // capital letters + }, + }, + { + name: "invalid time unit h", + annotations: map[string]string{ + "nginx.org/client-body-buffer-size": "4h", + }, + }, + { + name: "invalid random string", + annotations: map[string]string{ + "nginx.org/client-body-buffer-size": "3cd2", // wrong format, too many chars + }, + }, + { + name: "invalid with g suffix", + annotations: map[string]string{ + "nginx.org/client-body-buffer-size": "2g", + }, + }, + + { + name: "invalid negative value", + annotations: map[string]string{ + "nginx.org/client-body-buffer-size": "-16k", + }, + }, + { + name: "invalid with text prefix", + annotations: map[string]string{ + "nginx.org/client-body-buffer-size": "abc16k", + }, + }, + { + name: "invalid with special characters", + annotations: map[string]string{ + "nginx.org/client-body-buffer-size": "16@k", + }, + }, + { + name: "invalid with letters", + annotations: map[string]string{ + "nginx.org/client-body-buffer-size": "abcdef", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + + ingress := &networking.Ingress{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-ingress", + Namespace: "default", + Annotations: tt.annotations, + }, + } + + ingEx := &IngressEx{ + Ingress: ingress, + } + + baseCfgParams := NewDefaultConfigParams(context.Background(), false) + result := parseAnnotations(ingEx, baseCfgParams, false, false, false, false, false) + + if result.ClientBodyBufferSize != "" { + t.Errorf(`Test %q: expected ClientBodyBufferSize %q, got ""`, tt.name, result.ClientBodyBufferSize) + } + }) + } +} diff --git a/internal/configs/config_params.go b/internal/configs/config_params.go index 1410fd45f6..70db61367b 100644 --- a/internal/configs/config_params.go +++ b/internal/configs/config_params.go @@ -12,6 +12,7 @@ import ( type ConfigParams struct { Context context.Context ClientMaxBodySize string + ClientBodyBufferSize string DefaultServerAccessLogOff bool DefaultServerReturn string FailTimeout string @@ -52,6 +53,7 @@ type ConfigParams struct { MainWorkerProcesses string MainWorkerRlimitNofile string MainWorkerShutdownTimeout string + MainClientBodyBufferSize string MaxConns int MaxFails int AppProtectEnable string @@ -247,6 +249,7 @@ func NewDefaultConfigParams(ctx context.Context, isPlus bool) *ConfigParams { MainServerNamesHashMaxSize: "1024", MainMapHashBucketSize: "256", MainMapHashMaxSize: "2048", + MainClientBodyBufferSize: "8k", ProxyBuffering: true, MainWorkerProcesses: "auto", MainWorkerConnections: "1024", diff --git a/internal/configs/configmaps.go b/internal/configs/configmaps.go index c4e514e431..7137e0d6c3 100644 --- a/internal/configs/configmaps.go +++ b/internal/configs/configmaps.go @@ -103,6 +103,17 @@ func ParseConfigMap(ctx context.Context, cfgm *v1.ConfigMap, nginxPlus bool, has cfgParams.ClientMaxBodySize = clientMaxBodySize } + if clientBodyBufferSize, exists := cfgm.Data["client-body-buffer-size"]; exists { + if parsedClientBodyBufferSize, err := ParseSize(clientBodyBufferSize); err != nil { + wrappedError := fmt.Errorf("ConfigMap %s/%s: invalid value for 'client-body-buffer-size': %w", cfgm.GetNamespace(), cfgm.GetName(), err) + nl.Errorf(l, "%s", wrappedError.Error()) + eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, wrappedError.Error()) + configOk = false + } else { + cfgParams.MainClientBodyBufferSize = parsedClientBodyBufferSize + } + } + if serverNamesHashBucketSize, exists := cfgm.Data["server-names-hash-bucket-size"]; exists { cfgParams.MainServerNamesHashBucketSize = serverNamesHashBucketSize } @@ -1146,6 +1157,7 @@ func GenerateNginxMainConfig(staticCfgParams *StaticConfigParams, config *Config ServerNamesHashMaxSize: config.MainServerNamesHashMaxSize, MapHashBucketSize: config.MainMapHashBucketSize, MapHashMaxSize: config.MainMapHashMaxSize, + ClientBodyBufferSize: config.MainClientBodyBufferSize, ServerTokens: config.ServerTokens, SSLCiphers: config.MainServerSSLCiphers, SSLDHParam: config.MainServerSSLDHParam, diff --git a/internal/configs/configmaps_test.go b/internal/configs/configmaps_test.go index 30e054f3be..3e37eaeca4 100644 --- a/internal/configs/configmaps_test.go +++ b/internal/configs/configmaps_test.go @@ -2489,6 +2489,203 @@ func TestParseProxyBuffersInvalidFormat(t *testing.T) { }) } +func TestParseConfigMapClientBodyBufferSizeValid(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value string + expected string + description string + }{ + { + name: "valid size with k suffix", + value: "12k", + expected: "12k", + description: "should accept valid size with k suffix", + }, + { + name: "valid size with K suffix", + value: "16K", + expected: "16K", + description: "should accept valid size with K suffix", + }, + { + name: "valid size with m suffix", + value: "6m", + expected: "6m", + description: "should accept valid size with m suffix", + }, + { + name: "valid size with M suffix", + value: "8M", + expected: "8M", + description: "should accept valid size with M suffix", + }, + { + name: "valid size without suffix", + value: "1024", + expected: "1024", + description: "should accept valid size without suffix", + }, + { + name: "default when not set", + value: "", + expected: "8k", // default value from config_params.go + description: "should use default when not set", + }, + } + + nginxPlus := false + hasAppProtect := false + hasAppProtectDos := false + hasTLSPassthrough := false + directiveAutoadjustEnabled := false + + for _, test := range tests { + test := test // capture range variable + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + }, + } + + if test.value != "" { + cm.Data = map[string]string{ + "client-body-buffer-size": test.value, + } + } + + result, configOk := ParseConfigMap( + context.Background(), + cm, + nginxPlus, + hasAppProtect, + hasAppProtectDos, + hasTLSPassthrough, + directiveAutoadjustEnabled, + makeEventLogger(), + ) + + // Should always pass validation for valid cases + if !configOk { + t.Errorf("ParseConfigMap() for %s should have passed validation but failed", test.description) + } + + if result.MainClientBodyBufferSize != test.expected { + t.Errorf("ParseConfigMap() for %s returned MainClientBodyBufferSize=%q, expected %q", + test.description, result.MainClientBodyBufferSize, test.expected) + } + }) + } +} + +func TestParseConfigMapClientBodyBufferSizeInvalid(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + value string + description string + }{ + { + name: "invalid time unit h", + value: "4h", + description: "should reject invalid time units", + }, + { + name: "invalid random string", + value: "3cd2", + description: "should reject invalid strings", + }, + { + name: "invalid with g suffix", + value: "2g", + description: "should reject unsupported g suffix", + }, + { + name: "invalid with G suffix", + value: "1G", + description: "should reject unsupported G suffix", + }, + { + name: "invalid negative value", + value: "-16k", + description: "should reject negative values", + }, + { + name: "invalid whitespace only", + value: " ", + description: "should reject whitespace-only values", + }, + { + name: "invalid with text prefix", + value: "abc16k", + description: "should reject values with text prefix", + }, + { + name: "invalid with special characters", + value: "16@k", + description: "should reject values with special characters", + }, + { + name: "invalid decimal with unsupported suffix", + value: "16.5g", + description: "should reject decimal values with unsupported suffix", + }, + } + + nginxPlus := false + hasAppProtect := false + hasAppProtectDos := false + hasTLSPassthrough := false + directiveAutoadjustEnabled := false + + for _, test := range tests { + test := test // capture range variable + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-configmap", + Namespace: "default", + }, + Data: map[string]string{ + "client-body-buffer-size": test.value, + }, + } + + result, configOk := ParseConfigMap( + context.Background(), + cm, + nginxPlus, + hasAppProtect, + hasAppProtectDos, + hasTLSPassthrough, + directiveAutoadjustEnabled, + makeEventLogger(), + ) + + // Should always fail validation for invalid cases + if configOk { + t.Errorf("ParseConfigMap() for %s should have failed validation but passed", test.description) + } + + // Should fall back to default value when validation fails + expectedDefault := "8k" // default from config_params.go + if result.MainClientBodyBufferSize != expectedDefault { + t.Errorf("ParseConfigMap() for %s returned MainClientBodyBufferSize=%q, expected default %q", + test.description, result.MainClientBodyBufferSize, expectedDefault) + } + }) + } +} + func TestParseErrorLogLevelToVirtualServer(t *testing.T) { t.Parallel() diff --git a/internal/configs/ingress.go b/internal/configs/ingress.go index 1838498e36..4542ed9f2b 100644 --- a/internal/configs/ingress.go +++ b/internal/configs/ingress.go @@ -498,6 +498,7 @@ func createLocation(path string, upstream version1.Upstream, cfg *ConfigParams, ProxySendTimeout: cfg.ProxySendTimeout, ProxySetHeaders: cfg.ProxySetHeaders, ClientMaxBodySize: cfg.ClientMaxBodySize, + ClientBodyBufferSize: cfg.ClientBodyBufferSize, Websocket: websocket, Rewrite: rewrite, RewriteTarget: rewriteTarget, diff --git a/internal/configs/version1/__snapshots__/template_test.snap b/internal/configs/version1/__snapshots__/template_test.snap index 61a3dbfaef..1e1e31ca44 100644 --- a/internal/configs/version1/__snapshots__/template_test.snap +++ b/internal/configs/version1/__snapshots__/template_test.snap @@ -21,6 +21,7 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -146,6 +147,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -300,6 +303,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -439,6 +444,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -579,6 +586,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -2698,6 +2707,103 @@ server { --- +[TestExecuteTemplate_ForIngressWithClientBodyBufferSize - 1] +# configuration for default/cafe-ingress +upstream test { + zone test 256k; + server 127.0.0.1:8181 max_fails=0 fail_timeout=1s max_conns=0 slow_start=5s;keepalive 16; +} + + + + +server { + listen 443 ssl;listen [::]:443 ssl; + ssl_certificate secret.pem; + ssl_certificate_key secret.pem; + + server_tokens "off"; + + server_name test.example.com; + + status_zone test.example.com; + set $resource_type "ingress"; + set $resource_name "cafe-ingress"; + set $resource_namespace "default"; + app_protect_enable on; + app_protect_policy_file /etc/nginx/waf/nac-policies/default-dataguard-alarm; + app_protect_security_log_enable on; + app_protect_security_log /etc/nginx/waf/nac-logconfs/test_logconf syslog:server=127.0.0.1:514; + app_protect_security_log /etc/nginx/waf/nac-logconfs/test_logconf2; + + app_protect_dos_enable on; + app_protect_dos_policy_file /test/policy.json; + app_protect_dos_security_log_enable on; + app_protect_dos_security_log /test/logConf.json; + set $loggable '0'; + # app-protect-dos module will set it to '1' if a request doesn't pass the rate limit + access_log /var/log/dos log_dos if=$loggable; + app_protect_dos_monitor uri=/path/to/monitor protocol=http1 timeout=30; + app_protect_dos_name "testdos"; + app_protect_dos_access_file "/etc/nginx/dos/allowlist/default_test.example.com"; + + + if ($scheme = http) { + return 301 https://$host:443$request_uri; + } + + + auth_jwt_key_file /etc/nginx/secrets/key.jwk; + auth_jwt "closed site" token=$cookie_auth_token; + error_page 401 @login_url-default-cafe-ingress; + + location @hc-test { + proxy_set_header Test-Header "test-header-value"; + proxy_connect_timeout 0s; + proxy_read_timeout 0s; + proxy_send_timeout 0s; + proxy_pass ://test; + health_check uri= interval=1s fails=1 passes=1; + } + + location @login_url-default-cafe-ingress { + internal; + return 302 https://test.example.com/login; + } + + location /tea { + set $service ""; + status_zone ""; + # location for minion default/tea-minion + set $resource_name "tea-minion"; + set $resource_namespace "default"; + proxy_http_version 1.1; + proxy_set_header Connection ""; + auth_jwt_key_file /etc/nginx/secrets/location-key.jwk; + auth_jwt "closed site" token=$cookie_auth_token; + + proxy_connect_timeout 10s; + proxy_read_timeout 10s; + proxy_send_timeout 10s; + client_max_body_size 2m; + client_body_buffer_size 16k; + + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_buffering off; + proxy_pass http://test; + + + } + +} + +--- + [TestExecuteTemplate_ForMainForNGINXPlusTLSPassthroughPortDisabled - 1] worker_processes auto; worker_rlimit_nofile 65536; @@ -2722,6 +2828,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -2866,6 +2974,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -3010,6 +3120,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -3154,6 +3266,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -3298,6 +3412,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -3462,6 +3578,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -3621,6 +3739,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -3774,6 +3894,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -3918,6 +4040,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -4067,6 +4191,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -4224,6 +4350,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -4384,6 +4512,7 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -4505,6 +4634,7 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -4626,6 +4756,7 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -4747,6 +4878,7 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -4868,6 +5000,7 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -5008,6 +5141,7 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -5129,6 +5263,7 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -5251,6 +5386,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -5398,6 +5535,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -5543,6 +5682,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -5688,6 +5829,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -5833,6 +5976,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -5978,6 +6123,8 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -6124,6 +6271,7 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; @@ -6245,6 +6393,7 @@ http { map_hash_max_size ; map_hash_bucket_size ; + client_body_buffer_size ; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go index 65a617737b..317157db97 100644 --- a/internal/configs/version1/config.go +++ b/internal/configs/version1/config.go @@ -175,6 +175,7 @@ type Location struct { ProxySendTimeout string ProxySetHeaders []version2.Header ClientMaxBodySize string + ClientBodyBufferSize string Websocket bool Rewrite string RewriteTarget string @@ -272,6 +273,7 @@ type MainConfig struct { ServerNamesHashMaxSize string MapHashBucketSize string MapHashMaxSize string + ClientBodyBufferSize string ServerTokens string SSLRejectHandshake bool SSLCiphers string diff --git a/internal/configs/version1/nginx-plus.ingress.tmpl b/internal/configs/version1/nginx-plus.ingress.tmpl index a7b343aa75..25b2049e5d 100644 --- a/internal/configs/version1/nginx-plus.ingress.tmpl +++ b/internal/configs/version1/nginx-plus.ingress.tmpl @@ -291,6 +291,9 @@ server { proxy_read_timeout {{$location.ProxyReadTimeout}}; proxy_send_timeout {{$location.ProxySendTimeout}}; client_max_body_size {{$location.ClientMaxBodySize}}; + {{- if $location.ClientBodyBufferSize }} + client_body_buffer_size {{$location.ClientBodyBufferSize}}; + {{- end}} {{- $proxySetHeaders := generateProxySetHeaders $location $.Ingress.Annotations }} {{$proxySetHeaders}} proxy_set_header Host $host; diff --git a/internal/configs/version1/nginx-plus.tmpl b/internal/configs/version1/nginx-plus.tmpl index 79bb9b0158..5a83bdd58c 100644 --- a/internal/configs/version1/nginx-plus.tmpl +++ b/internal/configs/version1/nginx-plus.tmpl @@ -37,6 +37,8 @@ http { map_hash_max_size {{.MapHashMaxSize}}; map_hash_bucket_size {{.MapHashBucketSize}}; + client_body_buffer_size {{.ClientBodyBufferSize}}; + js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; diff --git a/internal/configs/version1/nginx.ingress.tmpl b/internal/configs/version1/nginx.ingress.tmpl index 47e28c5bbb..70d8f2cfa5 100644 --- a/internal/configs/version1/nginx.ingress.tmpl +++ b/internal/configs/version1/nginx.ingress.tmpl @@ -198,6 +198,9 @@ server { proxy_read_timeout {{$location.ProxyReadTimeout}}; proxy_send_timeout {{$location.ProxySendTimeout}}; client_max_body_size {{$location.ClientMaxBodySize}}; + {{- if $location.ClientBodyBufferSize }} + client_body_buffer_size {{$location.ClientBodyBufferSize}}; + {{- end}} {{- $proxySetHeaders := generateProxySetHeaders $location $.Ingress.Annotations -}} {{$proxySetHeaders}} proxy_set_header Host $host; diff --git a/internal/configs/version1/nginx.tmpl b/internal/configs/version1/nginx.tmpl index e5f9bf3e2c..4119a78160 100644 --- a/internal/configs/version1/nginx.tmpl +++ b/internal/configs/version1/nginx.tmpl @@ -30,6 +30,7 @@ http { map_hash_max_size {{.MapHashMaxSize}}; map_hash_bucket_size {{.MapHashBucketSize}}; + client_body_buffer_size {{.ClientBodyBufferSize}}; js_import /etc/nginx/njs/apikey_auth.js; js_set $apikey_auth_hash apikey_auth.hash; diff --git a/internal/configs/version1/template_test.go b/internal/configs/version1/template_test.go index 0f0d6b7714..d12f26a194 100644 --- a/internal/configs/version1/template_test.go +++ b/internal/configs/version1/template_test.go @@ -303,6 +303,28 @@ func TestExecuteTemplate_ForMergeableIngressWithOneMinionWithPathRegexAnnotation snaps.MatchSnapshot(t, buf.String()) } +func TestExecuteTemplate_ForIngressWithClientBodyBufferSize(t *testing.T) { + t.Parallel() + + tmpl := newNGINXPlusIngressTmpl(t) + buf := &bytes.Buffer{} + + testCfg := ingressCfg + testCfg.Servers[0].Locations[0].ClientBodyBufferSize = "16k" + + err := tmpl.Execute(buf, testCfg) + t.Log(buf.String()) + if err != nil { + t.Fatal(err) + } + + want := "client_body_buffer_size 16k;" + if !strings.Contains(buf.String(), want) { + t.Errorf("want %q in generated config", want) + } + snaps.MatchSnapshot(t, buf.String()) +} + func TestExecuteTemplate_ForMergeableIngressWithSecondMinionWithPathRegexAnnotation(t *testing.T) { t.Parallel() diff --git a/internal/configs/version2/__snapshots__/templates_test.snap b/internal/configs/version2/__snapshots__/templates_test.snap index 50f2fe69b4..c2f290d3ad 100644 --- a/internal/configs/version2/__snapshots__/templates_test.snap +++ b/internal/configs/version2/__snapshots__/templates_test.snap @@ -1876,6 +1876,490 @@ server { --- +[TestExecuteVirtualServerTemplate_RendersTemplateDefaultClientBodyBufferSize - 1] + +upstream test-upstream { + zone test-upstream 256k; + random; + server 10.0.0.20:8001 max_fails=4 fail_timeout=10s slow_start=10s max_conns=31; + keepalive 32; + queue 10 timeout=60s; + sticky cookie test expires=25s path=/tea; + ntlm; +} + +upstream coffee-v1 { + zone coffee-v1 256k; + server 10.0.0.31:8001 max_fails=8 fail_timeout=15s max_conns=2; +} + +upstream coffee-v2 { + zone coffee-v2 256k; + server 10.0.0.32:8001 max_fails=12 fail_timeout=20s max_conns=4; +} + +split_clients $request_id $split_0 { + 50% @loc0; + 50% @loc1; +} +map $match_0_0 $match { + ~^1 @match_loc_0; + default @match_loc_default; +} +map $http_x_version $match_0_0 { + v2 1; + default 0; +} +# HTTP snippet +limit_req_zone $url zone=pol_rl_test_test_test:10m rate=10r/s; +keyval $idp_sid $client_sid zone=oidc_sids; + +server { + listen 80 proxy_protocol; + listen [::]:80 proxy_protocol; + + + server_name example.com; + status_zone example.com; + set $resource_type "virtualserver"; + set $resource_name ""; + set $resource_namespace ""; + include oidc/oidc.conf; + + set $oidc_pkce_enable 0; + set $oidc_client_auth_method "client_secret_post"; + set $oidc_logout_redirect "https://example.com/logout"; + set $oidc_hmac_key ""; + set $zone_sync_leeway 0; + + set $oidc_authz_endpoint "https://idp.example.com/auth"; + set $oidc_authz_extra_args ""; + set $oidc_token_endpoint "https://idp.example.com/token"; + set $oidc_end_session_endpoint "https://idp.example.com/logout"; + set $oidc_jwt_keyfile "https://idp.example.com/jwks"; + set $oidc_scopes "openid+profile+email"; + set $oidc_client "test-client"; + set $oidc_client_secret "test-secret"; + set $redir_location "/custom-location"; + # Custom OIDC redirect location based on policy redirectURI + location = /custom-location { + status_zone "OIDC code exchange"; + js_content oidc.codeExchange; + error_page 500 502 504 @oidc_error; + } + listen 443 ssl proxy_protocol; + listen [::]:443 ssl proxy_protocol; + + http2 on; + ssl_certificate cafe-secret.pem; + ssl_certificate_key cafe-secret.pem; + ssl_client_certificate ingress-mtls-secret; + ssl_verify_client on; + ssl_verify_depth 2; + if ($scheme = 'http') { + return 301 https://$host$request_uri; + } + + server_tokens "off"; + set_real_ip_from 0.0.0.0/0; + real_ip_header X-Real-IP; + real_ip_recursive on; + allow 127.0.0.1; + deny all; + deny 127.0.0.1; + allow all; + limit_req_log_level error; + limit_req_status 503; + limit_req zone=pol_rl_test_test_test burst=5 delay=10; + auth_jwt "My Api"; + auth_jwt_key_file jwk-secret; + app_protect_enable on; + app_protect_policy_file /etc/nginx/waf/nac-policies/default-dataguard-alarm; + app_protect_security_log_enable on; + app_protect_security_log /etc/nginx/waf/nac-logconfs/default-logconf; + + # server snippet + location /split { + rewrite ^ @split_0 last; + } + location /coffee { + rewrite ^ @match last; + } + location @hc-coffee { + + proxy_connect_timeout ; + proxy_read_timeout ; + proxy_send_timeout ; + proxy_pass http://coffee-v2; + health_check uri=/ port=50 interval=5s jitter=0s fails=1 passes=1 mandatory persistent keepalive_time=60s; + + } + location @hc-tea { + + grpc_connect_timeout ; + grpc_read_timeout ; + grpc_send_timeout ; + grpc_pass grpc://tea-v3; + health_check port=50 interval=5s jitter=0s fails=1 passes=1 type=grpc grpc_status=12 grpc_service=tea-servicev2; + + } + location @vs_cafe_cafe_vsr_tea_tea_tea__tea_error_page_0 { + + default_type "application/json"; + + + # status code is ignored here, using 0 + return 0 "Hello World"; + } + + location @vs_cafe_cafe_vsr_tea_tea_tea__tea_error_page_1 { + + + add_header Set-Cookie "cookie1=test" always; + + add_header Set-Cookie "cookie2=test; Secure" always; + + # status code is ignored here, using 0 + return 0 "Hello World"; + } + + + + location @return_0 { + default_type "text/html"; + + # status code is ignored here, using 0 + return 0 "Hello!"; + } + + + + location / { + set $service ""; + status_zone ""; + internal; + # location snippet + allow 127.0.0.1; + deny all; + deny 127.0.0.1; + allow all; + limit_req zone=loc_pol_rl_test_test_test; + + + proxy_ssl_certificate egress-mtls-secret.pem; + proxy_ssl_certificate_key egress-mtls-secret.pem; + + proxy_ssl_trusted_certificate trusted-cert.pem; + proxy_ssl_verify on; + proxy_ssl_verify_depth 1; + proxy_ssl_protocols TLSv1.3; + proxy_ssl_ciphers DEFAULT; + proxy_ssl_session_reuse on; + proxy_ssl_server_name on; + proxy_ssl_name ; + set $default_connection_header close; + rewrite $request_uri $request_uri; + rewrite $request_uri $request_uri; + proxy_connect_timeout 30s; + proxy_read_timeout 31s; + proxy_send_timeout 32s; + client_max_body_size 1m; + client_body_buffer_size 8k; + proxy_max_temp_file_size 1024m; + + proxy_buffering on; + proxy_buffers 8 4k; + proxy_buffer_size 4k; + proxy_busy_buffers_size 8k; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $vs_connection_header; + proxy_pass_request_headers off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_hide_header Header; + proxy_pass_header Host; + proxy_ignore_headers Cache; + add_header Header-Name "Header Value" always; + proxy_pass http://test-upstream$request_uri; + proxy_next_upstream error timeout; + proxy_next_upstream_timeout 5s; + proxy_next_upstream_tries 0; + } + location @loc0 { + set $service ""; + status_zone ""; + + + error_page 400 500 =200 "@error_page_1"; + error_page 500 "@error_page_2"; + proxy_intercept_errors on; + set $default_connection_header close; + proxy_connect_timeout 30s; + proxy_read_timeout 31s; + proxy_send_timeout 32s; + client_max_body_size 1m; + + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $vs_connection_header; + proxy_pass_request_headers off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://coffee-v1; + proxy_next_upstream error timeout; + proxy_next_upstream_timeout 5s; + proxy_next_upstream_tries 0; + } + location @loc1 { + set $service ""; + status_zone ""; + + + set $default_connection_header close; + proxy_connect_timeout 30s; + proxy_read_timeout 31s; + proxy_send_timeout 32s; + client_max_body_size 1m; + + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $vs_connection_header; + proxy_pass_request_headers off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://coffee-v2; + proxy_next_upstream error timeout; + proxy_next_upstream_timeout 5s; + proxy_next_upstream_tries 0; + } + location @loc2 { + set $service ""; + status_zone ""; + + + error_page 400 = @grpc_internal; + error_page 401 = @grpc_unauthenticated; + error_page 403 = @grpc_permission_denied; + error_page 404 = @grpc_unimplemented; + error_page 429 = @grpc_unavailable; + error_page 502 = @grpc_unavailable; + error_page 503 = @grpc_unavailable; + error_page 504 = @grpc_unavailable; + error_page 405 = @grpc_internal; + error_page 408 = @grpc_deadline_exceeded; + error_page 413 = @grpc_resource_exhausted; + error_page 414 = @grpc_resource_exhausted; + error_page 415 = @grpc_internal; + error_page 426 = @grpc_internal; + error_page 495 = @grpc_unauthenticated; + error_page 496 = @grpc_unauthenticated; + error_page 497 = @grpc_internal; + error_page 500 = @grpc_internal; + error_page 501 = @grpc_internal; + set $default_connection_header close; + grpc_connect_timeout 30s; + grpc_read_timeout 31s; + grpc_send_timeout 32s; + client_max_body_size 1m; + + proxy_buffering off; + grpc_set_header X-Real-IP $remote_addr; + grpc_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + grpc_set_header X-Forwarded-Host $host; + grpc_set_header X-Forwarded-Port $server_port; + grpc_set_header X-Forwarded-Proto $scheme; + grpc_pass grpc://coffee-v3; + grpc_next_upstream ; + grpc_next_upstream_timeout ; + grpc_next_upstream_tries 0; + } + location @match_loc_0 { + set $service ""; + status_zone ""; + + + set $default_connection_header close; + proxy_connect_timeout 30s; + proxy_read_timeout 31s; + proxy_send_timeout 32s; + client_max_body_size 1m; + + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $vs_connection_header; + proxy_pass_request_headers off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://coffee-v2; + proxy_next_upstream error timeout; + proxy_next_upstream_timeout 5s; + proxy_next_upstream_tries 0; + } + location @match_loc_default { + set $service ""; + status_zone ""; + + + set $default_connection_header close; + proxy_connect_timeout 30s; + proxy_read_timeout 31s; + proxy_send_timeout 32s; + client_max_body_size 1m; + + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $vs_connection_header; + proxy_pass_request_headers off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://coffee-v1; + proxy_next_upstream error timeout; + proxy_next_upstream_timeout 5s; + proxy_next_upstream_tries 0; + } + location /return { + set $service ""; + status_zone ""; + + + error_page 418 =200 "@return_0"; + proxy_intercept_errors on; + proxy_pass http://unix:/var/lib/nginx/nginx-418-server.sock; + set $default_connection_header close; + } + + location @grpc_deadline_exceeded { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 4; + add_header grpc-message 'deadline exceeded'; + return 204; + } + + location @grpc_permission_denied { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 7; + add_header grpc-message 'permission denied'; + return 204; + } + + location @grpc_resource_exhausted { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 8; + add_header grpc-message 'resource exhausted'; + return 204; + } + + location @grpc_unimplemented { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 12; + add_header grpc-message unimplemented; + return 204; + } + + location @grpc_internal { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 13; + add_header grpc-message 'internal error'; + return 204; + } + + location @grpc_unavailable { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 14; + add_header grpc-message unavailable; + return 204; + } + + location @grpc_unauthenticated { + default_type application/grpc; + add_header content-type application/grpc; + add_header grpc-status 16; + add_header grpc-message unauthenticated; + return 204; + } + + + +} + +--- + +[TestExecuteVirtualServerTemplate_RendersTemplateWithClientBodyBufferSize - 1] + + +server { + listen 80; + listen [::]:80; + + + server_name example.com; + status_zone example.com; + set $resource_type "virtualserver"; + set $resource_name ""; + set $resource_namespace ""; + + server_tokens ""; + + + + + location / { + set $service ""; + status_zone ""; + + + set $default_connection_header close; + proxy_connect_timeout ; + proxy_read_timeout ; + proxy_send_timeout ; + client_max_body_size ; + client_body_buffer_size 16k; + + proxy_buffering off; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $vs_connection_header; + proxy_pass_request_headers off; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Forwarded-Port $server_port; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_pass http://test-upstream; + proxy_next_upstream ; + proxy_next_upstream_timeout ; + proxy_next_upstream_tries 0; + } +} + +--- + [TestExecuteVirtualServerTemplate_RendersTemplateWithCustomListener - 1] @@ -3052,6 +3536,7 @@ server { proxy_read_timeout 31s; proxy_send_timeout 32s; client_max_body_size 1m; + client_body_buffer_size 8k; proxy_max_temp_file_size 1024m; proxy_buffering on; diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index ea806bd563..da730fcf95 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -198,6 +198,7 @@ type Location struct { ProxyReadTimeout string ProxySendTimeout string ClientMaxBodySize string + ClientBodyBufferSize string ProxyMaxTempFileSize string ProxyBuffering bool ProxyBuffers string diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index 4886cc2463..9133427225 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -674,6 +674,9 @@ server { {{ $proxyOrGRPC }}_read_timeout {{ $l.ProxyReadTimeout }}; {{ $proxyOrGRPC }}_send_timeout {{ $l.ProxySendTimeout }}; client_max_body_size {{ $l.ClientMaxBodySize }}; + {{- if $l.ClientBodyBufferSize }} + client_body_buffer_size {{ $l.ClientBodyBufferSize }}; + {{- end }} {{- if $l.ProxyMaxTempFileSize }} proxy_max_temp_file_size {{ $l.ProxyMaxTempFileSize }}; diff --git a/internal/configs/version2/nginx.virtualserver.tmpl b/internal/configs/version2/nginx.virtualserver.tmpl index 13432de9d0..f4468e9464 100644 --- a/internal/configs/version2/nginx.virtualserver.tmpl +++ b/internal/configs/version2/nginx.virtualserver.tmpl @@ -368,6 +368,9 @@ server { {{ $proxyOrGRPC }}_read_timeout {{ $l.ProxyReadTimeout }}; {{ $proxyOrGRPC }}_send_timeout {{ $l.ProxySendTimeout }}; client_max_body_size {{ $l.ClientMaxBodySize }}; + {{- if $l.ClientBodyBufferSize }} + client_body_buffer_size {{ $l.ClientBodyBufferSize }}; + {{- end }} {{- if $l.ProxyMaxTempFileSize }} proxy_max_temp_file_size {{ $l.ProxyMaxTempFileSize }}; diff --git a/internal/configs/version2/templates_test.go b/internal/configs/version2/templates_test.go index 7b5253f92a..f101d6e3ae 100644 --- a/internal/configs/version2/templates_test.go +++ b/internal/configs/version2/templates_test.go @@ -440,6 +440,36 @@ func TestExecuteVirtualServerTemplate_RendersPlusTemplateWithHTTP2Off(t *testing t.Log(string(got)) } +func TestExecuteVirtualServerTemplate_RendersTemplateWithClientBodyBufferSize(t *testing.T) { + t.Parallel() + executor := newTmplExecutorNGINXPlus(t) + + got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfgWithClientBodyBufferSize) + if err != nil { + t.Error(err) + } + if !bytes.Contains(got, []byte("client_body_buffer_size 16k;")) { + t.Error("want `client_body_buffer_size 16k;` directive in generated template") + } + snaps.MatchSnapshot(t, string(got)) + t.Log(string(got)) +} + +func TestExecuteVirtualServerTemplate_RendersTemplateDefaultClientBodyBufferSize(t *testing.T) { + t.Parallel() + executor := newTmplExecutorNGINXPlus(t) + + got, err := executor.ExecuteVirtualServerTemplate(&virtualServerCfg) + if err != nil { + t.Error(err) + } + if !bytes.Contains(got, []byte("client_body_buffer_size 8k;")) { + t.Error("want `client_body_buffer_size 8k;` directive in generated template") + } + snaps.MatchSnapshot(t, string(got)) + t.Log(string(got)) +} + func TestExecuteVirtualServerTemplate_RendersOSSTemplateWithHTTP2On(t *testing.T) { t.Parallel() executor := newTmplExecutorNGINX(t) @@ -1594,6 +1624,7 @@ var ( ProxyReadTimeout: "31s", ProxySendTimeout: "32s", ClientMaxBodySize: "1m", + ClientBodyBufferSize: "8k", ProxyBuffering: true, ProxyBuffers: "8 4k", ProxyBufferSize: "4k", @@ -2171,6 +2202,20 @@ var ( }, } + virtualServerCfgWithClientBodyBufferSize = VirtualServerConfig{ + Server: Server{ + ServerName: "example.com", + StatusZone: "example.com", + Locations: []Location{ + { + Path: "/", + ProxyPass: "http://test-upstream", + ClientBodyBufferSize: "16k", + }, + }, + }, + } + virtualServerCfgWithRateLimitJWTClaim = VirtualServerConfig{ LimitReqZones: []LimitReqZone{ { diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 8dc42d007a..9696afb174 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -2647,6 +2647,7 @@ func generateLocationForProxying(path string, upstreamName string, upstream conf ProxyReadTimeout: generateTimeWithDefault(upstream.ProxyReadTimeout, cfgParams.ProxyReadTimeout), ProxySendTimeout: generateTimeWithDefault(upstream.ProxySendTimeout, cfgParams.ProxySendTimeout), ClientMaxBodySize: generateString(upstream.ClientMaxBodySize, cfgParams.ClientMaxBodySize), + ClientBodyBufferSize: generateString(upstream.ClientBodyBufferSize, cfgParams.ClientBodyBufferSize), ProxyMaxTempFileSize: cfgParams.ProxyMaxTempFileSize, ProxyBuffering: generateBool(upstream.ProxyBuffering, cfgParams.ProxyBuffering), ProxyBuffers: generateBuffers(upstream.ProxyBuffers, cfgParams.ProxyBuffers), diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index bf5f8d78d1..d9dd0cf516 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -16221,6 +16221,7 @@ func TestGenerateLocationForProxying(t *testing.T) { ProxyReadTimeout: "31s", ProxySendTimeout: "32s", ClientMaxBodySize: "1m", + ClientBodyBufferSize: "16k", ProxyMaxTempFileSize: "1024m", ProxyBuffering: true, ProxyBuffers: "8 4k", @@ -16238,6 +16239,7 @@ func TestGenerateLocationForProxying(t *testing.T) { ProxyReadTimeout: "31s", ProxySendTimeout: "32s", ClientMaxBodySize: "1m", + ClientBodyBufferSize: "16k", ProxyMaxTempFileSize: "1024m", ProxyBuffering: true, ProxyBuffers: "8 4k", @@ -16269,6 +16271,7 @@ func TestGenerateLocationForGrpcProxying(t *testing.T) { ProxyReadTimeout: "31s", ProxySendTimeout: "32s", ClientMaxBodySize: "1m", + ClientBodyBufferSize: "16k", ProxyMaxTempFileSize: "1024m", ProxyBuffering: true, ProxyBuffers: "8 4k", @@ -16288,6 +16291,7 @@ func TestGenerateLocationForGrpcProxying(t *testing.T) { ProxyReadTimeout: "31s", ProxySendTimeout: "32s", ClientMaxBodySize: "1m", + ClientBodyBufferSize: "16k", ProxyMaxTempFileSize: "1024m", ProxyBuffering: true, ProxyBuffers: "8 4k", diff --git a/internal/k8s/validation.go b/internal/k8s/validation.go index a6669932a7..b7ee366774 100644 --- a/internal/k8s/validation.go +++ b/internal/k8s/validation.go @@ -34,6 +34,7 @@ const ( proxyPassHeadersAnnotation = "nginx.org/proxy-pass-headers" // #nosec G101 proxySetHeadersAnnotation = "nginx.org/proxy-set-headers" clientMaxBodySizeAnnotation = "nginx.org/client-max-body-size" + clientBodyBufferSizeAnnotation = "nginx.org/client-body-buffer-size" redirectToHTTPSAnnotation = "nginx.org/redirect-to-https" sslRedirectAnnotation = "ingress.kubernetes.io/ssl-redirect" proxyBufferingAnnotation = "nginx.org/proxy-buffering" @@ -179,6 +180,10 @@ var ( validateRequiredAnnotation, validateOffsetAnnotation, }, + clientBodyBufferSizeAnnotation: { + validateRequiredAnnotation, + validateSizeAnnotation, + }, redirectToHTTPSAnnotation: { validateRequiredAnnotation, validateBoolAnnotation, diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 1f3dd4a6b7..a2fdfcbc91 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -157,6 +157,8 @@ type Upstream struct { ProxyBusyBuffersSize string `json:"busy-buffers-size"` // Sets the maximum allowed size of the client request body. The default is set in the client-max-body-size ConfigMap key. ClientMaxBodySize string `json:"client-max-body-size"` + // Sets the size of the buffer used for reading the client request body. + ClientBodyBufferSize string `json:"client-body-buffer-size"` // The TLS configuration for the Upstream. TLS UpstreamTLS `json:"tls"` // The health check configuration for the Upstream. Note: this feature is supported only in NGINX Plus. diff --git a/pkg/apis/configuration/validation/virtualserver.go b/pkg/apis/configuration/validation/virtualserver.go index e5edfff294..b8e5e57cac 100644 --- a/pkg/apis/configuration/validation/virtualserver.go +++ b/pkg/apis/configuration/validation/virtualserver.go @@ -634,6 +634,7 @@ func (vsv *VirtualServerValidator) validateUpstreams(upstreams []v1.Upstream, fi allErrs = append(allErrs, validatePositiveIntOrZeroFromPointer(u.Keepalive, idxPath.Child("keepalive"))...) allErrs = append(allErrs, validatePositiveIntOrZeroFromPointer(u.MaxConns, idxPath.Child("max-conns"))...) allErrs = append(allErrs, validateOffset(u.ClientMaxBodySize, idxPath.Child("client-max-body-size"))...) + allErrs = append(allErrs, validateSize(u.ClientBodyBufferSize, idxPath.Child("client-body-buffer-size"))...) allErrs = append(allErrs, validateUpstreamHealthCheck(u.HealthCheck, u.Type, idxPath.Child("healthCheck"))...) allErrs = append(allErrs, validateTime(u.SlowStart, idxPath.Child("slow-start"))...) allErrs = append(allErrs, validateBuffer(u.ProxyBuffers, idxPath.Child("buffers"))...) diff --git a/pkg/apis/configuration/validation/virtualserver_test.go b/pkg/apis/configuration/validation/virtualserver_test.go index 3a2c2edcda..27d84f9d53 100644 --- a/pkg/apis/configuration/validation/virtualserver_test.go +++ b/pkg/apis/configuration/validation/virtualserver_test.go @@ -550,6 +550,7 @@ func TestValidateUpstreams(t *testing.T) { ProxyNextUpstreamTries: 5, MaxConns: createPointerFromInt(16), Type: "grpc", + ClientBodyBufferSize: "16k", }, { Name: "upstream2", @@ -743,6 +744,20 @@ func TestValidateUpstreamsFails(t *testing.T) { }, msg: "invalid value for ClientMaxBodySize", }, + { + upstreams: []v1.Upstream{ + { + Name: "upstream1", + Service: "test-1", + Port: 80, + ClientBodyBufferSize: "10hello", + }, + }, + expectedUpstreamNames: map[string]sets.Empty{ + "upstream1": {}, + }, + msg: "invalid value for ClientBodyBufferSize", + }, { upstreams: []v1.Upstream{ {