diff --git a/CHANGELOG.md b/CHANGELOG.md index b014a98a..88eb3b8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ This document outlines major changes between releases. - Add new `reconnect_interval` config param (#291) - Support `GetBucketPolicyStatus` (#301) - Add FrostfsID cache (#269) +- Add new `source_ip_header` config param (#371) ### Changed - Generalise config param `use_default_xmlns_for_complete_multipart` to `use_default_xmlns` so that use default xmlns for all requests (#221) diff --git a/api/handler/handlers_test.go b/api/handler/handlers_test.go index eb677d54..b3be178b 100644 --- a/api/handler/handlers_test.go +++ b/api/handler/handlers_test.go @@ -415,7 +415,7 @@ func prepareTestRequestWithQuery(hc *handlerContext, bktName, objName string, qu r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(body)) r.URL.RawQuery = query.Encode() - reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}) + reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "") r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo)) return w, r @@ -425,7 +425,7 @@ func prepareTestPayloadRequest(hc *handlerContext, bktName, objName string, payl w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPut, defaultURL, payload) - reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}) + reqInfo := middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "") r = r.WithContext(middleware.SetReqInfo(hc.Context(), reqInfo)) return w, r diff --git a/api/handler/locking_test.go b/api/handler/locking_test.go index d4bb13eb..234e7774 100644 --- a/api/handler/locking_test.go +++ b/api/handler/locking_test.go @@ -315,7 +315,7 @@ func TestPutBucketLockConfigurationHandler(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(body)) - r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket}))) + r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket}, ""))) hc.Handler().PutBucketObjectLockConfigHandler(w, r) @@ -388,7 +388,7 @@ func TestGetBucketLockConfigurationHandler(t *testing.T) { t.Run(tc.name, func(t *testing.T) { w := httptest.NewRecorder() r := httptest.NewRequest(http.MethodPut, defaultURL, bytes.NewReader(nil)) - r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket}))) + r = r.WithContext(middleware.SetReqInfo(r.Context(), middleware.NewReqInfo(w, r, middleware.ObjectRequest{Bucket: tc.bucket}, ""))) hc.Handler().GetBucketObjectLockConfigHandler(w, r) diff --git a/api/handler/put_test.go b/api/handler/put_test.go index 4ff00daa..cad7416d 100644 --- a/api/handler/put_test.go +++ b/api/handler/put_test.go @@ -351,7 +351,7 @@ func getChunkedRequest(ctx context.Context, t *testing.T, bktName, objName strin req.Body = io.NopCloser(reqBody) w := httptest.NewRecorder() - reqInfo := middleware.NewReqInfo(w, req, middleware.ObjectRequest{Bucket: bktName, Object: objName}) + reqInfo := middleware.NewReqInfo(w, req, middleware.ObjectRequest{Bucket: bktName, Object: objName}, "") req = req.WithContext(middleware.SetReqInfo(ctx, reqInfo)) req = req.WithContext(middleware.SetBox(req.Context(), &middleware.Box{ ClientTime: signTime, diff --git a/api/middleware/reqinfo.go b/api/middleware/reqinfo.go index 2b071c91..b7f53c26 100644 --- a/api/middleware/reqinfo.go +++ b/api/middleware/reqinfo.go @@ -83,17 +83,24 @@ var ( ) // NewReqInfo returns new ReqInfo based on parameters. -func NewReqInfo(w http.ResponseWriter, r *http.Request, req ObjectRequest) *ReqInfo { - return &ReqInfo{ +func NewReqInfo(w http.ResponseWriter, r *http.Request, req ObjectRequest, sourceIPHeader string) *ReqInfo { + reqInfo := &ReqInfo{ API: req.Method, BucketName: req.Bucket, ObjectName: req.Object, UserAgent: r.UserAgent(), - RemoteHost: getSourceIP(r), RequestID: GetRequestID(w), DeploymentID: deploymentID.String(), URL: r.URL, } + + if sourceIPHeader != "" { + reqInfo.RemoteHost = r.Header.Get(sourceIPHeader) + } else { + reqInfo.RemoteHost = getSourceIP(r) + } + + return reqInfo } // AppendTags -- appends key/val to ReqInfo.tags. @@ -193,6 +200,7 @@ func GetReqLog(ctx context.Context) *zap.Logger { type RequestSettings interface { NamespaceHeader() string ResolveNamespaceAlias(string) string + SourceIPHeader() string } func Request(log *zap.Logger, settings RequestSettings) Func { @@ -211,7 +219,7 @@ func Request(log *zap.Logger, settings RequestSettings) Func { // set request info into context // bucket name and object will be set in reqInfo later (limitation of go-chi) - reqInfo := NewReqInfo(w, r, ObjectRequest{}) + reqInfo := NewReqInfo(w, r, ObjectRequest{}, settings.SourceIPHeader()) reqInfo.Namespace = settings.ResolveNamespaceAlias(r.Header.Get(settings.NamespaceHeader())) r = r.WithContext(SetReqInfo(r.Context(), reqInfo)) @@ -317,11 +325,14 @@ func getSourceIP(r *http.Request) string { } } - if addr != "" { - return addr + if addr == "" { + addr = r.RemoteAddr } // Default to remote address if headers not set. - addr, _, _ = net.SplitHostPort(r.RemoteAddr) - return addr + raddr, _, _ := net.SplitHostPort(addr) + if raddr == "" { + return addr + } + return raddr } diff --git a/api/router_mock_test.go b/api/router_mock_test.go index 49ff8650..8c996da7 100644 --- a/api/router_mock_test.go +++ b/api/router_mock_test.go @@ -64,6 +64,10 @@ type middlewareSettingsMock struct { aclEnabled bool } +func (r *middlewareSettingsMock) SourceIPHeader() string { + return "" +} + func (r *middlewareSettingsMock) NamespaceHeader() string { return FrostfsNamespaceHeader } diff --git a/cmd/s3-gw/app.go b/cmd/s3-gw/app.go index 2a989470..32cddd47 100644 --- a/cmd/s3-gw/app.go +++ b/cmd/s3-gw/app.go @@ -109,6 +109,7 @@ type ( defaultNamespaces []string authorizedControlAPIKeys [][]byte policyDenyByDefault bool + sourceIPHeader string } maxClientsConfig struct { @@ -235,6 +236,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger, key *keys.PrivateK s.setMD5Enabled(v.GetBool(cfgMD5Enabled)) s.setAuthorizedControlAPIKeys(append(fetchAuthorizedKeys(log, v), key.PublicKey())) s.setPolicyDenyByDefault(v.GetBool(cfgPolicyDenyByDefault)) + s.setSourceIPHeader(v.GetString(cfgSourceIPHeader)) } func (s *appSettings) updateNamespacesSettings(v *viper.Viper, log *zap.Logger) { @@ -429,6 +431,18 @@ func (s *appSettings) setPolicyDenyByDefault(policyDenyByDefault bool) { s.mu.Unlock() } +func (s *appSettings) setSourceIPHeader(header string) { + s.mu.Lock() + s.sourceIPHeader = header + s.mu.Unlock() +} + +func (s *appSettings) SourceIPHeader() string { + s.mu.RLock() + defer s.mu.RUnlock() + return s.sourceIPHeader +} + func (a *App) initAPI(ctx context.Context) { a.initLayer(ctx) a.initHandler() diff --git a/cmd/s3-gw/app_settings.go b/cmd/s3-gw/app_settings.go index 79bbb228..43d87120 100644 --- a/cmd/s3-gw/app_settings.go +++ b/cmd/s3-gw/app_settings.go @@ -181,6 +181,8 @@ const ( // Settings. // Namespaces. cfgNamespacesConfig = "namespaces.config" + cfgSourceIPHeader = "source_ip_header" + // Command line args. cmdHelp = "help" cmdVersion = "version" diff --git a/config/config.env b/config/config.env index a35dda6c..b5805c88 100644 --- a/config/config.env +++ b/config/config.env @@ -220,3 +220,6 @@ S3_GW_PROXY_CONTRACT=proxy.frostfs # Namespaces configuration S3_GW_NAMESPACES_CONFIG=namespaces.json + +# Custom header to retrieve Source IP +S3_GW_SOURCE_IP_HEADER=Source-Ip diff --git a/config/config.yaml b/config/config.yaml index cfc66c6f..b117d71f 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -259,3 +259,6 @@ proxy: namespaces: config: namespaces.json + +# Custom header to retrieve Source IP +source_ip_header: "Source-Ip" diff --git a/docs/configuration.md b/docs/configuration.md index a9decf3e..b4f34a96 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -220,6 +220,8 @@ allowed_access_key_id_prefixes: - 3stjWenX15YwYzczMr88gy3CQr4NYFBQ8P7keGzH5QFn reconnect_interval: 1m + +source_ip_header: "Source-Ip" ``` | Parameter | Type | SIGHUP reload | Default value | Description | @@ -236,6 +238,7 @@ reconnect_interval: 1m | `max_clients_deadline` | `duration` | no | `30s` | Deadline after which the gate sends error `RequestTimeout` to a client. | | `allowed_access_key_id_prefixes` | `[]string` | no | | List of allowed `AccessKeyID` prefixes which S3 GW serve. If the parameter is omitted, all `AccessKeyID` will be accepted. | | `reconnect_interval` | `duration` | no | `1m` | Listeners reconnection interval. | +| `source_ip_header` | `string` | yes | | Custom header to retrieve Source IP. | ### `wallet` section