Compare commits

...

48 commits

Author SHA1 Message Date
Aleksey Kravchenko
d85e5b10bb [#603] Fix GetBucketPolicyStatus case sensitivity
All checks were successful
/ Vulncheck (push) Successful in 1m1s
/ Builds (push) Successful in 1m4s
/ OCI image (push) Successful in 1m59s
/ Lint (push) Successful in 2m15s
/ Tests (push) Successful in 1m27s
According to the AWS documentation
(https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicyStatus.html),
the `IsPublic` tag value should be in uppercase. However, the `aws-cli`
utility interprets such responses as always `false`.

To avoid incorrect interpretation, we now return the tag value in lowercase.

Signed-off-by: Aleksey Kravchenko <al.kravchenko@yadro.com>
2025-03-14 09:44:38 +00:00
9edec7d573 [#641] Rework CORS bucket behaviour
All checks were successful
/ Vulncheck (push) Successful in 53s
/ Builds (push) Successful in 1m38s
/ OCI image (push) Successful in 2m24s
/ Lint (push) Successful in 3m0s
/ Tests (push) Successful in 1m16s
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2025-03-11 08:22:30 +00:00
1fac8e3ef2 [#656] Don't ignore Expect header in sigv4
All checks were successful
/ DCO (pull_request) Successful in 53s
/ Vulncheck (pull_request) Successful in 1m14s
/ Builds (pull_request) Successful in 1m43s
/ OCI image (pull_request) Successful in 2m15s
/ Lint (pull_request) Successful in 3m17s
/ Tests (pull_request) Successful in 2m51s
/ Vulncheck (push) Successful in 1m5s
/ Builds (push) Successful in 54s
/ OCI image (push) Successful in 2m32s
/ Lint (push) Successful in 2m36s
/ Tests (push) Successful in 1m14s
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-03-10 12:01:18 +03:00
079fd20513 [#652] Port release v0.32.11
All checks were successful
/ DCO (pull_request) Successful in 42s
/ Vulncheck (pull_request) Successful in 1m7s
/ Builds (pull_request) Successful in 1m23s
/ OCI image (pull_request) Successful in 2m25s
/ Lint (pull_request) Successful in 2m59s
/ Tests (pull_request) Successful in 1m38s
/ Vulncheck (push) Successful in 1m2s
/ Builds (push) Successful in 1m0s
/ OCI image (push) Successful in 2m4s
/ Lint (push) Successful in 2m32s
/ Tests (push) Successful in 1m20s
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-28 14:15:20 +03:00
d597dd7c03 [#651] Update sdk
Update sdk to fix TrueCloudLab/frostfs-sdk-go#336

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-28 14:12:48 +03:00
07b60b15b3 [#644] Support keepalive during listing
All checks were successful
/ DCO (pull_request) Successful in 32s
/ Vulncheck (pull_request) Successful in 1m7s
/ Builds (pull_request) Successful in 1m36s
/ Lint (pull_request) Successful in 2m0s
/ Tests (pull_request) Successful in 1m20s
/ OCI image (pull_request) Successful in 2m3s
/ Vulncheck (push) Successful in 1m1s
/ Builds (push) Successful in 1m2s
/ Lint (push) Successful in 2m14s
/ Tests (push) Successful in 1m28s
/ OCI image (push) Successful in 1m56s
Send whitespaces every time as new object in list is ready
to prevent client from context cancelling.

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-27 09:37:04 +03:00
776fd042ef [#647] Dont send error after returning object payload
All checks were successful
/ Vulncheck (push) Successful in 1m0s
/ Builds (push) Successful in 1m2s
/ Lint (push) Successful in 2m12s
/ Tests (push) Successful in 1m18s
/ OCI image (push) Successful in 1m54s
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-25 10:01:58 +00:00
ffe91b43a1 [#648] fix: Prevent InternalError response in PostObject handler
All checks were successful
/ Vulncheck (push) Successful in 1m4s
/ Builds (push) Successful in 1m3s
/ OCI image (push) Successful in 2m6s
/ Lint (push) Successful in 2m9s
/ Tests (push) Successful in 1m18s
Signed-off-by: Nikita Zinkevich <n.zinkevich@yadro.com>
2025-02-25 09:46:54 +00:00
2c0a032966 [#648] fix: Pass tags during PostObject request
Signed-off-by: Nikita Zinkevich <n.zinkevich@yadro.com>
2025-02-25 09:46:54 +00:00
297199d885 [#637] Add IO tags
All checks were successful
/ DCO (pull_request) Successful in 29s
/ Vulncheck (pull_request) Successful in 44s
/ Builds (pull_request) Successful in 1m42s
/ OCI image (pull_request) Successful in 2m11s
/ Lint (pull_request) Successful in 2m29s
/ Tests (pull_request) Successful in 1m54s
/ Vulncheck (push) Successful in 46s
/ Builds (push) Successful in 1m3s
/ OCI image (push) Successful in 2m8s
/ Lint (push) Successful in 2m39s
/ Tests (push) Successful in 1m44s
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2025-02-25 12:16:32 +03:00
0fba02aadb [#596] Use zaptest.Logger
All checks were successful
/ DCO (pull_request) Successful in 30s
/ Vulncheck (pull_request) Successful in 1m2s
/ Builds (pull_request) Successful in 1m19s
/ OCI image (pull_request) Successful in 2m4s
/ Lint (pull_request) Successful in 2m7s
/ Tests (pull_request) Successful in 1m17s
/ Vulncheck (push) Successful in 46s
/ Builds (push) Successful in 58s
/ OCI image (push) Successful in 1m59s
/ Lint (push) Successful in 2m12s
/ Tests (push) Successful in 1m39s
Use zaptest to get logs which get printed only if a test fails
or if you ran go test -v.

Dont use zaptest.Logger for fuzz otherwise ngfuzz/libfuzz crashes

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-21 15:53:27 +03:00
f8852c7626 [#498] Update frostfs-observability version
All checks were successful
/ DCO (pull_request) Successful in 35s
/ Vulncheck (pull_request) Successful in 58s
/ Builds (pull_request) Successful in 1m46s
/ Lint (pull_request) Successful in 2m4s
/ Tests (pull_request) Successful in 1m17s
/ OCI image (pull_request) Successful in 2m9s
/ Vulncheck (push) Successful in 1m3s
/ Builds (push) Successful in 1m5s
/ Lint (push) Successful in 2m0s
/ Tests (push) Successful in 1m18s
/ OCI image (push) Successful in 2m9s
The new version of frostfs-observability has
improved the detail of tracing low-level rpc
calls by adding send and receive events.

Signed-off-by: Roman Loginov <r.loginov@yadro.com>
2025-02-21 11:25:11 +03:00
ac0140506c [#498] middleware: Add spans to detail the trace
Spans are added only to the following middleware:
* PolicyCheck
* Auth
* FrostfsIDValidation

This is done this way because these middleware are basic and
they interact with frostfs-storage.

Also, an explicit context has been added to many functions
so that the middleware spans do not include all subsequent spans.

Signed-off-by: Roman Loginov <r.loginov@yadro.com>
2025-02-21 11:25:08 +03:00
c2c062b778 [#498] frostfs: Add spans to detail the trace
Signed-off-by: Roman Loginov <r.loginov@yadro.com>
2025-02-18 18:28:23 +03:00
94af2770e5 [#498] tree: Add spans to detail the trace
Signed-off-by: Roman Loginov <r.loginov@yadro.com>
2025-02-18 18:14:22 +03:00
b5f0d0871c [#498] layer: Add spans to detail the trace
Signed-off-by: Roman Loginov <r.loginov@yadro.com>
2025-02-18 18:14:22 +03:00
4f0af5a0fd [#498] handler: Add spans to detail the trace
Signed-off-by: Roman Loginov <r.loginov@yadro.com>
2025-02-18 18:14:11 +03:00
bfec3e0a5e [#619] Fix content-length invalid check
All checks were successful
/ Vulncheck (push) Successful in 1m8s
/ Builds (push) Successful in 1m2s
/ OCI image (push) Successful in 2m7s
/ Lint (push) Successful in 2m7s
/ Tests (push) Successful in 1m19s
Signed-off-by: Roman Loginov <r.loginov@yadro.com>
2025-02-18 14:15:23 +00:00
711d6b2c71 [#642] Simplify tests
Some checks failed
/ DCO (pull_request) Successful in 35s
/ Vulncheck (pull_request) Successful in 52s
/ Builds (pull_request) Successful in 1m33s
/ OCI image (pull_request) Successful in 2m13s
/ Lint (pull_request) Successful in 2m16s
/ Tests (pull_request) Successful in 1m51s
/ Vulncheck (push) Successful in 1m8s
/ Builds (push) Successful in 1m3s
/ Tests (push) Successful in 1m17s
/ OCI image (push) Successful in 2m9s
/ Lint (push) Has been cancelled
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-17 09:44:41 +03:00
092567a5a0 Release v0.32.10
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-14 16:11:27 +03:00
e0a54fcbd3 [#642] Fix streaming empty body
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-13 16:55:27 +03:00
e184b333e4 [#612] Port changelog from support branch
All checks were successful
/ DCO (pull_request) Successful in 32s
/ Vulncheck (pull_request) Successful in 1m2s
/ Builds (pull_request) Successful in 1m29s
/ OCI image (pull_request) Successful in 1m58s
/ Lint (pull_request) Successful in 2m20s
/ Tests (pull_request) Successful in 1m17s
/ Vulncheck (push) Successful in 56s
/ Builds (push) Successful in 1m10s
/ OCI image (push) Successful in 2m0s
/ Lint (push) Successful in 2m27s
/ Tests (push) Successful in 1m22s
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2025-02-12 14:50:09 +03:00
853036e44e [#612] Make Content-Md5 header check optional
All checks were successful
/ DCO (pull_request) Successful in 34s
/ Vulncheck (pull_request) Successful in 51s
/ Builds (pull_request) Successful in 1m38s
/ OCI image (pull_request) Successful in 2m19s
/ Lint (pull_request) Successful in 2m27s
/ Tests (pull_request) Successful in 1m53s
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2025-02-12 14:29:19 +03:00
ee46382a68 [#606] Reorganize some log tags
Some checks failed
/ DCO (pull_request) Successful in 32s
/ Vulncheck (pull_request) Successful in 1m12s
/ Builds (pull_request) Successful in 58s
/ OCI image (pull_request) Successful in 2m4s
/ Lint (pull_request) Successful in 2m23s
/ Tests (pull_request) Successful in 1m13s
/ Vulncheck (push) Successful in 42s
/ Builds (push) Successful in 1m12s
/ Lint (push) Successful in 2m25s
/ Tests (push) Successful in 1m7s
/ OCI image (push) Failing after 11m37s
Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2025-02-11 16:47:42 +03:00
b207eb48d9 [#606] Use all available log tags by default
Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2025-02-11 16:47:36 +03:00
b7650e01ac [#606] Make log tags more explicit
Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2025-02-11 16:47:30 +03:00
e7f620f137 [#606] Support log tagging
All checks were successful
/ DCO (pull_request) Successful in 28s
/ Vulncheck (pull_request) Successful in 1m6s
/ Builds (pull_request) Successful in 57s
/ OCI image (pull_request) Successful in 2m0s
/ Lint (pull_request) Successful in 3m19s
/ Tests (pull_request) Successful in 1m12s
Signed-off-by: Nikita Zinkevich <n.zinkevich@yadro.com>
2025-02-11 15:12:20 +03:00
ffac62e8b4 [#606] logs: Delete comments
Signed-off-by: Nikita Zinkevich <n.zinkevich@yadro.com>
2025-02-11 15:12:20 +03:00
182262ace2 [#636] Bump go version in vulncheck
All checks were successful
/ DCO (pull_request) Successful in 32s
/ Vulncheck (pull_request) Successful in 56s
/ Builds (pull_request) Successful in 1m6s
/ Lint (pull_request) Successful in 2m22s
/ Tests (pull_request) Successful in 1m15s
/ OCI image (pull_request) Successful in 2m31s
/ Builds (push) Successful in 1m3s
/ Vulncheck (push) Successful in 1m4s
/ OCI image (push) Successful in 2m11s
/ Lint (push) Successful in 3m11s
/ Tests (push) Successful in 1m14s
go1.23.5 triggers GO-2025-3447 but this is applicable
only for ppc64le platform.

Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2025-02-11 11:48:22 +03:00
893b506c83 [#626] Fix ALREADY REMOVED response status code
Some checks failed
/ DCO (pull_request) Successful in 30s
/ Vulncheck (pull_request) Failing after 1m15s
/ Builds (pull_request) Successful in 1m27s
/ OCI image (pull_request) Successful in 2m14s
/ Lint (pull_request) Successful in 3m7s
/ Tests (pull_request) Successful in 1m16s
/ Vulncheck (push) Failing after 1m25s
/ Builds (push) Successful in 58s
/ OCI image (push) Successful in 2m7s
/ Lint (push) Successful in 2m22s
/ Tests (push) Successful in 1m17s
Signed-off-by: Pavel Pogodaev <p.pogodaev@yadro.com>
2025-02-10 16:10:45 +03:00
beec37797d [#626] Fix ALREADY REMOVED response status code
Some checks failed
/ DCO (pull_request) Successful in 40s
/ Vulncheck (pull_request) Failing after 1m45s
/ Builds (pull_request) Successful in 2m4s
/ OCI image (pull_request) Successful in 3m3s
/ Lint (pull_request) Successful in 3m21s
/ Tests (pull_request) Successful in 1m24s
Signed-off-by: Pavel Pogodaev <p.pogodaev@yadro.com>
2025-02-07 17:37:40 +03:00
5538dce772 [#628] Add tree_stream_timeout config parameter
Some checks failed
/ DCO (pull_request) Successful in 36s
/ Vulncheck (pull_request) Failing after 58s
/ Builds (pull_request) Successful in 1m44s
/ Lint (pull_request) Successful in 2m17s
/ Tests (pull_request) Successful in 2m11s
/ OCI image (pull_request) Successful in 2m40s
/ Vulncheck (push) Failing after 1m7s
/ Builds (push) Successful in 1m9s
/ OCI image (push) Successful in 2m10s
/ Lint (push) Successful in 3m15s
/ Tests (push) Successful in 1m15s
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2025-02-07 11:57:54 +03:00
da9703ab63 [#623] Fix using copy numbers during multipart
All checks were successful
/ DCO (pull_request) Successful in 34s
/ Vulncheck (pull_request) Successful in 1m2s
/ Builds (pull_request) Successful in 1m12s
/ OCI image (pull_request) Successful in 2m1s
/ Lint (pull_request) Successful in 2m24s
/ Tests (pull_request) Successful in 1m22s
/ Vulncheck (push) Successful in 1m3s
/ Builds (push) Successful in 1m17s
/ Lint (push) Successful in 2m17s
/ Tests (push) Successful in 1m22s
/ OCI image (push) Successful in 2m36s
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-06 14:41:27 +03:00
a53e50b324 [#607] Support sigV4a streaming with trailers
All checks were successful
/ DCO (pull_request) Successful in 32s
/ Vulncheck (pull_request) Successful in 1m6s
/ Builds (pull_request) Successful in 1m34s
/ OCI image (pull_request) Successful in 2m2s
/ Lint (pull_request) Successful in 2m10s
/ Tests (pull_request) Successful in 1m17s
/ Vulncheck (push) Successful in 1m6s
/ Builds (push) Successful in 1m6s
/ Lint (push) Successful in 1m57s
/ Tests (push) Successful in 1m20s
/ OCI image (push) Successful in 2m10s
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-03 18:25:15 +03:00
5e9c562683 [#607] Fix aws example test for trailing with sigv4
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-03 18:25:15 +03:00
49bf3c1bce [#607] Support sigV4 streaming with trailers
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-03 18:25:15 +03:00
a4d9658fbb [#607] Support unsigned payload streaming with trailers
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-03 18:25:15 +03:00
bec63026bd [#607] Support unsigned payload streaming
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-03 18:25:15 +03:00
0064e7ab07 [#618] Port changelog from support branch
All checks were successful
/ Vulncheck (push) Successful in 58s
/ Builds (push) Successful in 1m17s
/ OCI image (push) Successful in 2m10s
/ Lint (push) Successful in 2m21s
/ Tests (push) Successful in 1m46s
Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2025-02-03 14:27:09 +00:00
41e1f1ad7a [#617] Bump SDK version to the latest master
Contains fixes:
- memory leak in gRPC client,
- panic and deadlock in tree pool.

Signed-off-by: Alex Vanin <a.vanin@yadro.com>
2025-02-03 14:27:09 +00:00
4d2e6f8650 [#610] Fix updateServers finding logic
All checks were successful
/ Vulncheck (push) Successful in 1m24s
/ Builds (push) Successful in 1m55s
/ OCI image (push) Successful in 2m36s
/ Lint (push) Successful in 2m41s
/ Tests (push) Successful in 2m2s
Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-02-03 13:00:15 +00:00
3d3dd00211 [#615] Use UNSIGNED_PAYLOAD to check sign
All checks were successful
/ Vulncheck (push) Successful in 1m4s
/ Lint (push) Successful in 1m53s
/ Tests (push) Successful in 1m17s
/ OCI image (push) Successful in 2m7s
/ Builds (push) Successful in 1m2s
Use `UNSIGNED_PAYLOAD` to check signature if x-amz-content-sha256 isn't provided as signed header

https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html

" You include the literal string UNSIGNED-PAYLOAD when constructing a canonical request, and set the same value as the x-amz-content-sha256 header value when sending the request to Amazon S3"

Signed-off-by: Denis Kirillov <d.kirillov@yadro.com>
2025-01-30 13:16:40 +00:00
510b0a1005 [#614] govulncheck: Use patch release with security fixes
All checks were successful
/ Vulncheck (push) Successful in 48s
/ Builds (push) Successful in 1m49s
/ OCI image (push) Successful in 2m28s
/ Lint (push) Successful in 3m3s
/ Tests (push) Successful in 1m48s
https://go.dev/doc/devel/release#go1.23.minor

Signed-off-by: Vitaliy Potyarkin <v.potyarkin@yadro.com>
2025-01-29 12:07:28 +00:00
da77e426b6 [#541] Fix setting of tls.enabled flag
Some checks failed
/ Builds (push) Has been cancelled
/ OCI image (push) Has been cancelled
/ Lint (push) Has been cancelled
/ Tests (push) Has been cancelled
/ Vulncheck (push) Has been cancelled
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2025-01-29 12:06:41 +00:00
e7a8d4bdaf [#605] Fix panic when payload discard
Some checks failed
/ DCO (pull_request) Successful in 30s
/ Vulncheck (pull_request) Successful in 1m7s
/ Builds (pull_request) Successful in 1m31s
/ Lint (pull_request) Successful in 2m4s
/ Tests (pull_request) Successful in 1m8s
/ OCI image (pull_request) Successful in 2m10s
/ Builds (push) Has been cancelled
/ OCI image (push) Has been cancelled
/ Lint (push) Has been cancelled
/ Vulncheck (push) Has been cancelled
/ Tests (push) Has been cancelled
Signed-off-by: Roman Loginov <r.loginov@yadro.com>
2025-01-27 17:01:50 +03:00
250538a9b4 [#541] Use default value if config param is unset after SIGHUP
All checks were successful
/ DCO (pull_request) Successful in 31s
/ Vulncheck (pull_request) Successful in 1m2s
/ Builds (pull_request) Successful in 1m19s
/ Lint (pull_request) Successful in 2m6s
/ Tests (pull_request) Successful in 1m17s
/ OCI image (pull_request) Successful in 2m6s
/ Vulncheck (push) Successful in 1m4s
/ Builds (push) Successful in 1m6s
/ OCI image (push) Successful in 2m6s
/ Lint (push) Successful in 2m6s
/ Tests (push) Successful in 1m15s
Signed-off-by: Marina Biryukova <m.biryukova@yadro.com>
2025-01-23 09:52:48 +03:00
619385836d [#585] Add ListBuckets handler test
All checks were successful
/ Vulncheck (push) Successful in 1m2s
/ Builds (push) Successful in 1m5s
/ OCI image (push) Successful in 1m55s
/ Lint (push) Successful in 2m15s
/ Tests (push) Successful in 1m17s
Modify containers field in TestFrostFS in order to get determined order of containers between test runs

Signed-off-by: Nikita Zinkevich <n.zinkevich@yadro.com>
2025-01-21 07:49:19 +00:00
65fc776dea [#585] Add ListBuckets pagination
Signed-off-by: Nikita Zinkevich <n.zinkevich@yadro.com>
2025-01-21 07:49:19 +00:00
113 changed files with 4823 additions and 1426 deletions

View file

@ -16,7 +16,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v3 uses: actions/setup-go@v3
with: with:
go-version: '1.23' go-version: '1.23.6'
- name: Install govulncheck - name: Install govulncheck
run: go install golang.org/x/vuln/cmd/govulncheck@latest run: go install golang.org/x/vuln/cmd/govulncheck@latest

View file

@ -4,6 +4,60 @@ This document outlines major changes between releases.
## [Unreleased] ## [Unreleased]
## [0.32.11] - 2025-02-28
### Fixed
- ListObjects could return empty result from priority storage node with failed shard (#651)
## [0.32.10] - 2025-02-14
### Fixed
- Chunk streaming empty body (#642)
## [0.32.9] - 2025-02-12
### Fixed
- Make `Content-Md5` header check optional (#612)
## [0.32.8] - 2025-02-11
### Fixed
- Return 404 instead of 500 when object is missing in object storage and available in the tree (#626)
### Added
- `tree_stream_timeout` configuration parameter (#627)
## [0.32.7] - 2025-02-06
### Fixed
- Correct passing copies number during multipart upload (#623)
## [0.32.6] - 2025-02-05
### Fixed
- Connection leak when `feature.tree_pool_netmap_support` is enabled (#622)
## [0.32.5] - 2025-02-04
### Fixed
- Support trailing headers signature during aws-chunk upload (#607)
## [0.32.4] - 2025-02-03
### Fixed
- Possible deadlock in tree pool component (#617)
- Possible memory leak in gRPC client (#617)
## [0.32.3] - 2025-01-29
### Fixed
- Use `UNSIGNED_PAYLOAD` as content hash to check signature if `x-amz-content-sha256` isn't signed header (#616)
## [0.32.2] - 2025-01-27
### Fixed
- Fix panic when payload discard (#605)
## [0.32.1] - 2025-01-17 ## [0.32.1] - 2025-01-17
### Fixed ### Fixed
@ -400,4 +454,14 @@ To see CHANGELOG for older versions, refer to https://github.com/nspcc-dev/neofs
[0.31.3]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.31.2...v0.31.3 [0.31.3]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.31.2...v0.31.3
[0.32.0]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.31.3...v0.32.0 [0.32.0]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.31.3...v0.32.0
[0.32.1]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.0...v0.32.1 [0.32.1]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.0...v0.32.1
[Unreleased]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.1...master [0.32.2]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.1...v0.32.2
[0.32.3]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.2...v0.32.3
[0.32.4]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.3...v0.32.4
[0.32.5]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.4...v0.32.5
[0.32.6]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.5...v0.32.6
[0.32.7]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.6...v0.32.7
[0.32.8]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.7...v0.32.8
[0.32.9]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.8...v0.32.9
[0.32.10]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.9...v0.32.10
[0.32.11]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.10...v0.32.11
[Unreleased]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.11...master

View file

@ -1 +1 @@
v0.32.1 v0.32.11

View file

@ -13,6 +13,7 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"regexp" "regexp"
"slices"
"strings" "strings"
"time" "time"
@ -32,13 +33,16 @@ var (
// AuthorizationFieldRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter. // AuthorizationFieldRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter.
AuthorizationFieldRegexp = regexp.MustCompile(`AWS4-HMAC-SHA256 Credential=(?P<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<region>[^/]*)/(?P<service>[^/]+)/aws4_request,\s*SignedHeaders=(?P<signed_header_fields>.+),\s*Signature=(?P<v4_signature>.+)`) AuthorizationFieldRegexp = regexp.MustCompile(`AWS4-HMAC-SHA256 Credential=(?P<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<region>[^/]*)/(?P<service>[^/]+)/aws4_request,\s*SignedHeaders=(?P<signed_header_fields>.+),\s*Signature=(?P<v4_signature>.+)`)
// authorizationFieldV4aRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter. // AuthorizationFieldV4aRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter.
authorizationFieldV4aRegexp = regexp.MustCompile(`AWS4-ECDSA-P256-SHA256 Credential=(?P<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<service>[^/]+)/aws4_request,\s*SignedHeaders=(?P<signed_header_fields>.+),\s*Signature=(?P<v4_signature>.+)`) AuthorizationFieldV4aRegexp = regexp.MustCompile(`AWS4-ECDSA-P256-SHA256 Credential=(?P<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<service>[^/]+)/aws4_request,\s*SignedHeaders=(?P<signed_header_fields>.+),\s*Signature=(?P<v4_signature>.+)`)
// postPolicyCredentialRegexp -- is regexp for credentials when uploading file using POST with policy. // postPolicyCredentialRegexp -- is regexp for credentials when uploading file using POST with policy.
postPolicyCredentialRegexp = regexp.MustCompile(`(?P<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<region>[^/]*)/(?P<service>[^/]+)/aws4_request`) postPolicyCredentialRegexp = regexp.MustCompile(`(?P<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<region>[^/]*)/(?P<service>[^/]+)/aws4_request`)
) )
// need for tests.
var timeNow = time.Now
type ( type (
Center struct { Center struct {
reg *RegexpSubmatcher reg *RegexpSubmatcher
@ -106,7 +110,7 @@ func New(creds tokens.Credentials, prefixes []string, settings CenterSettings) *
return &Center{ return &Center{
cli: creds, cli: creds,
reg: NewRegexpMatcher(AuthorizationFieldRegexp), reg: NewRegexpMatcher(AuthorizationFieldRegexp),
regV4a: NewRegexpMatcher(authorizationFieldV4aRegexp), regV4a: NewRegexpMatcher(AuthorizationFieldV4aRegexp),
postReg: NewRegexpMatcher(postPolicyCredentialRegexp), postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
allowedAccessKeyIDPrefixes: prefixes, allowedAccessKeyIDPrefixes: prefixes,
settings: settings, settings: settings,
@ -114,8 +118,8 @@ func New(creds tokens.Credentials, prefixes []string, settings CenterSettings) *
} }
const ( const (
signaturePreambleSigV4 = "AWS4-HMAC-SHA256" SignaturePreambleSigV4 = "AWS4-HMAC-SHA256"
signaturePreambleSigV4A = "AWS4-ECDSA-P256-SHA256" SignaturePreambleSigV4A = "AWS4-ECDSA-P256-SHA256"
) )
func (c *Center) parseAuthHeader(authHeader string, headers http.Header) (*AuthHeader, error) { func (c *Center) parseAuthHeader(authHeader string, headers http.Header) (*AuthHeader, error) {
@ -127,13 +131,13 @@ func (c *Center) parseAuthHeader(authHeader string, headers http.Header) (*AuthH
) )
switch preamble { switch preamble {
case signaturePreambleSigV4: case SignaturePreambleSigV4:
submatches = c.reg.GetSubmatches(authHeader) submatches = c.reg.GetSubmatches(authHeader)
if len(submatches) != authHeaderPartsNum { if len(submatches) != authHeaderPartsNum {
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader) return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader)
} }
region = submatches["region"] region = submatches["region"]
case signaturePreambleSigV4A: case SignaturePreambleSigV4A:
submatches = c.regV4a.GetSubmatches(authHeader) submatches = c.regV4a.GetSubmatches(authHeader)
if len(submatches) != authHeaderV4aPartsNum { if len(submatches) != authHeaderV4aPartsNum {
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader) return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader)
@ -160,7 +164,7 @@ func IsStandardContentSHA256(key string) bool {
return ok return ok
} }
func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) { func (c *Center) Authenticate(ctx context.Context, r *http.Request) (*middleware.Box, error) {
var ( var (
err error err error
authHdr *AuthHeader authHdr *AuthHeader
@ -169,7 +173,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
) )
queryValues := r.URL.Query() queryValues := r.URL.Query()
if queryValues.Get(AmzAlgorithm) == signaturePreambleSigV4 { if queryValues.Get(AmzAlgorithm) == SignaturePreambleSigV4 {
creds := strings.Split(queryValues.Get(AmzCredential), "/") creds := strings.Split(queryValues.Get(AmzCredential), "/")
if len(creds) != 5 || creds[4] != "aws4_request" { if len(creds) != 5 || creds[4] != "aws4_request" {
return nil, fmt.Errorf("bad X-Amz-Credential") return nil, fmt.Errorf("bad X-Amz-Credential")
@ -182,7 +186,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"), SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"),
Date: creds[1], Date: creds[1],
IsPresigned: true, IsPresigned: true,
Preamble: signaturePreambleSigV4, Preamble: SignaturePreambleSigV4,
PayloadHash: r.Header.Get(AmzContentSHA256), PayloadHash: r.Header.Get(AmzContentSHA256),
} }
authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s") authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s")
@ -190,7 +194,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
return nil, fmt.Errorf("%w: couldn't parse X-Amz-Expires %v", apierr.GetAPIError(apierr.ErrMalformedExpires), err) return nil, fmt.Errorf("%w: couldn't parse X-Amz-Expires %v", apierr.GetAPIError(apierr.ErrMalformedExpires), err)
} }
signatureDateTimeStr = queryValues.Get(AmzDate) signatureDateTimeStr = queryValues.Get(AmzDate)
} else if queryValues.Get(AmzAlgorithm) == signaturePreambleSigV4A { } else if queryValues.Get(AmzAlgorithm) == SignaturePreambleSigV4A {
creds := strings.Split(queryValues.Get(AmzCredential), "/") creds := strings.Split(queryValues.Get(AmzCredential), "/")
if len(creds) != 4 || creds[3] != "aws4_request" { if len(creds) != 4 || creds[3] != "aws4_request" {
return nil, fmt.Errorf("bad X-Amz-Credential") return nil, fmt.Errorf("bad X-Amz-Credential")
@ -203,7 +207,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"), SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"),
Date: creds[1], Date: creds[1],
IsPresigned: true, IsPresigned: true,
Preamble: signaturePreambleSigV4A, Preamble: SignaturePreambleSigV4A,
PayloadHash: r.Header.Get(AmzContentSHA256), PayloadHash: r.Header.Get(AmzContentSHA256),
} }
authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s") authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s")
@ -215,7 +219,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
authHeaderField := r.Header[AuthorizationHdr] authHeaderField := r.Header[AuthorizationHdr]
if len(authHeaderField) != 1 { if len(authHeaderField) != 1 {
if strings.HasPrefix(r.Header.Get(ContentTypeHdr), "multipart/form-data") { if strings.HasPrefix(r.Header.Get(ContentTypeHdr), "multipart/form-data") {
return c.checkFormData(r) return c.checkFormData(ctx, r)
} }
return nil, fmt.Errorf("%w: %v", middleware.ErrNoAuthorizationHeader, authHeaderField) return nil, fmt.Errorf("%w: %v", middleware.ErrNoAuthorizationHeader, authHeaderField)
} }
@ -241,7 +245,7 @@ func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
return nil, err return nil, err
} }
box, attrs, err := c.cli.GetBox(r.Context(), cnrID, authHdr.AccessKeyID) box, attrs, err := c.cli.GetBox(ctx, cnrID, authHdr.AccessKeyID)
if err != nil { if err != nil {
return nil, fmt.Errorf("get box by access key '%s': %w", authHdr.AccessKeyID, err) return nil, fmt.Errorf("get box by access key '%s': %w", authHdr.AccessKeyID, err)
} }
@ -314,7 +318,7 @@ func (c Center) checkAccessKeyID(accessKeyID string) error {
return fmt.Errorf("%w: accesskeyID prefix isn't allowed", apierr.GetAPIError(apierr.ErrAccessDenied)) return fmt.Errorf("%w: accesskeyID prefix isn't allowed", apierr.GetAPIError(apierr.ErrAccessDenied))
} }
func (c *Center) checkFormData(r *http.Request) (*middleware.Box, error) { func (c *Center) checkFormData(ctx context.Context, r *http.Request) (*middleware.Box, error) {
if err := r.ParseMultipartForm(maxFormSizeMemory); err != nil { if err := r.ParseMultipartForm(maxFormSizeMemory); err != nil {
return nil, fmt.Errorf("%w: parse multipart form with max size %d", apierr.GetAPIError(apierr.ErrInvalidArgument), maxFormSizeMemory) return nil, fmt.Errorf("%w: parse multipart form with max size %d", apierr.GetAPIError(apierr.ErrInvalidArgument), maxFormSizeMemory)
} }
@ -346,7 +350,7 @@ func (c *Center) checkFormData(r *http.Request) (*middleware.Box, error) {
return nil, err return nil, err
} }
box, attrs, err := c.cli.GetBox(r.Context(), cnrID, accessKeyID) box, attrs, err := c.cli.GetBox(ctx, cnrID, accessKeyID)
if err != nil { if err != nil {
return nil, fmt.Errorf("get box by accessKeyID '%s': %w", accessKeyID, err) return nil, fmt.Errorf("get box by accessKeyID '%s': %w", accessKeyID, err)
} }
@ -396,8 +400,12 @@ func cloneRequest(r *http.Request, authHeader *AuthHeader) *http.Request {
func (c *Center) checkSign(ctx context.Context, authHeader *AuthHeader, box *accessbox.Box, request *http.Request, signatureDateTime time.Time) error { func (c *Center) checkSign(ctx context.Context, authHeader *AuthHeader, box *accessbox.Box, request *http.Request, signatureDateTime time.Time) error {
var signature string var signature string
if !slices.Contains(authHeader.SignedFields, "x-amz-content-sha256") && authHeader.PayloadHash == "" {
authHeader.PayloadHash = UnsignedPayload
}
switch authHeader.Preamble { switch authHeader.Preamble {
case signaturePreambleSigV4: case SignaturePreambleSigV4:
creds := aws.Credentials{ creds := aws.Credentials{
AccessKeyID: authHeader.AccessKeyID, AccessKeyID: authHeader.AccessKeyID,
SecretAccessKey: box.Gate.SecretKey, SecretAccessKey: box.Gate.SecretKey,
@ -432,7 +440,7 @@ func (c *Center) checkSign(ctx context.Context, authHeader *AuthHeader, box *acc
authHeader.Signature, signature, authHeader.SignedFields) authHeader.Signature, signature, authHeader.SignedFields)
} }
case signaturePreambleSigV4A: case SignaturePreambleSigV4A:
signer := v4a.NewSigner(func(options *v4a.SignerOptions) { signer := v4a.NewSigner(func(options *v4a.SignerOptions) {
options.DisableURIPathEscaping = true options.DisableURIPathEscaping = true
}) })
@ -465,7 +473,7 @@ func (c *Center) checkSign(ctx context.Context, authHeader *AuthHeader, box *acc
} }
func checkPresignedDate(authHeader *AuthHeader, signatureDateTime time.Time) error { func checkPresignedDate(authHeader *AuthHeader, signatureDateTime time.Time) error {
now := time.Now() now := timeNow()
if signatureDateTime.Add(authHeader.Expiration).Before(now) { if signatureDateTime.Add(authHeader.Expiration).Before(now) {
return fmt.Errorf("%w: expired: now %s, signature %s", apierr.GetAPIError(apierr.ErrExpiredPresignRequest), return fmt.Errorf("%w: expired: now %s, signature %s", apierr.GetAPIError(apierr.ErrExpiredPresignRequest),
now.Format(time.RFC3339), signatureDateTime.Format(time.RFC3339)) now.Format(time.RFC3339), signatureDateTime.Format(time.RFC3339))

View file

@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url" "net/url"
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
@ -26,6 +27,9 @@ import (
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
smithyauth "github.com/aws/smithy-go/auth"
"github.com/aws/smithy-go/logging"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest" "go.uber.org/zap/zaptest"
@ -65,7 +69,7 @@ func TestAuthHeaderParse(t *testing.T) {
Signature: "2811ccb9e242f41426738fb1f", Signature: "2811ccb9e242f41426738fb1f",
SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"}, SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"},
Date: "20210809", Date: "20210809",
Preamble: signaturePreambleSigV4, Preamble: SignaturePreambleSigV4,
}, },
}, },
{ {
@ -178,6 +182,71 @@ func TestSignatureV4(t *testing.T) {
require.Equal(t, signature, signatureComputed, "signature mismatched") require.Equal(t, signature, signatureComputed, "signature mismatched")
} }
func TestSignExpectHeader(t *testing.T) {
timeNow = func() time.Time {
return time.Date(2025, 3, 10, 11, 0, 0, 0, time.UTC)
}
t.Cleanup(func() {
timeNow = time.Now
})
ctx := context.Background()
key, err := keys.NewPrivateKey()
require.NoError(t, err)
cfg := &cache.Config{
Size: 10,
Lifetime: 24 * time.Hour,
Logger: zaptest.NewLogger(t),
}
gateData := []*accessbox.GateData{{
BearerToken: &bearer.Token{},
GateKey: key.PublicKey(),
}}
accessBox, _, err := accessbox.PackTokens(gateData, []byte("0a85c05b993e7663bdd245aa780569ee8abbdc1d9d9a9e185395ff87dffb0105"), true)
require.NoError(t, err)
data, err := accessBox.Marshal()
require.NoError(t, err)
var obj object.Object
obj.SetPayload(data)
var addr oid.Address
err = addr.DecodeString("2t6v52sJdxN2NV2bFfYnuTSQiYkbcDQ2yMKHfGSsjHiv/HbUvXKFK7meCQWoo1Es3oGSMQFbFK7Np82JVEMbCM48d")
require.NoError(t, err)
frostfs := newFrostFSMock()
frostfs.objects[getAccessKeyID(addr)] = &obj
tknCfg := tokens.Config{
FrostFS: frostfs,
Key: key,
CacheConfig: cfg,
}
creds := tokens.New(tknCfg)
cntr := New(creds, nil, &centerSettingsMock{})
body := bytes.NewBufferString("")
req, err := http.NewRequest("PUT", "http://localhost:8184/test/tmp.txt", body)
require.NoError(t, err)
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential=2t6v52sJdxN2NV2bFfYnuTSQiYkbcDQ2yMKHfGSsjHiv0HbUvXKFK7meCQWoo1Es3oGSMQFbFK7Np82JVEMbCM48d/20250310/us-east-2/s3/aws4_request, SignedHeaders=expect;host;x-amz-content-sha256;x-amz-date, Signature=966bda3d22213ae3f10d82b302041d2fc1c18804e8c02ca7d9bd35b6bcb8c161")
req.Header.Set("Expect", "100-continue")
req.Header.Set("X-Amz-Content-Sha256", UnsignedPayload)
req.Header.Set("X-Amz-Date", "20250310T082808Z")
_, err = cntr.Authenticate(ctx, req)
require.NoError(t, err)
req2, err := http.NewRequest("PUT", "http://localhost:8184/test/tmp.txt?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=2t6v52sJdxN2NV2bFfYnuTSQiYkbcDQ2yMKHfGSsjHiv0HbUvXKFK7meCQWoo1Es3oGSMQFbFK7Np82JVEMbCM48d%2F20250310%2Fru%2Fs3%2Faws4_request&X-Amz-Date=20250310T083436Z&X-Amz-Expires=43200&X-Amz-SignedHeaders=expect%3Bhost&X-Amz-Signature=42c3dbe45842fdfd62d78632e135f5460e3f709ab6b949e257de30168946859b", body)
require.NoError(t, err)
req2.Header.Set("Expect", "100-continue")
_, err = cntr.Authenticate(ctx, req2)
require.NoError(t, err)
}
func TestCheckFormatContentSHA256(t *testing.T) { func TestCheckFormatContentSHA256(t *testing.T) {
defaultErr := errors.GetAPIError(errors.ErrContentSHA256Mismatch) defaultErr := errors.GetAPIError(errors.ErrContentSHA256Mismatch)
@ -298,6 +367,7 @@ func TestAuthenticate(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
region string
prefixes []string prefixes []string
request *http.Request request *http.Request
err bool err bool
@ -308,10 +378,23 @@ func TestAuthenticate(t *testing.T) {
prefixes: []string{addr.Container().String()}, prefixes: []string{addr.Container().String()},
request: func() *http.Request { request: func() *http.Request {
r := httptest.NewRequest(http.MethodPost, "/", nil) r := httptest.NewRequest(http.MethodPost, "/", nil)
err = defaultSigner.SignHTTP(ctx, awsCreds, r, UnsignedPayload, service, region, time.Now())
require.NoError(t, err)
return r
}(),
region: region,
},
{
name: "valid sign with hash",
prefixes: []string{addr.Container().String()},
request: func() *http.Request {
r := httptest.NewRequest(http.MethodPost, "/", nil)
r.Header.Set(AmzContentSHA256, "")
err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now()) err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now())
require.NoError(t, err) require.NoError(t, err)
return r return r
}(), }(),
region: region,
}, },
{ {
name: "no authorization header", name: "no authorization header",
@ -418,12 +501,27 @@ func TestAuthenticate(t *testing.T) {
request: func() *http.Request { request: func() *http.Request {
r := httptest.NewRequest(http.MethodPost, "/", nil) r := httptest.NewRequest(http.MethodPost, "/", nil)
r.Header.Set(AmzExpires, "60") r.Header.Set(AmzExpires, "60")
signedURI, _, err := defaultSigner.PresignHTTP(ctx, awsCreds, r, UnsignedPayload, service, region, time.Now())
require.NoError(t, err)
r.URL, err = url.ParseRequestURI(signedURI)
require.NoError(t, err)
return r
}(),
region: region,
},
{
name: "valid presign with hash",
request: func() *http.Request {
r := httptest.NewRequest(http.MethodPost, "/", nil)
r.Header.Set(AmzExpires, "60")
r.Header.Set(AmzContentSHA256, "")
signedURI, _, err := defaultSigner.PresignHTTP(ctx, awsCreds, r, "", service, region, time.Now()) signedURI, _, err := defaultSigner.PresignHTTP(ctx, awsCreds, r, "", service, region, time.Now())
require.NoError(t, err) require.NoError(t, err)
r.URL, err = url.ParseRequestURI(signedURI) r.URL, err = url.ParseRequestURI(signedURI)
require.NoError(t, err) require.NoError(t, err)
return r return r
}(), }(),
region: region,
}, },
{ {
name: "presign, bad X-Amz-Credential", name: "presign, bad X-Amz-Credential",
@ -480,11 +578,61 @@ func TestAuthenticate(t *testing.T) {
err: true, err: true,
errCode: errors.ErrBadRequest, errCode: errors.ErrBadRequest,
}, },
{
name: "presign using original aws sdk",
request: func() *http.Request {
cli := s3.NewPresignClient(s3.New(s3.Options{
Credentials: credentials.NewStaticCredentialsProvider(awsCreds.AccessKeyID, awsCreds.SecretAccessKey, ""),
UsePathStyle: true,
BaseEndpoint: aws.String("http://localhost"),
Region: region,
Logger: logging.NewStandardLogger(os.Stdout),
ClientLogMode: aws.LogSigning,
}))
res, err := cli.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String("bucket"),
Key: aws.String("object"),
})
require.NoError(t, err)
r := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
r.URL, err = url.ParseRequestURI(res.URL)
require.NoError(t, err)
return r
}(),
region: region,
},
{
name: "presign sigv4a using original aws sdk",
request: func() *http.Request {
cli := s3.NewPresignClient(s3.New(s3.Options{
Credentials: credentials.NewStaticCredentialsProvider(awsCreds.AccessKeyID, awsCreds.SecretAccessKey, ""),
UsePathStyle: true,
BaseEndpoint: aws.String("http://localhost"),
Region: region,
Logger: logging.NewStandardLogger(os.Stdout),
ClientLogMode: aws.LogSigning,
AuthSchemeResolver: resolver{},
}))
res, err := cli.PresignGetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String("bucket"),
Key: aws.String("object"),
})
require.NoError(t, err)
r := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
r.URL, err = url.ParseRequestURI(res.URL)
require.NoError(t, err)
return r
}(),
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
creds := tokens.New(bigConfig) creds := tokens.New(bigConfig)
cntr := New(creds, tc.prefixes, &centerSettingsMock{}) cntr := New(creds, tc.prefixes, &centerSettingsMock{})
box, err := cntr.Authenticate(tc.request) box, err := cntr.Authenticate(ctx, tc.request)
if tc.err { if tc.err {
require.Error(t, err) require.Error(t, err)
@ -495,13 +643,19 @@ func TestAuthenticate(t *testing.T) {
} else { } else {
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, accessKeyID, box.AuthHeaders.AccessKeyID) require.Equal(t, accessKeyID, box.AuthHeaders.AccessKeyID)
require.Equal(t, region, box.AuthHeaders.Region) require.Equal(t, tc.region, box.AuthHeaders.Region)
require.Equal(t, secret.SecretKey, box.AccessBox.Gate.SecretKey) require.Equal(t, secret.SecretKey, box.AccessBox.Gate.SecretKey)
} }
}) })
} }
} }
type resolver struct{}
func (r resolver) ResolveAuthSchemes(context.Context, *s3.AuthResolverParameters) ([]*smithyauth.Option, error) {
return []*smithyauth.Option{{SchemeID: smithyauth.SchemeIDSigV4A}}, nil
}
func TestHTTPPostAuthenticate(t *testing.T) { func TestHTTPPostAuthenticate(t *testing.T) {
const ( const (
policyBase64 = "eyJleHBpcmF0aW9uIjogIjIwMjUtMTItMDFUMTI6MDA6MDAuMDAwWiIsImNvbmRpdGlvbnMiOiBbCiBbInN0YXJ0cy13aXRoIiwgIiR4LWFtei1jcmVkZW50aWFsIiwgIiJdLAogWyJzdGFydHMtd2l0aCIsICIkeC1hbXotZGF0ZSIsICIiXQpdfQ==" policyBase64 = "eyJleHBpcmF0aW9uIjogIjIwMjUtMTItMDFUMTI6MDA6MDAuMDAwWiIsImNvbmRpdGlvbnMiOiBbCiBbInN0YXJ0cy13aXRoIiwgIiR4LWFtei1jcmVkZW50aWFsIiwgIiJdLAogWyJzdGFydHMtd2l0aCIsICIkeC1hbXotZGF0ZSIsICIiXQpdfQ=="
@ -511,6 +665,7 @@ func TestHTTPPostAuthenticate(t *testing.T) {
region = "default" region = "default"
) )
ctx := context.Background()
key, err := keys.NewPrivateKey() key, err := keys.NewPrivateKey()
require.NoError(t, err) require.NoError(t, err)
@ -662,7 +817,7 @@ func TestHTTPPostAuthenticate(t *testing.T) {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
creds := tokens.New(bigConfig) creds := tokens.New(bigConfig)
cntr := New(creds, tc.prefixes, &centerSettingsMock{}) cntr := New(creds, tc.prefixes, &centerSettingsMock{})
box, err := cntr.Authenticate(tc.request) box, err := cntr.Authenticate(ctx, tc.request)
if tc.err { if tc.err {
require.Error(t, err) require.Error(t, err)

View file

@ -52,7 +52,12 @@ func PresignRequest(ctx context.Context, creds aws.Credentials, reqData RequestD
options.Logger = log options.Logger = log
}) })
signedURI, _, err := signer.PresignHTTP(ctx, creds, req, presignData.Headers[AmzContentSHA256], presignData.Service, presignData.Region, presignData.SignTime) payloadHash := presignData.Headers[AmzContentSHA256]
if payloadHash == "" {
payloadHash = UnsignedPayload
}
signedURI, _, err := signer.PresignHTTP(ctx, creds, req, payloadHash, presignData.Service, presignData.Region, presignData.SignTime)
if err != nil { if err != nil {
return nil, fmt.Errorf("presign: %w", err) return nil, fmt.Errorf("presign: %w", err)
} }
@ -93,7 +98,13 @@ func PresignRequestV4a(cred aws.Credentials, reqData RequestData, presignData Pr
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to derive assymetric key from credentials: %w", err) return nil, fmt.Errorf("failed to derive assymetric key from credentials: %w", err)
} }
presignedURL, _, err := signer.PresignHTTP(req.Context(), creds, req, presignData.Headers[AmzContentSHA256], presignData.Service, []string{presignData.Region}, presignData.SignTime)
payloadHash := presignData.Headers[AmzContentSHA256]
if payloadHash == "" {
payloadHash = UnsignedPayload
}
presignedURL, _, err := signer.PresignHTTP(req.Context(), creds, req, payloadHash, presignData.Service, []string{presignData.Region}, presignData.SignTime)
if err != nil { if err != nil {
return nil, fmt.Errorf("presign: %w", err) return nil, fmt.Errorf("presign: %w", err)
} }

View file

@ -77,8 +77,7 @@ func TestCheckSign(t *testing.T) {
Lifetime: 10 * time.Minute, Lifetime: 10 * time.Minute,
SignTime: time.Now().UTC(), SignTime: time.Now().UTC(),
Headers: map[string]string{ Headers: map[string]string{
ContentTypeHdr: "text/plain", ContentTypeHdr: "text/plain",
AmzContentSHA256: UnsignedPayload,
}, },
} }
@ -100,12 +99,14 @@ func TestCheckSign(t *testing.T) {
postReg: NewRegexpMatcher(postPolicyCredentialRegexp), postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
settings: &centerSettingsMock{}, settings: &centerSettingsMock{},
} }
box, err := c.Authenticate(req) box, err := c.Authenticate(ctx, req)
require.NoError(t, err) require.NoError(t, err)
require.EqualValues(t, expBox, box.AccessBox) require.EqualValues(t, expBox, box.AccessBox)
} }
func TestCheckSignV4a(t *testing.T) { func TestCheckSignV4a(t *testing.T) {
ctx := context.Background()
var accessKeyAddr oid.Address var accessKeyAddr oid.Address
err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto") err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto")
require.NoError(t, err) require.NoError(t, err)
@ -146,10 +147,10 @@ func TestCheckSignV4a(t *testing.T) {
c := &Center{ c := &Center{
cli: mock, cli: mock,
regV4a: NewRegexpMatcher(authorizationFieldV4aRegexp), regV4a: NewRegexpMatcher(AuthorizationFieldV4aRegexp),
postReg: NewRegexpMatcher(postPolicyCredentialRegexp), postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
} }
box, err := c.Authenticate(req) box, err := c.Authenticate(ctx, req)
require.NoError(t, err) require.NoError(t, err)
require.EqualValues(t, expBox, box.AccessBox) require.EqualValues(t, expBox, box.AccessBox)
} }

View file

@ -1,4 +1,6 @@
// This file is adopting https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/stream.go for sigv4a. // This file is adopting https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/stream.go for sigv4a.
// with changes
// * add VerifyTrailerSignature
package v4a package v4a
@ -88,6 +90,39 @@ func (s *StreamSigner) buildEventStreamStringToSign(headers, payload, previousSi
}, "\n") }, "\n")
} }
func (s *StreamSigner) VerifyTrailerSignature(payload []byte, signingTime time.Time, signature []byte) error {
prevSignature := s.prevSignature
st := v4Internal.NewSigningTime(signingTime)
scope := buildCredentialScope(st, s.service)
stringToSign := s.buildEventStreamStringToSignTrailer(payload, prevSignature, scope, &st)
ok, err := signerCrypto.VerifySignature(&s.credentials.PrivateKey.PublicKey, makeHash(sha256.New(), []byte(stringToSign)), signature)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("v4a: invalid signature")
}
s.prevSignature = signature
return nil
}
func (s *StreamSigner) buildEventStreamStringToSignTrailer(payload, previousSignature []byte, credentialScope string, signingTime *v4Internal.SigningTime) string {
hash := sha256.New()
return strings.Join([]string{
"AWS4-ECDSA-P256-SHA256-TRAILER",
signingTime.TimeFormat(),
credentialScope,
hex.EncodeToString(previousSignature),
hex.EncodeToString(makeHash(hash, payload)),
}, "\n")
}
func buildCredentialScope(st v4Internal.SigningTime, service string) string { func buildCredentialScope(st v4Internal.SigningTime, service string) string {
return strings.Join([]string{ return strings.Join([]string{
st.Format(shortTimeFormat), st.Format(shortTimeFormat),

View file

@ -1,6 +1,7 @@
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header.go // This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header.go
// with changes: // with changes:
// * drop User-Agent header from ignored // * drop User-Agent header from ignored
// * drop Expect header from ignored
package v4 package v4
@ -11,7 +12,7 @@ var IgnoredPresignedHeaders = Rules{
"Authorization": struct{}{}, "Authorization": struct{}{},
"User-Agent": struct{}{}, "User-Agent": struct{}{},
"X-Amzn-Trace-Id": struct{}{}, "X-Amzn-Trace-Id": struct{}{},
"Expect": struct{}{}, //"Expect": struct{}{},
}, },
}, },
} }
@ -24,7 +25,7 @@ var IgnoredHeaders = Rules{
"Authorization": struct{}{}, "Authorization": struct{}{},
//"User-Agent": struct{}{}, //"User-Agent": struct{}{},
"X-Amzn-Trace-Id": struct{}{}, "X-Amzn-Trace-Id": struct{}{},
"Expect": struct{}{}, //"Expect": struct{}{},
}, },
}, },
} }

View file

@ -1,4 +1,6 @@
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header_test.go // This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header_test.go
// with changes:
// * drop Expect header from ignored
package v4 package v4
@ -41,10 +43,10 @@ func TestIgnoredHeaders(t *testing.T) {
Header string Header string
ExpectIgnored bool ExpectIgnored bool
}{ }{
"expect": { //"expect": {
Header: "Expect", // Header: "Expect",
ExpectIgnored: true, // ExpectIgnored: true,
}, //},
"authorization": { "authorization": {
Header: "Authorization", Header: "Authorization",
ExpectIgnored: true, ExpectIgnored: true,

View file

@ -1,4 +1,6 @@
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/stream.go // This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/stream.go
// with changes
// * add GetTrailingSignature
package v4 package v4
@ -87,3 +89,32 @@ func (s *StreamSigner) buildEventStreamStringToSign(headers, payload, previousSi
hex.EncodeToString(makeHash(hash, payload)), hex.EncodeToString(makeHash(hash, payload)),
}, "\n") }, "\n")
} }
// GetTrailerSignature signs the provided header and payload bytes.
func (s *StreamSigner) GetTrailerSignature(payload []byte, signingTime time.Time) ([]byte, error) {
prevSignature := s.prevSignature
st := v4Internal.NewSigningTime(signingTime)
sigKey := s.signingKeyDeriver.DeriveKey(s.credentials, s.service, s.region, st)
scope := v4Internal.BuildCredentialScope(st, s.region, s.service)
stringToSign := s.buildEventStreamStringToSignTrailer(payload, prevSignature, scope, &st)
signature := v4Internal.HMACSHA256(sigKey, []byte(stringToSign))
s.prevSignature = signature
return signature, nil
}
func (s *StreamSigner) buildEventStreamStringToSignTrailer(payload, previousSignature []byte, credentialScope string, signingTime *v4Internal.SigningTime) string {
hash := sha256.New()
return strings.Join([]string{
"AWS4-HMAC-SHA256-TRAILER",
signingTime.TimeFormat(),
credentialScope,
hex.EncodeToString(previousSignature),
hex.EncodeToString(makeHash(hash, payload)),
}, "\n")
}

View file

@ -48,7 +48,7 @@ func (o *AccessControlCache) Get(owner user.ID, key string) bool {
result, ok := entry.(bool) result, ok := entry.(bool)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return false return false
} }

View file

@ -67,7 +67,7 @@ func (o *AccessBoxCache) Get(accessKeyID string) *AccessBoxCacheValue {
result, ok := entry.(*AccessBoxCacheValue) result, ok := entry.(*AccessBoxCacheValue)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }

View file

@ -65,7 +65,7 @@ func (o *BucketCache) GetByCID(cnrID cid.ID) *data.BucketInfo {
key, ok := entry.(string) key, ok := entry.(string)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", key))) zap.String("expected", fmt.Sprintf("%T", key)), logs.TagField(logs.TagDatapath))
return nil return nil
} }
@ -81,7 +81,7 @@ func (o *BucketCache) get(key string) *data.BucketInfo {
result, ok := entry.(*data.BucketInfo) result, ok := entry.(*data.BucketInfo)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }

View file

@ -69,7 +69,7 @@ func get[T any](c *FrostfsIDCache, key any) *T {
result, ok := entry.(*T) result, ok := entry.(*T)
if !ok { if !ok {
c.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), c.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }

View file

@ -52,7 +52,7 @@ func NewListSessionCache(config *Config) *ListSessionCache {
session, ok := val.(*data.ListSession) session, ok := val.(*data.ListSession)
if !ok { if !ok {
config.Logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", val)), config.Logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", val)),
zap.String("expected", fmt.Sprintf("%T", session))) zap.String("expected", fmt.Sprintf("%T", session)), logs.TagField(logs.TagDatapath))
} }
if !session.Acquired.Load() { if !session.Acquired.Load() {
@ -72,7 +72,7 @@ func (l *ListSessionCache) GetListSession(key ListSessionKey) *data.ListSession
result, ok := entry.(*data.ListSession) result, ok := entry.(*data.ListSession)
if !ok { if !ok {
l.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), l.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }

2
api/cache/names.go vendored
View file

@ -50,7 +50,7 @@ func (o *ObjectsNameCache) Get(key string) *oid.Address {
result, ok := entry.(oid.Address) result, ok := entry.(oid.Address)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }

View file

@ -54,7 +54,7 @@ func (c *NetworkCache) GetNetworkInfo() *netmap.NetworkInfo {
result, ok := entry.(netmap.NetworkInfo) result, ok := entry.(netmap.NetworkInfo)
if !ok { if !ok {
c.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), c.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }
@ -74,7 +74,7 @@ func (c *NetworkCache) GetNetmap() *netmap.NetMap {
result, ok := entry.(netmap.NetMap) result, ok := entry.(netmap.NetMap)
if !ok { if !ok {
c.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), c.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }

View file

@ -49,7 +49,7 @@ func (o *ObjectsCache) GetObject(address oid.Address) *data.ExtendedObjectInfo {
result, ok := entry.(*data.ExtendedObjectInfo) result, ok := entry.(*data.ExtendedObjectInfo)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }

View file

@ -8,14 +8,14 @@ import (
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
objecttest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/test" objecttest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap" "go.uber.org/zap/zaptest"
) )
func getTestConfig() *Config { func getTestConfig(t *testing.T) *Config {
return &Config{ return &Config{
Size: 10, Size: 10,
Lifetime: 5 * time.Second, Lifetime: 5 * time.Second,
Logger: zap.NewExample(), Logger: zaptest.NewLogger(t),
} }
} }
@ -44,7 +44,7 @@ func TestCache(t *testing.T) {
} }
t.Run("check get", func(t *testing.T) { t.Run("check get", func(t *testing.T) {
cache := New(getTestConfig()) cache := New(getTestConfig(t))
err := cache.PutObject(extObjInfo) err := cache.PutObject(extObjInfo)
require.NoError(t, err) require.NoError(t, err)
@ -53,7 +53,7 @@ func TestCache(t *testing.T) {
}) })
t.Run("check delete", func(t *testing.T) { t.Run("check delete", func(t *testing.T) {
cache := New(getTestConfig()) cache := New(getTestConfig(t))
err := cache.PutObject(extObjInfo) err := cache.PutObject(extObjInfo)
require.NoError(t, err) require.NoError(t, err)

View file

@ -77,7 +77,7 @@ func (l *ObjectsListCache) GetVersions(key ObjectsListKey) []*data.NodeVersion {
result, ok := entry.([]*data.NodeVersion) result, ok := entry.([]*data.NodeVersion)
if !ok { if !ok {
l.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), l.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }
@ -96,7 +96,7 @@ func (l *ObjectsListCache) CleanCacheEntriesContainingObject(objectName string,
k, ok := key.(ObjectsListKey) k, ok := key.(ObjectsListKey)
if !ok { if !ok {
l.logger.Warn(logs.InvalidCacheKeyType, zap.String("actual", fmt.Sprintf("%T", key)), l.logger.Warn(logs.InvalidCacheKeyType, zap.String("actual", fmt.Sprintf("%T", key)),
zap.String("expected", fmt.Sprintf("%T", k))) zap.String("expected", fmt.Sprintf("%T", k)), logs.TagField(logs.TagDatapath))
continue continue
} }
if cnr.Equals(k.cid) && strings.HasPrefix(objectName, k.prefix) { if cnr.Equals(k.cid) && strings.HasPrefix(objectName, k.prefix) {

View file

@ -8,17 +8,17 @@ import (
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test" cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test" oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap" "go.uber.org/zap/zaptest"
) )
const testingCacheLifetime = 5 * time.Second const testingCacheLifetime = 5 * time.Second
const testingCacheSize = 10 const testingCacheSize = 10
func getTestObjectsListConfig() *Config { func getTestObjectsListConfig(t *testing.T) *Config {
return &Config{ return &Config{
Size: testingCacheSize, Size: testingCacheSize,
Lifetime: testingCacheLifetime, Lifetime: testingCacheLifetime,
Logger: zap.NewExample(), Logger: zaptest.NewLogger(t),
} }
} }
@ -35,7 +35,7 @@ func TestObjectsListCache(t *testing.T) {
t.Run("lifetime", func(t *testing.T) { t.Run("lifetime", func(t *testing.T) {
var ( var (
config = getTestObjectsListConfig() config = getTestObjectsListConfig(t)
cache = NewObjectsListCache(config) cache = NewObjectsListCache(config)
listKey = ObjectsListKey{cid: cidKey} listKey = ObjectsListKey{cid: cidKey}
) )
@ -53,7 +53,7 @@ func TestObjectsListCache(t *testing.T) {
t.Run("get cache with empty prefix", func(t *testing.T) { t.Run("get cache with empty prefix", func(t *testing.T) {
var ( var (
cache = NewObjectsListCache(getTestObjectsListConfig()) cache = NewObjectsListCache(getTestObjectsListConfig(t))
listKey = ObjectsListKey{cid: cidKey} listKey = ObjectsListKey{cid: cidKey}
) )
err := cache.PutVersions(listKey, versions) err := cache.PutVersions(listKey, versions)
@ -73,7 +73,7 @@ func TestObjectsListCache(t *testing.T) {
prefix: "dir", prefix: "dir",
} }
cache := NewObjectsListCache(getTestObjectsListConfig()) cache := NewObjectsListCache(getTestObjectsListConfig(t))
err := cache.PutVersions(listKey, versions) err := cache.PutVersions(listKey, versions)
require.NoError(t, err) require.NoError(t, err)
@ -98,7 +98,7 @@ func TestObjectsListCache(t *testing.T) {
} }
) )
cache := NewObjectsListCache(getTestObjectsListConfig()) cache := NewObjectsListCache(getTestObjectsListConfig(t))
err := cache.PutVersions(listKey, versions) err := cache.PutVersions(listKey, versions)
require.NoError(t, err) require.NoError(t, err)
@ -116,7 +116,7 @@ func TestObjectsListCache(t *testing.T) {
} }
) )
cache := NewObjectsListCache(getTestObjectsListConfig()) cache := NewObjectsListCache(getTestObjectsListConfig(t))
err := cache.PutVersions(listKey, versions) err := cache.PutVersions(listKey, versions)
require.NoError(t, err) require.NoError(t, err)
@ -137,7 +137,7 @@ func TestCleanCacheEntriesChangedWithPutObject(t *testing.T) {
} }
t.Run("put object to the root of the bucket", func(t *testing.T) { t.Run("put object to the root of the bucket", func(t *testing.T) {
config := getTestObjectsListConfig() config := getTestObjectsListConfig(t)
config.Lifetime = time.Minute config.Lifetime = time.Minute
cache := NewObjectsListCache(config) cache := NewObjectsListCache(config)
for _, k := range keys { for _, k := range keys {
@ -156,7 +156,7 @@ func TestCleanCacheEntriesChangedWithPutObject(t *testing.T) {
}) })
t.Run("put object to dir/", func(t *testing.T) { t.Run("put object to dir/", func(t *testing.T) {
config := getTestObjectsListConfig() config := getTestObjectsListConfig(t)
config.Lifetime = time.Minute config.Lifetime = time.Minute
cache := NewObjectsListCache(config) cache := NewObjectsListCache(config)
for _, k := range keys { for _, k := range keys {
@ -175,7 +175,7 @@ func TestCleanCacheEntriesChangedWithPutObject(t *testing.T) {
}) })
t.Run("put object to dir/lol/", func(t *testing.T) { t.Run("put object to dir/lol/", func(t *testing.T) {
config := getTestObjectsListConfig() config := getTestObjectsListConfig(t)
config.Lifetime = time.Minute config.Lifetime = time.Minute
cache := NewObjectsListCache(config) cache := NewObjectsListCache(config)
for _, k := range keys { for _, k := range keys {

2
api/cache/policy.go vendored
View file

@ -54,7 +54,7 @@ func (o *MorphPolicyCache) Get(key MorphPolicyCacheKey) []*chain.Chain {
result, ok := entry.([]*chain.Chain) result, ok := entry.([]*chain.Chain)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }

8
api/cache/system.go vendored
View file

@ -50,7 +50,7 @@ func (o *SystemCache) GetObject(key string) *data.ObjectInfo {
result, ok := entry.(*data.ObjectInfo) result, ok := entry.(*data.ObjectInfo)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }
@ -81,7 +81,7 @@ func (o *SystemCache) GetCORS(key string) *data.CORSConfiguration {
result, ok := entry.(*data.CORSConfiguration) result, ok := entry.(*data.CORSConfiguration)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }
@ -97,7 +97,7 @@ func (o *SystemCache) GetLifecycleConfiguration(key string) *data.LifecycleConfi
result, ok := entry.(*data.LifecycleConfiguration) result, ok := entry.(*data.LifecycleConfiguration)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }
@ -113,7 +113,7 @@ func (o *SystemCache) GetSettings(key string) *data.BucketSettings {
result, ok := entry.(*data.BucketSettings) result, ok := entry.(*data.BucketSettings)
if !ok { if !ok {
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)), o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
zap.String("expected", fmt.Sprintf("%T", result))) zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
return nil return nil
} }

View file

@ -2,6 +2,7 @@ package data
import ( import (
"encoding/xml" "encoding/xml"
"fmt"
"strings" "strings"
"time" "time"
@ -20,6 +21,8 @@ const (
VersioningUnversioned = "Unversioned" VersioningUnversioned = "Unversioned"
VersioningEnabled = "Enabled" VersioningEnabled = "Enabled"
VersioningSuspended = "Suspended" VersioningSuspended = "Suspended"
corsFilePathTemplate = "/%s.cors"
) )
type ( type (
@ -103,6 +106,11 @@ func (b *BucketInfo) CORSObjectName() string {
return b.CID.EncodeToString() + bktCORSConfigurationObject return b.CID.EncodeToString() + bktCORSConfigurationObject
} }
// CORSObjectFilePath returns a FilePath for a bucket CORS configuration file.
func (b *BucketInfo) CORSObjectFilePath() string {
return fmt.Sprintf(corsFilePathTemplate, b.CID)
}
func (b *BucketInfo) LifecycleConfigurationObjectName() string { func (b *BucketInfo) LifecycleConfigurationObjectName() string {
return b.CID.EncodeToString() + bktLifecycleConfigurationObject return b.CID.EncodeToString() + bktLifecycleConfigurationObject
} }

View file

@ -109,7 +109,6 @@ type MultipartInfo struct {
Owner user.ID Owner user.ID
Created time.Time Created time.Time
Meta map[string]string Meta map[string]string
CopiesNumbers []uint32
Finished bool Finished bool
CreationEpoch uint64 CreationEpoch uint64
} }

View file

@ -60,7 +60,6 @@ const (
ErrMalformedACL ErrMalformedACL
ErrMalformedXML ErrMalformedXML
ErrMissingContentLength ErrMissingContentLength
ErrMissingContentMD5
ErrMissingRequestBodyError ErrMissingRequestBodyError
ErrMissingSecurityHeader ErrMissingSecurityHeader
ErrNoSuchBucket ErrNoSuchBucket
@ -478,12 +477,6 @@ var errorCodes = errorCodeMap{
Description: "You must provide the Content-Length HTTP header.", Description: "You must provide the Content-Length HTTP header.",
HTTPStatusCode: http.StatusLengthRequired, HTTPStatusCode: http.StatusLengthRequired,
}, },
ErrMissingContentMD5: {
ErrCode: ErrMissingContentMD5,
Code: "MissingContentMD5",
Description: "Missing required header for this request: Content-Md5.",
HTTPStatusCode: http.StatusBadRequest,
},
ErrMissingSecurityHeader: { ErrMissingSecurityHeader: {
ErrCode: ErrMissingSecurityHeader, ErrCode: ErrMissingSecurityHeader,
Code: "MissingSecurityHeader", Code: "MissingSecurityHeader",

View file

@ -11,6 +11,7 @@ import (
"net/http" "net/http"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -47,7 +48,9 @@ const (
) )
func (h *handler) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetBucketACLHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetBucketACL")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -99,7 +102,7 @@ func (h *handler) encodePrivateCannedACL(ctx context.Context, bktInfo *data.Buck
ownerEncodedID := ownerDisplayName ownerEncodedID := ownerDisplayName
if settings.OwnerKey == nil { if settings.OwnerKey == nil {
h.reqLogger(ctx).Warn(logs.BucketOwnerKeyIsMissing, zap.String("owner", bktInfo.Owner.String())) h.reqLogger(ctx).Warn(logs.BucketOwnerKeyIsMissing, zap.String("owner", bktInfo.Owner.String()), logs.TagField(logs.TagDatapath))
} else { } else {
ownerDisplayName = settings.OwnerKey.Address() ownerDisplayName = settings.OwnerKey.Address()
ownerEncodedID = hex.EncodeToString(settings.OwnerKey.Bytes()) ownerEncodedID = hex.EncodeToString(settings.OwnerKey.Bytes())
@ -127,7 +130,9 @@ func getTokenIssuerKey(box *accessbox.Box) (*keys.PublicKey, error) {
} }
func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutBucketACLHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutBucketACL")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -150,7 +155,7 @@ func (h *handler) putBucketACLAPEHandler(w http.ResponseWriter, r *http.Request,
defer func() { defer func() {
if errBody := r.Body.Close(); errBody != nil { if errBody := r.Body.Close(); errBody != nil {
h.reqLogger(ctx).Warn(logs.CouldNotCloseRequestBody, zap.Error(errBody)) h.reqLogger(ctx).Warn(logs.CouldNotCloseRequestBody, zap.Error(errBody), logs.TagField(logs.TagDatapath))
} }
}() }()
@ -194,7 +199,9 @@ func (h *handler) putBucketACLAPEHandler(w http.ResponseWriter, r *http.Request,
} }
func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetObjectACL")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -216,7 +223,9 @@ func (h *handler) GetObjectACLHandler(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutObjectACL")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
if _, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName); err != nil { if _, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName); err != nil {
@ -228,7 +237,9 @@ func (h *handler) PutObjectACLHandler(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) GetBucketPolicyStatusHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetBucketPolicyStatusHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetBucketPolicyStatus")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -271,7 +282,9 @@ func (h *handler) GetBucketPolicyStatusHandler(w http.ResponseWriter, r *http.Re
} }
func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetBucketPolicy")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -298,7 +311,9 @@ func (h *handler) GetBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
} }
func (h *handler) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) DeleteBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.DeleteBucketPolicy")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -328,7 +343,9 @@ func checkOwner(info *data.BucketInfo, owner string) error {
} }
func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutBucketPolicy")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -382,7 +399,7 @@ func (h *handler) PutBucketPolicyHandler(w http.ResponseWriter, r *http.Request)
h.logAndSendError(ctx, w, "could not convert s3 policy to native chain policy", reqInfo, err) h.logAndSendError(ctx, w, "could not convert s3 policy to native chain policy", reqInfo, err)
return return
} else { } else {
h.reqLogger(ctx).Warn(logs.PolicyCouldntBeConvertedToNativeRules) h.reqLogger(ctx).Warn(logs.PolicyCouldntBeConvertedToNativeRules, logs.TagField(logs.TagDatapath))
} }
chainsToSave := []*chain.Chain{s3Chain} chainsToSave := []*chain.Chain{s3Chain}

View file

@ -7,6 +7,7 @@ import (
"encoding/xml" "encoding/xml"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"net/url"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
@ -297,10 +298,17 @@ type createBucketInfo struct {
Key *keys.PrivateKey Key *keys.PrivateKey
} }
type bucketPrm struct {
bktName string
query url.Values
box *accessbox.Box
createParams createBucketParams
}
func createBucket(hc *handlerContext, bktName string) *createBucketInfo { func createBucket(hc *handlerContext, bktName string) *createBucketInfo {
box, key := createAccessBox(hc.t) box, key := createAccessBox(hc.t)
w := createBucketBase(hc, bktName, box) w := createBucketBase(hc, bucketPrm{bktName: bktName, box: box})
assertStatus(hc.t, w, http.StatusOK) assertStatus(hc.t, w, http.StatusOK)
bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName) bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName)
@ -314,13 +322,32 @@ func createBucket(hc *handlerContext, bktName string) *createBucketInfo {
} }
func createBucketAssertS3Error(hc *handlerContext, bktName string, box *accessbox.Box, code apierr.ErrorCode) { func createBucketAssertS3Error(hc *handlerContext, bktName string, box *accessbox.Box, code apierr.ErrorCode) {
w := createBucketBase(hc, bktName, box) w := createBucketBase(hc, bucketPrm{bktName: bktName, box: box})
assertS3Error(hc.t, w, apierr.GetAPIError(code)) assertS3Error(hc.t, w, apierr.GetAPIError(code))
} }
func createBucketBase(hc *handlerContext, bktName string, box *accessbox.Box) *httptest.ResponseRecorder { func createBucketWithConstraint(hc *handlerContext, bktName, constraint string) *createBucketInfo {
w, r := prepareTestRequest(hc, bktName, "", nil) box, key := createAccessBox(hc.t)
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box}) var prm createBucketParams
if constraint != "" {
prm.LocationConstraint = constraint
}
w := createBucketBase(hc, bucketPrm{bktName: bktName, box: box, createParams: prm})
assertStatus(hc.t, w, http.StatusOK)
bktInfo, err := hc.Layer().GetBucketInfo(hc.Context(), bktName)
require.NoError(hc.t, err)
return &createBucketInfo{
BktInfo: bktInfo,
Box: box,
Key: key,
}
}
func createBucketBase(hc *handlerContext, prm bucketPrm) *httptest.ResponseRecorder {
w, r := prepareTestFullRequest(hc, prm.bktName, "", nil, prm.createParams)
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: prm.box})
r = r.WithContext(ctx) r = r.WithContext(ctx)
hc.Handler().CreateBucketHandler(w, r) hc.Handler().CreateBucketHandler(w, r)
return w return w

View file

@ -42,6 +42,7 @@ type (
RetryMaxBackoff() time.Duration RetryMaxBackoff() time.Duration
RetryStrategy() RetryStrategy RetryStrategy() RetryStrategy
TLSTerminationHeader() string TLSTerminationHeader() string
ListingKeepaliveThrottle() time.Duration
} }
FrostFSID interface { FrostFSID interface {

View file

@ -8,6 +8,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -70,7 +71,9 @@ var validAttributes = map[string]struct{}{
} }
func (h *handler) GetObjectAttributesHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetObjectAttributesHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetObjectAttributes")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
params, err := parseGetObjectAttributeArgs(r, h.reqLogger(ctx)) params, err := parseGetObjectAttributeArgs(r, h.reqLogger(ctx))

View file

@ -0,0 +1,73 @@
package handler
import (
"net/http"
"strconv"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
)
const maxBucketList = 10000
// ListBucketsHandler handles bucket listing requests.
func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.ListBuckets")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx)
params, err := parseListBucketParams(r)
if err != nil {
h.logAndSendError(ctx, w, "failed to parse params", reqInfo, err)
return
}
resp, err := h.obj.ListBuckets(ctx, params)
if err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
return
}
if err = middleware.EncodeToResponse(w, encodeListBuckets(reqInfo.User, resp, params)); err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
}
}
func encodeListBuckets(owner string, resp layer.ListBucketsResult, params layer.ListBucketsParams) *ListBucketsResponse {
res := &ListBucketsResponse{
Owner: Owner{
ID: owner,
DisplayName: owner,
},
ContinuationToken: resp.ContinuationToken,
Prefix: params.Prefix,
}
for _, item := range resp.Containers {
res.Buckets.Buckets = append(res.Buckets.Buckets, Bucket{
Name: item.Name,
CreationDate: item.Created.UTC().Format(time.RFC3339),
BucketRegion: item.LocationConstraint,
})
}
return res
}
func parseListBucketParams(r *http.Request) (prm layer.ListBucketsParams, err error) {
prm.MaxBuckets = maxBucketList
strMaxBuckets := r.URL.Query().Get(middleware.QueryMaxBuckets)
if strMaxBuckets != "" {
if prm.MaxBuckets, err = strconv.Atoi(strMaxBuckets); err != nil || prm.MaxBuckets < 0 {
return layer.ListBucketsParams{}, errors.GetAPIError(errors.ErrInvalidMaxKeys)
}
}
prm.Prefix = r.URL.Query().Get(middleware.QueryPrefix)
prm.BucketRegion = r.URL.Query().Get(middleware.QueryBucketRegion)
prm.ContinuationToken = r.URL.Query().Get(middleware.QueryContinuationToken)
return
}

View file

@ -0,0 +1,174 @@
package handler
import (
"encoding/xml"
"net/http"
"net/http/httptest"
"net/url"
"sort"
"testing"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"github.com/stretchr/testify/require"
)
func TestHandler_ListBucketsHandler(t *testing.T) {
const defaultConstraint = "default"
region := "us-west-1"
hc := prepareWithoutCORSHandlerContext(t)
hc.config.putLocationConstraint(region)
props := []Bucket{
{Name: "first"},
{Name: "regional", BucketRegion: "us-west-1"},
{Name: "third"},
}
sort.Slice(props, func(i, j int) bool {
return props[i].Name < props[j].Name
})
for _, bkt := range props {
createBucketWithConstraint(hc, bkt.Name, bkt.BucketRegion)
}
for _, tt := range []struct {
title string
token string
prefix string
bucketRegion string
maxBuckets string
expectErr bool
expected []Bucket
expectedToken string
}{
{
title: "no params",
expected: []Bucket{
{Name: "first", BucketRegion: defaultConstraint},
{Name: "regional", BucketRegion: "us-west-1"},
{Name: "third", BucketRegion: defaultConstraint},
},
},
{
title: "negative max-buckets",
maxBuckets: "-1",
expected: []Bucket{},
expectErr: true,
},
{
title: "zero max-buckets",
maxBuckets: "0",
expected: []Bucket{},
},
{
title: "prefix",
prefix: "thi",
expected: []Bucket{{Name: "third", BucketRegion: defaultConstraint}},
},
{
title: "wrong prefix",
prefix: "sdh",
expected: []Bucket{},
},
{
title: "bucket region",
bucketRegion: region,
expected: []Bucket{{Name: "regional", BucketRegion: "us-west-1"}},
},
{
title: "default bucket region",
bucketRegion: defaultConstraint,
expected: []Bucket{
{Name: "first", BucketRegion: defaultConstraint},
{Name: "third", BucketRegion: defaultConstraint},
},
},
{
title: "wrong bucket region",
bucketRegion: "sj dfdlsj",
expected: []Bucket{},
},
} {
t.Run(tt.title, func(t *testing.T) {
if tt.expectErr {
listBucketsErr(hc, tt.prefix, tt.token, tt.bucketRegion, tt.maxBuckets, apierr.GetAPIError(apierr.ErrInvalidMaxKeys))
return
}
resp := listBuckets(hc, tt.prefix, tt.token, tt.bucketRegion, tt.maxBuckets)
require.Len(t, resp.Buckets.Buckets, len(tt.expected))
require.Equal(t, tt.prefix, resp.Prefix)
require.Equal(t, hc.owner.String(), resp.Owner.ID)
if len(resp.Buckets.Buckets) > 0 {
t.Log(resp.Buckets.Buckets[0].Name)
}
for i, bkt := range resp.Buckets.Buckets {
require.Equal(t, tt.expected[i].Name, bkt.Name)
require.Equal(t, tt.expected[i].BucketRegion, bkt.BucketRegion)
}
})
}
t.Run("pagination", func(t *testing.T) {
t.Run("happy path", func(t *testing.T) {
resp := listBuckets(hc, "", "", "", "1")
require.Len(t, resp.Buckets.Buckets, 1)
require.Equal(t, props[0].Name, resp.Buckets.Buckets[0].Name)
require.NotEmpty(t, resp.ContinuationToken)
resp = listBuckets(hc, "", resp.ContinuationToken, "", "1")
require.Len(t, resp.Buckets.Buckets, 1)
require.Equal(t, props[1].Name, resp.Buckets.Buckets[0].Name)
require.NotEmpty(t, resp.ContinuationToken)
resp = listBuckets(hc, "", resp.ContinuationToken, "", "1")
require.Len(t, resp.Buckets.Buckets, 1)
require.Equal(t, props[2].Name, resp.Buckets.Buckets[0].Name)
require.Empty(t, resp.ContinuationToken)
})
t.Run("wrong continuation-token", func(t *testing.T) {
resp := listBuckets(hc, "", "CebuVwfRpdMqi9dvgV2SUNbrkfteGtudchKKhNabXUu9", "", "1")
require.Len(t, resp.Buckets.Buckets, 0)
require.Empty(t, resp.ContinuationToken)
})
})
}
func listBuckets(hc *handlerContext, prefix, token, bucketRegion, maxBuckets string) ListBucketsResponse {
query := url.Values{
middleware.QueryPrefix: []string{prefix},
middleware.QueryContinuationToken: []string{token},
middleware.QueryBucketRegion: []string{bucketRegion},
middleware.QueryMaxBuckets: []string{maxBuckets},
}
w := listBucketsBase(hc, bucketPrm{query: query})
assertStatus(hc.t, w, http.StatusOK)
var resp ListBucketsResponse
err := xml.NewDecoder(w.Body).Decode(&resp)
require.NoError(hc.t, err)
return resp
}
func listBucketsErr(hc *handlerContext, prefix, token, bucketRegion, maxBuckets string, err apierr.Error) {
query := url.Values{
middleware.QueryPrefix: []string{prefix},
middleware.QueryContinuationToken: []string{token},
middleware.QueryBucketRegion: []string{bucketRegion},
middleware.QueryMaxBuckets: []string{maxBuckets},
}
w := listBucketsBase(hc, bucketPrm{query: query})
assertS3Error(hc.t, w, err)
}
func listBucketsBase(hc *handlerContext, prm bucketPrm) *httptest.ResponseRecorder {
box, _ := createAccessBox(hc.t)
w, r := prepareTestFullRequest(hc, "", "", prm.query, nil)
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
r = r.WithContext(ctx)
hc.Handler().ListBucketsHandler(w, r)
return w
}

View file

@ -6,6 +6,7 @@ import (
"regexp" "regexp"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
@ -40,18 +41,19 @@ func path2BucketObject(path string) (string, string, error) {
} }
func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.CopyObject")
defer span.End()
var ( var (
err error err error
versionID string versionID string
metadata map[string]string metadata map[string]string
tagSet map[string]string tagSet map[string]string
ctx = r.Context()
reqInfo = middleware.GetReqInfo(ctx)
cannedACLStatus = aclHeadersStatus(r)
) )
reqInfo := middleware.GetReqInfo(ctx)
cannedACLStatus := aclHeadersStatus(r)
src := r.Header.Get(api.AmzCopySource) src := r.Header.Get(api.AmzCopySource)
// Check https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html // Check https://docs.aws.amazon.com/AmazonS3/latest/dev/ObjectVersioning.html
// Regardless of whether you have enabled versioning, each object in your bucket // Regardless of whether you have enabled versioning, each object in your bucket
@ -244,7 +246,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
h.reqLogger(ctx).Info(logs.ObjectIsCopied, zap.Stringer("object_id", dstObjInfo.ID)) h.reqLogger(ctx).Info(logs.ObjectIsCopied, zap.Stringer("object_id", dstObjInfo.ID), logs.TagField(logs.TagExternalStorage))
if dstEncryptionParams.Enabled() { if dstEncryptionParams.Enabled() {
addSSECHeaders(w.Header(), r.Header) addSSECHeaders(w.Header(), r.Header)

View file

@ -5,10 +5,13 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
qostagging "git.frostfs.info/TrueCloudLab/frostfs-qos/tagging"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/util"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
"go.uber.org/zap" "go.uber.org/zap"
) )
@ -20,7 +23,10 @@ const (
) )
func (h *handler) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetBucketCors")
defer span.End()
ctx = qostagging.ContextWithIOTag(ctx, util.InternalIOTag)
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -42,7 +48,10 @@ func (h *handler) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutBucketCors")
defer span.End()
ctx = qostagging.ContextWithIOTag(ctx, util.InternalIOTag)
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -76,7 +85,10 @@ func (h *handler) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.DeleteBucketCors")
defer span.End()
ctx = qostagging.ContextWithIOTag(ctx, util.InternalIOTag)
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -93,6 +105,9 @@ func (h *handler) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request
} }
func (h *handler) AppendCORSHeaders(w http.ResponseWriter, r *http.Request) { func (h *handler) AppendCORSHeaders(w http.ResponseWriter, r *http.Request) {
ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.AppendCORSHeaders")
defer span.End()
if r.Method == http.MethodOptions { if r.Method == http.MethodOptions {
return return
} }
@ -101,20 +116,20 @@ func (h *handler) AppendCORSHeaders(w http.ResponseWriter, r *http.Request) {
return return
} }
ctx := r.Context() ctx = qostagging.ContextWithIOTag(ctx, util.InternalIOTag)
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
if reqInfo.BucketName == "" { if reqInfo.BucketName == "" {
return return
} }
bktInfo, err := h.getBucketInfo(ctx, reqInfo.BucketName) bktInfo, err := h.getBucketInfo(ctx, reqInfo.BucketName)
if err != nil { if err != nil {
h.reqLogger(ctx).Warn(logs.GetBucketInfo, zap.Error(err)) h.reqLogger(ctx).Warn(logs.GetBucketInfo, zap.Error(err), logs.TagField(logs.TagDatapath))
return return
} }
cors, err := h.obj.GetBucketCORS(ctx, bktInfo, h.cfg.NewXMLDecoder) cors, err := h.obj.GetBucketCORS(ctx, bktInfo, h.cfg.NewXMLDecoder)
if err != nil { if err != nil {
h.reqLogger(ctx).Warn(logs.GetBucketCors, zap.Error(err)) h.reqLogger(ctx).Warn(logs.GetBucketCors, zap.Error(err), logs.TagField(logs.TagDatapath))
return return
} }
@ -153,7 +168,10 @@ func (h *handler) AppendCORSHeaders(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) Preflight(w http.ResponseWriter, r *http.Request) { func (h *handler) Preflight(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.Preflight")
defer span.End()
ctx = qostagging.ContextWithIOTag(ctx, util.InternalIOTag)
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketInfo(ctx, reqInfo.BucketName) bktInfo, err := h.getBucketInfo(ctx, reqInfo.BucketName)
if err != nil { if err != nil {

View file

@ -1,12 +1,19 @@
package handler package handler
import ( import (
"encoding/xml"
"net/http" "net/http"
"net/http/httptest"
"strings" "strings"
"testing" "testing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@ -37,26 +44,14 @@ func TestCORSOriginWildcard(t *testing.T) {
hc.Handler().CreateBucketHandler(w, r) hc.Handler().CreateBucketHandler(w, r)
assertStatus(t, w, http.StatusOK) assertStatus(t, w, http.StatusOK)
w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(body)) putBucketCORS(hc, bktName, body)
ctx = middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
r = r.WithContext(ctx)
hc.Handler().PutBucketCorsHandler(w, r)
assertStatus(t, w, http.StatusOK)
w, r = prepareTestPayloadRequest(hc, bktName, "", nil) getBucketCORS(hc, bktName)
hc.Handler().GetBucketCorsHandler(w, r)
assertStatus(t, w, http.StatusOK)
hc.config.useDefaultXMLNS = true hc.config.useDefaultXMLNS = true
w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(bodyNoXmlns)) putBucketCORS(hc, bktName, bodyNoXmlns)
ctx = middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
r = r.WithContext(ctx)
hc.Handler().PutBucketCorsHandler(w, r)
assertStatus(t, w, http.StatusOK)
w, r = prepareTestPayloadRequest(hc, bktName, "", nil) getBucketCORS(hc, bktName)
hc.Handler().GetBucketCorsHandler(w, r)
assertStatus(t, w, http.StatusOK)
} }
func TestPreflight(t *testing.T) { func TestPreflight(t *testing.T) {
@ -170,11 +165,7 @@ func TestPreflightWildcardOrigin(t *testing.T) {
hc.Handler().CreateBucketHandler(w, r) hc.Handler().CreateBucketHandler(w, r)
assertStatus(t, w, http.StatusOK) assertStatus(t, w, http.StatusOK)
w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(body)) putBucketCORS(hc, bktName, body)
ctx = middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
r = r.WithContext(ctx)
hc.Handler().PutBucketCorsHandler(w, r)
assertStatus(t, w, http.StatusOK)
for _, tc := range []struct { for _, tc := range []struct {
name string name string
@ -236,3 +227,183 @@ func TestPreflightWildcardOrigin(t *testing.T) {
}) })
} }
} }
func TestDeleteAllCORSVersions(t *testing.T) {
body := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
`
hc := prepareHandlerContext(t)
bktName := "bucket-delete-all-cors-version"
createBucket(hc, bktName)
require.Len(t, hc.tp.Objects(), 0)
for range 5 {
putBucketCORS(hc, bktName, body)
}
require.Len(t, hc.tp.Objects(), 5)
deleteBucketCORS(hc, bktName)
require.Len(t, hc.tp.Objects(), 0)
}
func TestGetLatestCORSVersion(t *testing.T) {
bodyTree := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
`
body := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>DELETE</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
`
hc := prepareHandlerContextWithMinCache(t)
bktName := "bucket-get-latest-cors"
info := createBucket(hc, bktName)
addCORSToTree(hc, bodyTree, info.BktInfo, info.BktInfo.CID)
w := getBucketCORS(hc, bktName)
requireEqualCORS(hc.t, bodyTree, w.Body.String())
hc.tp.AddCORSObject(info.BktInfo, hc.corsCnrID, body)
w = getBucketCORS(hc, bktName)
requireEqualCORS(hc.t, body, w.Body.String())
hc.tp.AddCORSObject(info.BktInfo, hc.corsCnrID, bodyTree)
w = getBucketCORS(hc, bktName)
requireEqualCORS(hc.t, bodyTree, w.Body.String())
}
func TestDeleteTreeCORSVersions(t *testing.T) {
body := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
`
hc := prepareHandlerContext(t)
bktName := "bucket-delete-tree-cors-versions"
info := createBucket(hc, bktName)
addCORSToTree(hc, body, info.BktInfo, info.BktInfo.CID)
addCORSToTree(hc, body, info.BktInfo, hc.corsCnrID)
require.Len(t, hc.tp.Objects(), 2)
putBucketCORS(hc, bktName, body)
require.Len(t, hc.tp.Objects(), 1)
addCORSToTree(hc, body, info.BktInfo, info.BktInfo.CID)
addCORSToTree(hc, body, info.BktInfo, hc.corsCnrID)
require.Len(t, hc.tp.Objects(), 3)
deleteBucketCORS(hc, bktName)
require.Len(t, hc.tp.Objects(), 0)
}
func TestDeleteCORSInDeleteBucket(t *testing.T) {
body := `
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedOrigin>*</AllowedOrigin>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
`
hc := prepareHandlerContext(t)
bktName := "bucket-delete-cors-in-delete-bucket"
info := createBucket(hc, bktName)
addCORSToTree(hc, body, info.BktInfo, hc.corsCnrID)
addCORSToTree(hc, body, info.BktInfo, info.BktInfo.CID)
hc.tp.AddCORSObject(info.BktInfo, hc.corsCnrID, body)
require.Len(t, hc.tp.Objects(), 3)
hc.owner = info.BktInfo.Owner
deleteBucket(t, hc, bktName, http.StatusNoContent)
require.Len(t, hc.tp.Objects(), 1) // CORS object in bucket container is not deleted
}
func addCORSToTree(hc *handlerContext, cors string, bkt *data.BucketInfo, corsCnrID cid.ID) {
var addr oid.Address
addr.SetContainer(corsCnrID)
addr.SetObject(oidtest.ID())
var obj object.Object
obj.SetPayload([]byte(cors))
obj.SetPayloadSize(uint64(len(cors)))
hc.tp.SetObject(addr, &obj)
meta := make(map[string]string)
meta["FileName"] = "bucket-cors"
meta["OID"] = addr.Object().EncodeToString()
meta["CID"] = addr.Container().EncodeToString()
_, err := hc.treeMock.AddNode(hc.context, bkt, "system", 0, meta)
require.NoError(hc.t, err)
}
func requireEqualCORS(t *testing.T, expected string, actual string) {
expectedCORS := &data.CORSConfiguration{}
err := xml.NewDecoder(strings.NewReader(expected)).Decode(expectedCORS)
require.NoError(t, err)
actualCORS := &data.CORSConfiguration{}
err = xml.NewDecoder(strings.NewReader(actual)).Decode(actualCORS)
require.NoError(t, err)
require.Equal(t, expectedCORS, actualCORS)
}
func putBucketCORS(hc *handlerContext, bktName string, body string) {
w, r := prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(body))
box, _ := createAccessBox(hc.t)
r = r.WithContext(middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box}))
hc.Handler().PutBucketCorsHandler(w, r)
assertStatus(hc.t, w, http.StatusOK)
}
func deleteBucketCORS(hc *handlerContext, bktName string) {
w, r := prepareTestPayloadRequest(hc, bktName, "", nil)
box, _ := createAccessBox(hc.t)
r = r.WithContext(middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box}))
hc.Handler().DeleteBucketCorsHandler(w, r)
assertStatus(hc.t, w, http.StatusNoContent)
}
func getBucketCORS(hc *handlerContext, bktName string) *httptest.ResponseRecorder {
w, r := prepareTestPayloadRequest(hc, bktName, "", nil)
hc.Handler().GetBucketCorsHandler(w, r)
assertStatus(hc.t, w, http.StatusOK)
return w
}

View file

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
@ -61,7 +62,9 @@ type DeleteObjectsResponse struct {
} }
func (h *handler) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.DeleteObject")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
versionID := reqInfo.URL.Query().Get(api.QueryVersionID) versionID := reqInfo.URL.Query().Get(api.QueryVersionID)
versionedObject := []*layer.VersionedObject{{ versionedObject := []*layer.VersionedObject{{
@ -128,15 +131,10 @@ func isErrObjectLocked(err error) bool {
// DeleteMultipleObjectsHandler handles multiple delete requests. // DeleteMultipleObjectsHandler handles multiple delete requests.
func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.DeleteMultipleObjects")
reqInfo := middleware.GetReqInfo(ctx) defer span.End()
// Content-Md5 is required and should be set reqInfo := middleware.GetReqInfo(ctx)
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
if _, ok := r.Header[api.ContentMD5]; !ok {
h.logAndSendError(ctx, w, "missing Content-MD5", reqInfo, errors.GetAPIError(errors.ErrMissingContentMD5))
return
}
// Content-Length is required and should be non-zero // Content-Length is required and should be non-zero
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html // http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
@ -237,7 +235,9 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
} }
func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.DeleteBucket")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
if err != nil { if err != nil {

View file

@ -542,7 +542,6 @@ func deleteObjectsBase(hc *handlerContext, bktName string, objVersions [][2]stri
} }
w, r := prepareTestRequest(hc, bktName, "", req) w, r := prepareTestRequest(hc, bktName, "", req)
r.Header.Set(api.ContentMD5, "")
hc.Handler().DeleteMultipleObjectsHandler(w, r) hc.Handler().DeleteMultipleObjectsHandler(w, r)
assertStatus(hc.t, w, http.StatusOK) assertStatus(hc.t, w, http.StatusOK)

View file

@ -8,6 +8,7 @@ import (
"strings" "strings"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -148,12 +149,11 @@ func writeHeaders(h http.Header, requestHeader http.Header, extendedInfo *data.E
} }
func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
var ( ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetObject")
params *layer.RangeParams defer span.End()
ctx = r.Context() var params *layer.RangeParams
reqInfo = middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
)
conditional := parseConditionalHeaders(r.Header, h.reqLogger(ctx)) conditional := parseConditionalHeaders(r.Header, h.reqLogger(ctx))
@ -255,7 +255,7 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
} }
if err = objPayload.StreamTo(w); err != nil { if err = objPayload.StreamTo(w); err != nil {
h.logAndSendError(ctx, w, "could not stream object payload", reqInfo, err) h.logError(ctx, "could not stream object payload", reqInfo, err)
return return
} }
} }
@ -296,12 +296,12 @@ func parseConditionalHeaders(headers http.Header, log *zap.Logger) *conditionalA
if httpTime, err := parseHTTPTime(headers.Get(api.IfModifiedSince)); err == nil { if httpTime, err := parseHTTPTime(headers.Get(api.IfModifiedSince)); err == nil {
args.IfModifiedSince = httpTime args.IfModifiedSince = httpTime
} else { } else {
log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfModifiedSince, headers.Get(api.IfModifiedSince)), zap.Error(err)) log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfModifiedSince, headers.Get(api.IfModifiedSince)), zap.Error(err), logs.TagField(logs.TagDatapath))
} }
if httpTime, err := parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err == nil { if httpTime, err := parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err == nil {
args.IfUnmodifiedSince = httpTime args.IfUnmodifiedSince = httpTime
} else { } else {
log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfUnmodifiedSince, headers.Get(api.IfUnmodifiedSince)), zap.Error(err)) log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfUnmodifiedSince, headers.Get(api.IfUnmodifiedSince)), zap.Error(err), logs.TagField(logs.TagDatapath))
} }
return args return args

View file

@ -2,6 +2,7 @@ package handler
import ( import (
"bytes" "bytes"
"context"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -197,6 +198,46 @@ func TestGetObject(t *testing.T) {
getObjectAssertS3Error(hc, bktName, objName, emptyVersion, apierr.ErrNoSuchKey) getObjectAssertS3Error(hc, bktName, objName, emptyVersion, apierr.ErrNoSuchKey)
} }
func TestGetObjectStreamError(t *testing.T) {
hc := prepareHandlerContext(t)
bktName, objName := "bucket", "obj"
info := createBucket(hc, bktName)
putObject(hc, bktName, objName)
addr := getAddressOfLastVersion(hc, info.BktInfo, objName)
hc.tp.SetObjectStreamError(addr, 4, context.Canceled)
d, _ := getObject(hc, bktName, objName)
require.Equal(t, "cont", string(d))
}
func TestGetDeletedObject(t *testing.T) {
hc := prepareHandlerContextWithMinCache(t)
bktName, objName := "bucket", "obj"
bktInfo, objInfo := createVersionedBucketAndObject(hc.t, hc, bktName, objName)
putObject(hc, bktName, objName)
checkFound(hc.t, hc, bktName, objName, objInfo.VersionID())
checkFound(hc.t, hc, bktName, objName, emptyVersion)
addr := getAddressOfLastVersion(hc, bktInfo, objName)
t.Run("not found error", func(_ *testing.T) {
hc.tp.SetObjectError(addr, &apistatus.ObjectNotFound{})
hc.tp.SetObjectError(objInfo.Address(), &apistatus.ObjectNotFound{})
getObjectAssertS3Error(hc, bktName, objName, objInfo.VersionID(), apierr.ErrNoSuchVersion)
getObjectAssertS3Error(hc, bktName, objName, emptyVersion, apierr.ErrNoSuchKey)
})
t.Run("already removed error", func(_ *testing.T) {
hc.tp.SetObjectError(addr, &apistatus.ObjectAlreadyRemoved{})
hc.tp.SetObjectError(objInfo.Address(), &apistatus.ObjectAlreadyRemoved{})
getObjectAssertS3Error(hc, bktName, objName, objInfo.VersionID(), apierr.ErrNoSuchVersion)
getObjectAssertS3Error(hc, bktName, objName, emptyVersion, apierr.ErrNoSuchKey)
})
}
func TestGetObjectEnabledMD5(t *testing.T) { func TestGetObjectEnabledMD5(t *testing.T) {
hc := prepareHandlerContext(t) hc := prepareHandlerContext(t)
bktName, objName := "bucket", "obj" bktName, objName := "bucket", "obj"

View file

@ -22,7 +22,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam" engineiam "git.frostfs.info/TrueCloudLab/policy-engine/iam"
utils "github.com/trailofbits/go-fuzz-utils" utils "github.com/trailofbits/go-fuzz-utils"
"go.uber.org/zap/zaptest" "go.uber.org/zap"
) )
var ( var (
@ -40,9 +40,9 @@ const (
func createTestBucketAndInitContext() { func createTestBucketAndInitContext() {
fuzzt = new(tt.T) fuzzt = new(tt.T)
log := zaptest.NewLogger(fuzzt) log := zap.NewExample()
var err error var err error
fuzzHc, err = prepareHandlerContextBase(layer.DefaultCachesConfigs(log)) fuzzHc, err = prepareHandlerContextBase(layer.DefaultCachesConfigs(log), log)
if err != nil { if err != nil {
panic(err) panic(err)
} }
@ -171,9 +171,9 @@ func generateHeaders(tp *utils.TypeProvider, r *http.Request, params []string) e
func InitFuzzCreateBucketHandler() { func InitFuzzCreateBucketHandler() {
fuzzt = new(tt.T) fuzzt = new(tt.T)
log := zaptest.NewLogger(fuzzt) log := zap.NewExample()
var err error var err error
fuzzHc, err = prepareHandlerContextBase(layer.DefaultCachesConfigs(log)) fuzzHc, err = prepareHandlerContextBase(layer.DefaultCachesConfigs(log), log)
if err != nil { if err != nil {
panic(err) panic(err)
} }

View file

@ -23,7 +23,9 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/pkg/service/tree"
bearertest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer/test"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
@ -35,6 +37,7 @@ import (
"github.com/panjf2000/ants/v2" "github.com/panjf2000/ants/v2"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zaptest"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
) )
@ -44,12 +47,13 @@ type handlerContext struct {
} }
type handlerContextBase struct { type handlerContextBase struct {
owner user.ID owner user.ID
h *handler h *handler
tp *layer.TestFrostFS tp *layer.TestFrostFS
tree *tree.Tree tree *tree.Tree
context context.Context context context.Context
config *configMock config *configMock
corsCnrID cid.ID
layerFeatures *layer.FeatureSettingsMock layerFeatures *layer.FeatureSettingsMock
treeMock *tree.ServiceClientMemory treeMock *tree.ServiceClientMemory
@ -74,6 +78,7 @@ func (hc *handlerContextBase) Context() context.Context {
type configMock struct { type configMock struct {
defaultPolicy netmap.PlacementPolicy defaultPolicy netmap.PlacementPolicy
placementPolicies map[string]netmap.PlacementPolicy
copiesNumbers map[string][]uint32 copiesNumbers map[string][]uint32
defaultCopiesNumbers []uint32 defaultCopiesNumbers []uint32
bypassContentEncodingInChunks bool bypassContentEncodingInChunks bool
@ -86,8 +91,9 @@ func (c *configMock) DefaultPlacementPolicy(_ string) netmap.PlacementPolicy {
return c.defaultPolicy return c.defaultPolicy
} }
func (c *configMock) PlacementPolicy(_, _ string) (netmap.PlacementPolicy, bool) { func (c *configMock) PlacementPolicy(_, constraint string) (netmap.PlacementPolicy, bool) {
return netmap.PlacementPolicy{}, false policy, ok := c.placementPolicies[constraint]
return policy, ok
} }
func (c *configMock) CopiesNumbers(_, locationConstraint string) ([]uint32, bool) { func (c *configMock) CopiesNumbers(_, locationConstraint string) ([]uint32, bool) {
@ -151,8 +157,35 @@ func (c *configMock) TLSTerminationHeader() string {
return c.tlsTerminationHeader return c.tlsTerminationHeader
} }
func (c *configMock) ListingKeepaliveThrottle() time.Duration {
return 0
}
func (c *configMock) putLocationConstraint(constraint string) {
c.placementPolicies[constraint] = c.defaultPolicy
}
type handlerConfig struct {
cacheCfg *layer.CachesConfig
withoutCORS bool
}
func prepareHandlerContext(t *testing.T) *handlerContext { func prepareHandlerContext(t *testing.T) *handlerContext {
hc, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(zap.NewExample())) log := zaptest.NewLogger(t)
hc, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: layer.DefaultCachesConfigs(log)}, log)
require.NoError(t, err)
return &handlerContext{
handlerContextBase: hc,
t: t,
}
}
func prepareWithoutCORSHandlerContext(t *testing.T) *handlerContext {
log := zaptest.NewLogger(t)
hc, err := prepareHandlerContextBase(&handlerConfig{
cacheCfg: layer.DefaultCachesConfigs(log),
withoutCORS: true,
}, log)
require.NoError(t, err) require.NoError(t, err)
return &handlerContext{ return &handlerContext{
handlerContextBase: hc, handlerContextBase: hc,
@ -161,7 +194,8 @@ func prepareHandlerContext(t *testing.T) *handlerContext {
} }
func prepareHandlerContextWithMinCache(t *testing.T) *handlerContext { func prepareHandlerContextWithMinCache(t *testing.T) *handlerContext {
hc, err := prepareHandlerContextBase(getMinCacheConfig(zap.NewExample())) log := zaptest.NewLogger(t)
hc, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: getMinCacheConfig(log)}, log)
require.NoError(t, err) require.NoError(t, err)
return &handlerContext{ return &handlerContext{
handlerContextBase: hc, handlerContextBase: hc,
@ -169,13 +203,12 @@ func prepareHandlerContextWithMinCache(t *testing.T) *handlerContext {
} }
} }
func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBase, error) { func prepareHandlerContextBase(config *handlerConfig, log *zap.Logger) (*handlerContextBase, error) {
key, err := keys.NewPrivateKey() key, err := keys.NewPrivateKey()
if err != nil { if err != nil {
return nil, err return nil, err
} }
log := zap.NewExample()
tp := layer.NewTestFrostFS(key) tp := layer.NewTestFrostFS(key)
testResolver := &resolver.Resolver{Name: "test_resolver"} testResolver := &resolver.Resolver{Name: "test_resolver"}
@ -191,7 +224,7 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas
return nil, err return nil, err
} }
treeMock := tree.NewTree(memCli, zap.NewExample()) treeMock := tree.NewTree(memCli, log)
features := &layer.FeatureSettingsMock{} features := &layer.FeatureSettingsMock{}
@ -201,15 +234,23 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas
} }
layerCfg := &layer.Config{ layerCfg := &layer.Config{
Cache: layer.NewCache(cacheCfg), Cache: layer.NewCache(config.cacheCfg),
AnonKey: layer.AnonymousKey{Key: key}, AnonKey: layer.AnonymousKey{Key: key},
Resolver: testResolver, Resolver: testResolver,
TreeService: treeMock, TreeService: treeMock,
Features: features, Features: features,
GateOwner: owner, GateOwner: owner,
GateKey: key,
WorkerPool: pool, WorkerPool: pool,
} }
if !config.withoutCORS {
layerCfg.CORSCnrInfo, err = createCORSContainer(key, tp)
if err != nil {
return nil, err
}
}
var pp netmap.PlacementPolicy var pp netmap.PlacementPolicy
err = pp.DecodeString("REP 1") err = pp.DecodeString("REP 1")
if err != nil { if err != nil {
@ -217,7 +258,8 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas
} }
cfg := &configMock{ cfg := &configMock{
defaultPolicy: pp, defaultPolicy: pp,
placementPolicies: make(map[string]netmap.PlacementPolicy),
} }
h := &handler{ h := &handler{
log: log, log: log,
@ -232,7 +274,7 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas
return nil, err return nil, err
} }
return &handlerContextBase{ hc := &handlerContextBase{
owner: owner, owner: owner,
h: h, h: h,
tp: tp, tp: tp,
@ -243,6 +285,44 @@ func prepareHandlerContextBase(cacheCfg *layer.CachesConfig) (*handlerContextBas
layerFeatures: features, layerFeatures: features,
treeMock: memCli, treeMock: memCli,
cache: layerCfg.Cache, cache: layerCfg.Cache,
}
if layerCfg.CORSCnrInfo != nil {
hc.corsCnrID = layerCfg.CORSCnrInfo.CID
}
return hc, nil
}
func createCORSContainer(key *keys.PrivateKey, tp *layer.TestFrostFS) (*data.BucketInfo, error) {
bearerToken := bearertest.Token()
err := bearerToken.Sign(key.PrivateKey)
if err != nil {
return nil, err
}
bktName := "cors"
res, err := tp.CreateContainer(middleware.SetBox(context.Background(), &middleware.Box{AccessBox: &accessbox.Box{
Gate: &accessbox.GateData{
BearerToken: &bearerToken,
GateKey: key.PublicKey(),
},
}}), frostfs.PrmContainerCreate{
Name: bktName,
Policy: getPlacementPolicy(),
})
if err != nil {
return nil, err
}
var owner user.ID
user.IDFromKey(&owner, key.PrivateKey.PublicKey)
return &data.BucketInfo{
Name: bktName,
Owner: owner,
CID: res.ContainerID,
HomomorphicHashDisabled: res.HomomorphicHashDisabled,
}, nil }, nil
} }

View file

@ -4,6 +4,7 @@ import (
"io" "io"
"net/http" "net/http"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -27,7 +28,9 @@ func getRangeToDetectContentType(maxSize uint64) *layer.RangeParams {
} }
func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.HeadObject")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -123,7 +126,9 @@ func (h *handler) HeadObjectHandler(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) HeadBucketHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) HeadBucketHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.HeadBucket")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)

View file

@ -3,11 +3,14 @@ package handler
import ( import (
"net/http" "net/http"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
) )
func (h *handler) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetBucketLocationHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetBucketLocation")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)

View file

@ -10,6 +10,8 @@ import (
"net/http" "net/http"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
qostagging "git.frostfs.info/TrueCloudLab/frostfs-qos/tagging"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -27,7 +29,10 @@ const (
) )
func (h *handler) GetBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetBucketLifecycle")
defer span.End()
ctx = qostagging.ContextWithIOTag(ctx, util.InternalIOTag)
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -49,40 +54,38 @@ func (h *handler) GetBucketLifecycleHandler(w http.ResponseWriter, r *http.Reque
} }
func (h *handler) PutBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutBucketLifecycle")
defer span.End()
var buf bytes.Buffer var buf bytes.Buffer
tee := io.TeeReader(r.Body, &buf) tee := io.TeeReader(r.Body, &buf)
ctx := r.Context() ctx = qostagging.ContextWithIOTag(ctx, util.InternalIOTag)
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
// Content-Md5 is required and should be set
// https://docs.aws.amazon.com/AmazonS3/latest/API/API_PutBucketLifecycleConfiguration.html
if _, ok := r.Header[api.ContentMD5]; !ok {
h.logAndSendError(ctx, w, "missing Content-MD5", reqInfo, apierr.GetAPIError(apierr.ErrMissingContentMD5))
return
}
headerMD5, err := base64.StdEncoding.DecodeString(r.Header.Get(api.ContentMD5))
if err != nil {
h.logAndSendError(ctx, w, "invalid Content-MD5", reqInfo, apierr.GetAPIError(apierr.ErrInvalidDigest))
return
}
cfg := new(data.LifecycleConfiguration) cfg := new(data.LifecycleConfiguration)
if err = h.cfg.NewXMLDecoder(tee, r.UserAgent()).Decode(cfg); err != nil { if err := h.cfg.NewXMLDecoder(tee, r.UserAgent()).Decode(cfg); err != nil {
h.logAndSendError(ctx, w, "could not decode body", reqInfo, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrMalformedXML), err.Error())) h.logAndSendError(ctx, w, "could not decode body", reqInfo, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrMalformedXML), err.Error()))
return return
} }
bodyMD5, err := getContentMD5(&buf) if _, ok := r.Header[api.ContentMD5]; ok {
if err != nil { headerMD5, err := base64.StdEncoding.DecodeString(r.Header.Get(api.ContentMD5))
h.logAndSendError(ctx, w, "could not get content md5", reqInfo, err) if err != nil {
return h.logAndSendError(ctx, w, "invalid Content-MD5", reqInfo, apierr.GetAPIError(apierr.ErrInvalidDigest))
} return
}
if !bytes.Equal(headerMD5, bodyMD5) { bodyMD5, err := getContentMD5(&buf)
h.logAndSendError(ctx, w, "Content-MD5 does not match", reqInfo, apierr.GetAPIError(apierr.ErrInvalidDigest)) if err != nil {
return h.logAndSendError(ctx, w, "could not get content md5", reqInfo, err)
return
}
if !bytes.Equal(headerMD5, bodyMD5) {
h.logAndSendError(ctx, w, "Content-MD5 does not match", reqInfo, apierr.GetAPIError(apierr.ErrInvalidDigest))
return
}
} }
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -120,7 +123,10 @@ func (h *handler) PutBucketLifecycleHandler(w http.ResponseWriter, r *http.Reque
} }
func (h *handler) DeleteBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) DeleteBucketLifecycleHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.DeleteBucketLifecycle")
defer span.End()
ctx = qostagging.ContextWithIOTag(ctx, util.InternalIOTag)
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)

View file

@ -29,6 +29,8 @@ func TestPutBucketLifecycleConfiguration(t *testing.T) {
for _, tc := range []struct { for _, tc := range []struct {
name string name string
body *data.LifecycleConfiguration body *data.LifecycleConfiguration
headers map[string]string
addMD5 bool
errorCode apierr.ErrorCode errorCode apierr.ErrorCode
}{ }{
{ {
@ -70,6 +72,22 @@ func TestPutBucketLifecycleConfiguration(t *testing.T) {
}, },
}, },
}, },
{
name: "correct Content-Md5 header",
body: &data.LifecycleConfiguration{
Rules: []data.LifecycleRule{
{
ID: "rule",
Status: data.LifecycleStatusEnabled,
Expiration: &data.LifecycleExpiration{
Days: ptr(21),
},
},
},
},
addMD5: true,
},
{ {
name: "too many rules", name: "too many rules",
body: func() *data.LifecycleConfiguration { body: func() *data.LifecycleConfiguration {
@ -407,14 +425,44 @@ func TestPutBucketLifecycleConfiguration(t *testing.T) {
}, },
errorCode: apierr.ErrInvalidRequest, errorCode: apierr.ErrInvalidRequest,
}, },
{
name: "invalid Content-Md5 header",
body: &data.LifecycleConfiguration{
Rules: []data.LifecycleRule{
{
Status: data.LifecycleStatusEnabled,
Expiration: &data.LifecycleExpiration{
Days: ptr(21),
},
},
},
},
headers: map[string]string{api.ContentMD5: "invalid"},
errorCode: apierr.ErrInvalidDigest,
},
{
name: "Content-Md5 header does not match body md5 hash",
body: &data.LifecycleConfiguration{
Rules: []data.LifecycleRule{
{
Status: data.LifecycleStatusEnabled,
Expiration: &data.LifecycleExpiration{
Days: ptr(21),
},
},
},
},
headers: map[string]string{api.ContentMD5: base64.StdEncoding.EncodeToString([]byte("some-hash"))},
errorCode: apierr.ErrInvalidDigest,
},
} { } {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
if tc.errorCode > 0 { if tc.errorCode > 0 {
putBucketLifecycleConfigurationErr(hc, bktName, tc.body, apierr.GetAPIError(tc.errorCode)) putBucketLifecycleConfigurationErr(hc, bktName, tc.body, tc.headers, apierr.GetAPIError(tc.errorCode))
return return
} }
putBucketLifecycleConfiguration(hc, bktName, tc.body) putBucketLifecycleConfiguration(hc, bktName, tc.body, tc.headers, tc.addMD5)
cfg := getBucketLifecycleConfiguration(hc, bktName) cfg := getBucketLifecycleConfiguration(hc, bktName)
require.Equal(t, tc.body.Rules, cfg.Rules) require.Equal(t, tc.body.Rules, cfg.Rules)
@ -448,45 +496,13 @@ func TestPutBucketLifecycleIDGeneration(t *testing.T) {
}, },
} }
putBucketLifecycleConfiguration(hc, bktName, lifecycle) putBucketLifecycleConfiguration(hc, bktName, lifecycle, nil, false)
cfg := getBucketLifecycleConfiguration(hc, bktName) cfg := getBucketLifecycleConfiguration(hc, bktName)
require.Len(t, cfg.Rules, 2) require.Len(t, cfg.Rules, 2)
require.NotEmpty(t, cfg.Rules[0].ID) require.NotEmpty(t, cfg.Rules[0].ID)
require.NotEmpty(t, cfg.Rules[1].ID) require.NotEmpty(t, cfg.Rules[1].ID)
} }
func TestPutBucketLifecycleInvalidMD5(t *testing.T) {
hc := prepareHandlerContext(t)
bktName := "bucket-lifecycle-md5"
createBucket(hc, bktName)
lifecycle := &data.LifecycleConfiguration{
Rules: []data.LifecycleRule{
{
Status: data.LifecycleStatusEnabled,
Expiration: &data.LifecycleExpiration{
Days: ptr(21),
},
},
},
}
w, r := prepareTestRequest(hc, bktName, "", lifecycle)
hc.Handler().PutBucketLifecycleHandler(w, r)
assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrMissingContentMD5))
w, r = prepareTestRequest(hc, bktName, "", lifecycle)
r.Header.Set(api.ContentMD5, "")
hc.Handler().PutBucketLifecycleHandler(w, r)
assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrInvalidDigest))
w, r = prepareTestRequest(hc, bktName, "", lifecycle)
r.Header.Set(api.ContentMD5, "some-hash")
hc.Handler().PutBucketLifecycleHandler(w, r)
assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrInvalidDigest))
}
func TestPutBucketLifecycleInvalidXML(t *testing.T) { func TestPutBucketLifecycleInvalidXML(t *testing.T) {
hc := prepareHandlerContext(t) hc := prepareHandlerContext(t)
@ -505,25 +521,32 @@ func TestPutBucketLifecycleInvalidXML(t *testing.T) {
assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrMalformedXML)) assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrMalformedXML))
} }
func putBucketLifecycleConfiguration(hc *handlerContext, bktName string, cfg *data.LifecycleConfiguration) { func putBucketLifecycleConfiguration(hc *handlerContext, bktName string, cfg *data.LifecycleConfiguration, headers map[string]string, addMD5 bool) {
w := putBucketLifecycleConfigurationBase(hc, bktName, cfg) w := putBucketLifecycleConfigurationBase(hc, bktName, cfg, headers, addMD5)
assertStatus(hc.t, w, http.StatusOK) assertStatus(hc.t, w, http.StatusOK)
} }
func putBucketLifecycleConfigurationErr(hc *handlerContext, bktName string, cfg *data.LifecycleConfiguration, err apierr.Error) { func putBucketLifecycleConfigurationErr(hc *handlerContext, bktName string, cfg *data.LifecycleConfiguration, headers map[string]string, err apierr.Error) {
w := putBucketLifecycleConfigurationBase(hc, bktName, cfg) w := putBucketLifecycleConfigurationBase(hc, bktName, cfg, headers, false)
assertS3Error(hc.t, w, err) assertS3Error(hc.t, w, err)
} }
func putBucketLifecycleConfigurationBase(hc *handlerContext, bktName string, cfg *data.LifecycleConfiguration) *httptest.ResponseRecorder { func putBucketLifecycleConfigurationBase(hc *handlerContext, bktName string, cfg *data.LifecycleConfiguration, headers map[string]string, addMD5 bool) *httptest.ResponseRecorder {
w, r := prepareTestRequest(hc, bktName, "", cfg) w, r := prepareTestRequest(hc, bktName, "", cfg)
rawBody, err := xml.Marshal(cfg) for k, v := range headers {
require.NoError(hc.t, err) r.Header.Set(k, v)
}
if addMD5 {
rawBody, err := xml.Marshal(cfg)
require.NoError(hc.t, err)
hash := md5.New()
hash.Write(rawBody)
r.Header.Set(api.ContentMD5, base64.StdEncoding.EncodeToString(hash.Sum(nil)))
}
hash := md5.New()
hash.Write(rawBody)
r.Header.Set(api.ContentMD5, base64.StdEncoding.EncodeToString(hash.Sum(nil)))
hc.Handler().PutBucketLifecycleHandler(w, r) hc.Handler().PutBucketLifecycleHandler(w, r)
return w return w
} }

View file

@ -1,49 +0,0 @@
package handler
import (
"net/http"
"time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
)
const maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
// ListBucketsHandler handles bucket listing requests.
func (h *handler) ListBucketsHandler(w http.ResponseWriter, r *http.Request) {
var (
own user.ID
res *ListBucketsResponse
ctx = r.Context()
reqInfo = middleware.GetReqInfo(ctx)
)
list, err := h.obj.ListBuckets(ctx)
if err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
return
}
if len(list) > 0 {
own = list[0].Owner
}
res = &ListBucketsResponse{
Owner: Owner{
ID: own.String(),
DisplayName: own.String(),
},
}
for _, item := range list {
res.Buckets.Buckets = append(res.Buckets.Buckets, Bucket{
Name: item.Name,
CreationDate: item.Created.UTC().Format(time.RFC3339),
})
}
if err = middleware.EncodeToResponse(w, res); err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
}
}

View file

@ -7,6 +7,7 @@ import (
"strconv" "strconv"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -26,7 +27,9 @@ const (
) )
func (h *handler) PutBucketObjectLockConfigHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutBucketObjectLockConfigHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutBucketObjectLockConfig")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -74,7 +77,9 @@ func (h *handler) PutBucketObjectLockConfigHandler(w http.ResponseWriter, r *htt
} }
func (h *handler) GetBucketObjectLockConfigHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetBucketObjectLockConfigHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetBucketObjectLockConfig")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -108,7 +113,9 @@ func (h *handler) GetBucketObjectLockConfigHandler(w http.ResponseWriter, r *htt
} }
func (h *handler) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutObjectLegalHold")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -161,7 +168,9 @@ func (h *handler) PutObjectLegalHoldHandler(w http.ResponseWriter, r *http.Reque
} }
func (h *handler) GetObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetObjectLegalHoldHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetObjectLegalHold")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -199,7 +208,9 @@ func (h *handler) GetObjectLegalHoldHandler(w http.ResponseWriter, r *http.Reque
} }
func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutObjectRetention")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -247,7 +258,9 @@ func (h *handler) PutObjectRetentionHandler(w http.ResponseWriter, r *http.Reque
} }
func (h *handler) GetObjectRetentionHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetObjectRetentionHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetObjectRetention")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)

View file

@ -10,6 +10,7 @@ import (
"strings" "strings"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -104,7 +105,9 @@ const (
) )
func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.CreateMultipartUpload")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
uploadID := uuid.New() uploadID := uuid.New()
cannedACLStatus := aclHeadersStatus(r) cannedACLStatus := aclHeadersStatus(r)
@ -152,12 +155,6 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
p.Header[api.ContentLanguage] = contentLanguage p.Header[api.ContentLanguage] = contentLanguage
} }
p.CopiesNumbers, err = h.pickCopiesNumbers(p.Header, reqInfo.Namespace, bktInfo.LocationConstraint)
if err != nil {
h.logAndSendError(ctx, w, "invalid copies number", reqInfo, err, additional...)
return
}
if err = h.obj.CreateMultipartUpload(ctx, p); err != nil { if err = h.obj.CreateMultipartUpload(ctx, p); err != nil {
h.logAndSendError(ctx, w, "could create multipart upload", reqInfo, err, additional...) h.logAndSendError(ctx, w, "could create multipart upload", reqInfo, err, additional...)
return return
@ -180,7 +177,9 @@ func (h *handler) CreateMultipartUploadHandler(w http.ResponseWriter, r *http.Re
} }
func (h *handler) UploadPartHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) UploadPartHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.UploadPart")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -229,6 +228,12 @@ func (h *handler) UploadPartHandler(w http.ResponseWriter, r *http.Request) {
return return
} }
p.CopiesNumbers, err = h.pickCopiesNumbers(parseMetadata(r), reqInfo.Namespace, bktInfo.LocationConstraint)
if err != nil {
h.logAndSendError(ctx, w, "invalid copies number", reqInfo, err, additional...)
return
}
hash, err := h.obj.UploadPart(ctx, p) hash, err := h.obj.UploadPart(ctx, p)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "could not upload a part", reqInfo, err, additional...) h.logAndSendError(ctx, w, "could not upload a part", reqInfo, err, additional...)
@ -247,15 +252,16 @@ func (h *handler) UploadPartHandler(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) { func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) {
var ( ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.UploadPartCopy")
versionID string defer span.End()
ctx = r.Context()
reqInfo = middleware.GetReqInfo(ctx) var versionID string
queryValues = reqInfo.URL.Query()
uploadID = queryValues.Get(uploadIDHeaderName) reqInfo := middleware.GetReqInfo(ctx)
partNumStr = queryValues.Get(partNumberHeaderName) queryValues := reqInfo.URL.Query()
additional = []zap.Field{zap.String("uploadID", uploadID), zap.String("partNumber", partNumStr)} uploadID := queryValues.Get(uploadIDHeaderName)
) partNumStr := queryValues.Get(partNumberHeaderName)
additional := []zap.Field{zap.String("uploadID", uploadID), zap.String("partNumber", partNumStr)}
partNumber, err := strconv.Atoi(partNumStr) partNumber, err := strconv.Atoi(partNumStr)
if err != nil || partNumber < layer.UploadMinPartNumber || partNumber > layer.UploadMaxPartNumber { if err != nil || partNumber < layer.UploadMinPartNumber || partNumber > layer.UploadMaxPartNumber {
@ -354,6 +360,12 @@ func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) {
return return
} }
p.CopiesNumbers, err = h.pickCopiesNumbers(parseMetadata(r), reqInfo.Namespace, bktInfo.LocationConstraint)
if err != nil {
h.logAndSendError(ctx, w, "invalid copies number", reqInfo, err, additional...)
return
}
info, err := h.obj.UploadPartCopy(ctx, p) info, err := h.obj.UploadPartCopy(ctx, p)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "could not upload part copy", reqInfo, err, additional...) h.logAndSendError(ctx, w, "could not upload part copy", reqInfo, err, additional...)
@ -375,7 +387,9 @@ func (h *handler) UploadPartCopy(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) CompleteMultipartUploadHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) CompleteMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.CompleteMultipartUpload")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -416,6 +430,12 @@ func (h *handler) CompleteMultipartUploadHandler(w http.ResponseWriter, r *http.
Parts: reqBody.Parts, Parts: reqBody.Parts,
} }
c.CopiesNumbers, err = h.pickCopiesNumbers(parseMetadata(r), reqInfo.Namespace, bktInfo.LocationConstraint)
if err != nil {
h.logAndSendError(ctx, w, "invalid copies number", reqInfo, err, additional...)
return
}
// Start complete multipart upload which may take some time to fetch object // Start complete multipart upload which may take some time to fetch object
// and re-upload it part by part. // and re-upload it part by part.
objInfo, err := h.completeMultipartUpload(r, c, bktInfo) objInfo, err := h.completeMultipartUpload(r, c, bktInfo)
@ -496,7 +516,9 @@ func (h *handler) completeMultipartUpload(r *http.Request, c *layer.CompleteMult
} }
func (h *handler) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.ListMultipartUploads")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -547,7 +569,9 @@ func (h *handler) ListMultipartUploadsHandler(w http.ResponseWriter, r *http.Req
} }
func (h *handler) ListPartsHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) ListPartsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.ListParts")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -611,7 +635,9 @@ func (h *handler) ListPartsHandler(w http.ResponseWriter, r *http.Request) {
} }
func (h *handler) AbortMultipartUploadHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) AbortMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.AbortMultipartUpload")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)

View file

@ -81,6 +81,26 @@ func TestDeleteMultipartAllParts(t *testing.T) {
require.Empty(t, hc.tp.Objects()) require.Empty(t, hc.tp.Objects())
} }
func TestMultipartCopiesNumber(t *testing.T) {
hc := prepareHandlerContext(t)
bktName, objName := "bucket", "object"
createTestBucket(hc, bktName)
copies := []uint32{2, 0}
hc.config.copiesNumbers = map[string][]uint32{"default": copies}
multipartInfo := createMultipartUpload(hc, bktName, objName, nil)
uploadPart(hc, bktName, objName, multipartInfo.UploadID, 1, layer.UploadMinSize)
objs := hc.tp.Objects()
require.Len(t, objs, 1)
require.EqualValues(t, copies, hc.tp.CopiesNumbers(addrFromObject(objs[0]).EncodeToString()))
}
func TestSpecialMultipartName(t *testing.T) { func TestSpecialMultipartName(t *testing.T) {
hc := prepareHandlerContextWithMinCache(t) hc := prepareHandlerContextWithMinCache(t)
@ -792,3 +812,14 @@ func listPartsBase(hc *handlerContext, bktName, objName string, encrypted bool,
return listPartsResponse return listPartsResponse
} }
func addrFromObject(obj *object.Object) oid.Address {
var addr oid.Address
cnrID, _ := obj.ContainerID()
objID, _ := obj.ID()
addr.SetContainer(cnrID)
addr.SetObject(objID)
return addr
}

View file

@ -1,23 +1,32 @@
package handler package handler
import ( import (
"context"
"encoding/xml"
"io"
"net/http" "net/http"
"net/url" "net/url"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
"go.uber.org/zap"
) )
const maxObjectList = 1000 // Limit number of objects in a listObjectsResponse/listObjectsVersionsResponse.
// ListObjectsV1Handler handles objects listing requests for API version 1. // ListObjectsV1Handler handles objects listing requests for API version 1.
func (h *handler) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) { func (h *handler) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.ListObjectsV1")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
params, err := parseListObjectsArgsV1(reqInfo) params, err := parseListObjectsArgsV1(reqInfo)
if err != nil { if err != nil {
@ -30,14 +39,28 @@ func (h *handler) ListObjectsV1Handler(w http.ResponseWriter, r *http.Request) {
return return
} }
ch := make(chan struct{})
defer close(ch)
params.Chan = ch
stopPeriodicResponseWriter := periodicXMLWriter(w, h.cfg.ListingKeepaliveThrottle(), ch)
list, err := h.obj.ListObjectsV1(ctx, params) list, err := h.obj.ListObjectsV1(ctx, params)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err) logAndSendError := h.periodicWriterErrorSender(stopPeriodicResponseWriter())
logAndSendError(ctx, w, "could not list objects v1", reqInfo, err)
return return
} }
if err = middleware.EncodeToResponse(w, h.encodeV1(params, list)); err != nil { headerIsWritten := stopPeriodicResponseWriter()
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err) if headerIsWritten {
if err = middleware.EncodeToResponseNoHeader(w, h.encodeV1(params, list)); err != nil {
h.logAndSendErrorNoHeader(ctx, w, "could not encode listing v1 response", reqInfo, err)
}
} else {
if err = middleware.EncodeToResponse(w, h.encodeV1(params, list)); err != nil {
h.logAndSendError(ctx, w, "could not encode listing v1 response", reqInfo, err)
}
} }
} }
@ -62,7 +85,9 @@ func (h *handler) encodeV1(p *layer.ListObjectsParamsV1, list *layer.ListObjects
// ListObjectsV2Handler handles objects listing requests for API version 2. // ListObjectsV2Handler handles objects listing requests for API version 2.
func (h *handler) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) { func (h *handler) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.ListObjectsV2")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
params, err := parseListObjectsArgsV2(reqInfo) params, err := parseListObjectsArgsV2(reqInfo)
if err != nil { if err != nil {
@ -75,14 +100,28 @@ func (h *handler) ListObjectsV2Handler(w http.ResponseWriter, r *http.Request) {
return return
} }
ch := make(chan struct{})
defer close(ch)
params.Chan = ch
stopPeriodicResponseWriter := periodicXMLWriter(w, h.cfg.ListingKeepaliveThrottle(), ch)
list, err := h.obj.ListObjectsV2(ctx, params) list, err := h.obj.ListObjectsV2(ctx, params)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err) logAndSendError := h.periodicWriterErrorSender(stopPeriodicResponseWriter())
logAndSendError(ctx, w, "could not list objects v2", reqInfo, err)
return return
} }
if err = middleware.EncodeToResponse(w, h.encodeV2(params, list)); err != nil { headerIsWritten := stopPeriodicResponseWriter()
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err) if headerIsWritten {
if err = middleware.EncodeToResponseNoHeader(w, h.encodeV2(params, list)); err != nil {
h.logAndSendErrorNoHeader(ctx, w, "could not encode listing v2 response", reqInfo, err)
}
} else {
if err = middleware.EncodeToResponse(w, h.encodeV2(params, list)); err != nil {
h.logAndSendError(ctx, w, "could not encode listing v2 response", reqInfo, err)
}
} }
} }
@ -221,7 +260,9 @@ func fillContents(src []*data.ExtendedNodeVersion, encode string, fetchOwner, md
} }
func (h *handler) ListBucketObjectVersionsHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) ListBucketObjectVersionsHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.ListBucketObjectVersions")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
p, err := parseListObjectVersionsRequest(reqInfo) p, err := parseListObjectVersionsRequest(reqInfo)
if err != nil { if err != nil {
@ -234,15 +275,29 @@ func (h *handler) ListBucketObjectVersionsHandler(w http.ResponseWriter, r *http
return return
} }
info, err := h.obj.ListObjectVersions(ctx, p) ch := make(chan struct{})
defer close(ch)
p.Chan = ch
stopPeriodicResponseWriter := periodicXMLWriter(w, h.cfg.ListingKeepaliveThrottle(), ch)
list, err := h.obj.ListObjectVersions(ctx, p)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err) logAndSendError := h.periodicWriterErrorSender(stopPeriodicResponseWriter())
logAndSendError(ctx, w, "could not list objects versions", reqInfo, err)
return return
} }
response := encodeListObjectVersionsToResponse(p, info, p.BktInfo.Name, h.cfg.MD5Enabled()) response := encodeListObjectVersionsToResponse(p, list, p.BktInfo.Name, h.cfg.MD5Enabled())
if err = middleware.EncodeToResponse(w, response); err != nil { headerIsWritten := stopPeriodicResponseWriter()
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err) if headerIsWritten {
if err = middleware.EncodeToResponseNoHeader(w, response); err != nil {
h.logAndSendErrorNoHeader(ctx, w, "could not encode listing versions response", reqInfo, err)
}
} else {
if err = middleware.EncodeToResponse(w, response); err != nil {
h.logAndSendError(ctx, w, "could not encode listing versions response", reqInfo, err)
}
} }
} }
@ -325,3 +380,70 @@ func encodeListObjectVersionsToResponse(p *layer.ListObjectVersionsParams, info
return &res return &res
} }
// periodicWriterErrorSender returns handler function to send error. If header is
// already written by periodic XML writer, do not send HTTP and XML headers.
func (h *handler) periodicWriterErrorSender(headerWritten bool) func(context.Context, http.ResponseWriter, string, *middleware.ReqInfo, error, ...zap.Field) {
if headerWritten {
return h.logAndSendErrorNoHeader
}
return h.logAndSendError
}
// periodicXMLWriter creates go routine to write xml header and whitespaces
// over time to avoid connection drop from the client. To work properly,
// pass `http.ResponseWriter` with implemented `http.Flusher` interface.
// Returns stop function which returns boolean if writer has been used
// during goroutine execution. Zero or negative duration disable writing.
func periodicXMLWriter(w io.Writer, dur time.Duration, ch <-chan struct{}) (stop func() bool) {
if dur <= 0 {
return func() bool { return false }
}
whitespaceChar := []byte(" ")
closer := make(chan struct{})
done := make(chan struct{})
headerWritten := false
go func() {
defer close(done)
var lastEvent time.Time
for {
select {
case _, ok := <-ch:
if !ok {
return
}
if time.Since(lastEvent) < dur {
continue
}
lastEvent = time.Now()
if !headerWritten {
_, err := w.Write([]byte(xml.Header))
headerWritten = err == nil
}
_, err := w.Write(whitespaceChar)
if err != nil {
return // is there anything we can do better than ignore error?
}
if buffered, ok := w.(http.Flusher); ok {
buffered.Flush()
}
case <-closer:
return
}
}
}()
stop = func() bool {
close(closer)
<-done // wait for goroutine to stop
return headerWritten
}
return stop
}

View file

@ -1,7 +1,9 @@
package handler package handler
import ( import (
"bytes"
"context" "context"
"encoding/xml"
"fmt" "fmt"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
@ -103,7 +105,7 @@ func TestListObjectsVersionsSkipLogTaggingNodesError(t *testing.T) {
loggerCore, observedLog := observer.New(zap.DebugLevel) loggerCore, observedLog := observer.New(zap.DebugLevel)
log := zap.New(loggerCore) log := zap.New(loggerCore)
hcBase, err := prepareHandlerContextBase(layer.DefaultCachesConfigs(log)) hcBase, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: layer.DefaultCachesConfigs(log)}, log)
require.NoError(t, err) require.NoError(t, err)
hc := &handlerContext{ hc := &handlerContext{
handlerContextBase: hcBase, handlerContextBase: hcBase,
@ -176,7 +178,7 @@ func TestListObjectsContextCanceled(t *testing.T) {
layerCfg.SessionList.Lifetime = time.Hour layerCfg.SessionList.Lifetime = time.Hour
layerCfg.SessionList.Size = 1 layerCfg.SessionList.Size = 1
hcBase, err := prepareHandlerContextBase(layerCfg) hcBase, err := prepareHandlerContextBase(&handlerConfig{cacheCfg: layerCfg}, log)
require.NoError(t, err) require.NoError(t, err)
hc := &handlerContext{ hc := &handlerContext{
handlerContextBase: hcBase, handlerContextBase: hcBase,
@ -841,6 +843,101 @@ func TestListingsWithInvalidEncodingType(t *testing.T) {
listObjectsV1Err(hc, bktName, "invalid", apierr.GetAPIError(apierr.ErrInvalidEncodingMethod)) listObjectsV1Err(hc, bktName, "invalid", apierr.GetAPIError(apierr.ErrInvalidEncodingMethod))
} }
func TestPeriodicWriter(t *testing.T) {
const dur = 100 * time.Millisecond
const whitespaces = 8
expected := []byte(xml.Header)
for i := 0; i < whitespaces; i++ {
expected = append(expected, []byte(" ")...)
}
t.Run("writes data", func(t *testing.T) {
ch := make(chan struct{})
go func() {
defer close(ch)
for range whitespaces {
ch <- struct{}{}
time.Sleep(dur)
}
}()
buf := bytes.NewBuffer(nil)
stop := periodicXMLWriter(buf, time.Nanosecond, ch)
// N number of whitespaces + half durations to guarantee at least N writes in buffer
time.Sleep(whitespaces*dur + dur/2)
require.True(t, stop())
require.Equal(t, string(expected), buf.String())
t.Run("no additional data after stop", func(t *testing.T) {
time.Sleep(2 * dur)
require.Equal(t, string(expected), buf.String())
})
})
t.Run("does not write data", func(t *testing.T) {
buf := bytes.NewBuffer(nil)
ch := make(chan struct{})
go func() {
defer close(ch)
for range whitespaces {
time.Sleep(2 * dur)
ch <- struct{}{}
}
}()
stop := periodicXMLWriter(buf, time.Nanosecond, ch)
require.False(t, stop())
require.Empty(t, buf.Bytes())
})
t.Run("throttling works", func(t *testing.T) {
buf := bytes.NewBuffer(nil)
ch := make(chan struct{})
go func() {
defer close(ch)
for range whitespaces {
ch <- struct{}{}
time.Sleep(dur / 2)
}
}()
stop := periodicXMLWriter(buf, dur, ch)
// N number of whitespaces + half durations to guarantee at least N writes in buffer
time.Sleep(whitespaces*dur + dur/2)
require.True(t, stop())
require.Equal(t, string(expected[:len(expected)-4]), buf.String())
})
t.Run("disabled", func(t *testing.T) {
buf := bytes.NewBuffer(nil)
ch := make(chan struct{})
go func() {
defer close(ch)
for range whitespaces {
select {
case ch <- struct{}{}:
default:
}
time.Sleep(dur / 2)
}
}()
stop := periodicXMLWriter(buf, 0, ch)
// N number of whitespaces + half durations to guarantee at least N writes in buffer
time.Sleep(whitespaces*dur + dur/2)
require.False(t, stop())
require.Empty(t, buf.String())
})
}
func checkVersionsNames(t *testing.T, versions *ListObjectsVersionsResponse, names []string) { func checkVersionsNames(t *testing.T, versions *ListObjectsVersionsResponse, names []string) {
for i, v := range versions.Version { for i, v := range versions.Version {
require.Equal(t, names[i], v.Key) require.Equal(t, names[i], v.Key)

View file

@ -7,6 +7,7 @@ import (
"strings" "strings"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -19,7 +20,9 @@ import (
const maxPatchSize = 5 * 1024 * 1024 * 1024 // 5GB const maxPatchSize = 5 * 1024 * 1024 * 1024 // 5GB
func (h *handler) PatchObjectHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PatchObjectHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PatchObject")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
if _, ok := r.Header[api.ContentRange]; !ok { if _, ok := r.Header[api.ContentRange]; !ok {
@ -142,7 +145,7 @@ func parsePatchConditionalHeaders(headers http.Header, log *zap.Logger) *conditi
if httpTime, err := parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err == nil { if httpTime, err := parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err == nil {
args.IfUnmodifiedSince = httpTime args.IfUnmodifiedSince = httpTime
} else { } else {
log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfUnmodifiedSince, headers.Get(api.IfUnmodifiedSince)), zap.Error(err)) log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfUnmodifiedSince, headers.Get(api.IfUnmodifiedSince)), zap.Error(err), logs.TagField(logs.TagDatapath))
} }
return args return args

View file

@ -18,6 +18,7 @@ import (
"strings" "strings"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
@ -54,17 +55,29 @@ func (p *postPolicy) condition(key string) *policyCondition {
return nil return nil
} }
func (p *postPolicy) CheckContentLength(size uint64) bool { func (p *postPolicy) CheckContentLength(size uint64) error {
if p.empty { if p.empty {
return true return nil
} }
for _, condition := range p.Conditions { for _, condition := range p.Conditions {
if condition.Matching == "content-length-range" { if condition.Matching == "content-length-range" {
length := strconv.FormatUint(size, 10) start, err := strconv.ParseUint(condition.Key, 10, 64)
return condition.Key <= length && length <= condition.Value if err != nil {
return errInvalidCondition
}
end, err := strconv.ParseUint(condition.Value, 10, 64)
if err != nil {
return errInvalidCondition
}
if start <= size && size <= end {
return nil
}
return fmt.Errorf("length of the content did not fall within the range specified in the condition")
} }
} }
return true return nil
} }
func (p *policyCondition) match(value string) bool { func (p *policyCondition) match(value string) bool {
@ -184,12 +197,13 @@ type createBucketParams struct {
} }
func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
var ( ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutObject")
err error defer span.End()
cannedACLStatus = aclHeadersStatus(r)
ctx = r.Context() var err error
reqInfo = middleware.GetReqInfo(ctx)
) cannedACLStatus := aclHeadersStatus(r)
reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
if err != nil { if err != nil {
@ -311,10 +325,23 @@ func (h *handler) PutObjectHandler(w http.ResponseWriter, r *http.Request) {
} }
} }
func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) { type BodyReader interface {
io.ReadCloser
TrailerHeaders() map[string]string
}
type noTrailerBodyReader struct {
io.ReadCloser
}
func (r *noTrailerBodyReader) TrailerHeaders() map[string]string {
return nil
}
func (h *handler) getBodyReader(r *http.Request) (BodyReader, error) {
shaType, streaming := api.IsSignedStreamingV4(r) shaType, streaming := api.IsSignedStreamingV4(r)
if !streaming { if !streaming {
return r.Body, nil return &noTrailerBodyReader{r.Body}, nil
} }
encodings := r.Header.Values(api.ContentEncoding) encodings := r.Header.Values(api.ContentEncoding)
@ -350,12 +377,15 @@ func (h *handler) getBodyReader(r *http.Request) (io.ReadCloser, error) {
var ( var (
err error err error
chunkReader io.ReadCloser chunkReader BodyReader
) )
if shaType == api.StreamingContentV4aSHA256 { switch shaType {
chunkReader, err = newSignV4aChunkedReader(r) case api.StreamingContentSHA256, api.StreamingContentSHA256Trailer:
} else {
chunkReader, err = newSignV4ChunkedReader(r) chunkReader, err = newSignV4ChunkedReader(r)
case api.StreamingContentV4aSHA256, api.StreamingContentV4aSHA256Trailer:
chunkReader, err = newSignV4aChunkedReader(r)
default:
chunkReader, err = newUnsignedChunkedReader(r.Body)
} }
if err != nil { if err != nil {
@ -450,7 +480,7 @@ func (h *handler) isTLSCheckRequired(r *http.Request) bool {
tlsTermination, err := strconv.ParseBool(tlsTerminationStr) tlsTermination, err := strconv.ParseBool(tlsTerminationStr)
if err != nil { if err != nil {
h.reqLogger(r.Context()).Warn(logs.WarnInvalidTypeTLSTerminationHeader, zap.String("header", tlsTerminationStr), zap.Error(err)) h.reqLogger(r.Context()).Warn(logs.WarnInvalidTypeTLSTerminationHeader, zap.String("header", tlsTerminationStr), zap.Error(err), logs.TagField(logs.TagDatapath))
return true return true
} }
@ -458,16 +488,18 @@ func (h *handler) isTLSCheckRequired(r *http.Request) bool {
} }
func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) { func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
var ( ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PostObject")
tagSet map[string]string defer span.End()
ctx = r.Context()
reqInfo = middleware.GetReqInfo(ctx) var tagSet map[string]string
metadata = make(map[string]string)
) reqInfo := middleware.GetReqInfo(ctx)
metadata := make(map[string]string)
policy, err := checkPostPolicy(r, reqInfo, metadata) policy, err := checkPostPolicy(r, reqInfo, metadata)
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "failed check policy", reqInfo, err) h.logAndSendError(ctx, w, "failed check policy", reqInfo,
fmt.Errorf("%w: %v", apierr.GetAPIError(apierr.ErrInvalidArgument), err))
return return
} }
@ -517,7 +549,8 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
if reqInfo.ObjectName == "" || strings.Contains(reqInfo.ObjectName, "${filename}") { if reqInfo.ObjectName == "" || strings.Contains(reqInfo.ObjectName, "${filename}") {
_, head, err := r.FormFile("file") _, head, err := r.FormFile("file")
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "could not parse file field", reqInfo, err) h.logAndSendError(ctx, w, "could not parse file field", reqInfo,
fmt.Errorf("%w: %v", apierr.GetAPIError(apierr.ErrInvalidArgument), err))
return return
} }
filename = head.Filename filename = head.Filename
@ -526,7 +559,8 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
var head *multipart.FileHeader var head *multipart.FileHeader
contentReader, head, err = r.FormFile("file") contentReader, head, err = r.FormFile("file")
if err != nil { if err != nil {
h.logAndSendError(ctx, w, "could not parse file field", reqInfo, err) h.logAndSendError(ctx, w, "could not parse file field", reqInfo,
fmt.Errorf("%w: %v", apierr.GetAPIError(apierr.ErrInvalidArgument), err))
return return
} }
size = uint64(head.Size) size = uint64(head.Size)
@ -544,8 +578,8 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
return return
} }
if !policy.CheckContentLength(size) { if err := policy.CheckContentLength(size); err != nil {
h.logAndSendError(ctx, w, "invalid content-length", reqInfo, apierr.GetAPIError(apierr.ErrInvalidArgument)) h.logAndSendError(ctx, w, err.Error(), reqInfo, apierr.GetAPIError(apierr.ErrPostPolicyConditionInvalidFormat))
return return
} }
@ -571,6 +605,7 @@ func (h *handler) PostObject(w http.ResponseWriter, r *http.Request) {
ObjectName: objInfo.Name, ObjectName: objInfo.Name,
VersionID: objInfo.VersionID(), VersionID: objInfo.VersionID(),
}, },
TagSet: tagSet,
NodeVersion: extendedObjInfo.NodeVersion, NodeVersion: extendedObjInfo.NodeVersion,
} }
@ -748,7 +783,10 @@ func parseCannedACL(header http.Header) (string, error) {
} }
func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) CreateBucketHandler(w http.ResponseWriter, r *http.Request) {
h.createBucketHandlerPolicy(w, r) ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.CreateBucket")
defer span.End()
h.createBucketHandlerPolicy(w, r.WithContext(ctx))
} }
func (h *handler) parseCommonCreateBucketParams(reqInfo *middleware.ReqInfo, boxData *accessbox.Box, r *http.Request) (*keys.PublicKey, *layer.CreateBucketParams, error) { func (h *handler) parseCommonCreateBucketParams(reqInfo *middleware.ReqInfo, boxData *accessbox.Box, r *http.Request) (*keys.PublicKey, *layer.CreateBucketParams, error) {
@ -812,7 +850,7 @@ func (h *handler) createBucketHandlerPolicy(w http.ResponseWriter, r *http.Reque
h.logAndSendError(ctx, w, "could not create bucket", reqInfo, err) h.logAndSendError(ctx, w, "could not create bucket", reqInfo, err)
return return
} }
h.reqLogger(ctx).Info(logs.BucketIsCreated, zap.Stringer("container_id", bktInfo.CID)) h.reqLogger(ctx).Info(logs.BucketIsCreated, zap.Stringer("container_id", bktInfo.CID), logs.TagField(logs.TagExternalStorage))
chains := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID) chains := bucketCannedACLToAPERules(cannedACL, reqInfo, bktInfo.CID)
if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), chains); err != nil { if err = h.ape.SaveACLChains(bktInfo.CID.EncodeToString(), chains); err != nil {
@ -1025,7 +1063,7 @@ func isLockEnabled(log *zap.Logger, header http.Header) bool {
lockEnabled, err := strconv.ParseBool(lockEnabledStr) lockEnabled, err := strconv.ParseBool(lockEnabledStr)
if err != nil { if err != nil {
log.Warn(logs.InvalidBucketObjectLockEnabledHeader, zap.String("header", lockEnabledStr), zap.Error(err)) log.Warn(logs.InvalidBucketObjectLockEnabledHeader, zap.String("header", lockEnabledStr), zap.Error(err), logs.TagField(logs.TagDatapath))
} }
return lockEnabled return lockEnabled

View file

@ -9,7 +9,10 @@ import (
"encoding/base64" "encoding/base64"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"encoding/xml"
"errors" "errors"
"fmt"
"hash/crc32"
"io" "io"
"mime/multipart" "mime/multipart"
"net/http" "net/http"
@ -146,8 +149,31 @@ func TestPostObject(t *testing.T) {
filename string filename string
content string content string
objName string objName string
tagging string
err bool err bool
}{ }{
{
key: "user/user1/${filename}",
filename: "object",
content: "content",
objName: "user/user1/object",
tagging: "<Tagging xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><TagSet><Tag><Key>Environment</Key><Value>Production</Value></Tag></TagSet></Tagging>",
},
{
key: "user/user1/${filename}",
filename: "object",
content: "content",
objName: "user/user1/object",
tagging: "wrong tagging",
err: true,
},
{
key: "user/user1/${filename}",
filename: "object",
content: "content",
objName: "user/user1/object",
tagging: "<Tagging xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\"><TagSet></TagSet></Tagging>",
},
{ {
key: "user/user1/${filename}", key: "user/user1/${filename}",
filename: "object", filename: "object",
@ -205,14 +231,21 @@ func TestPostObject(t *testing.T) {
}, },
} { } {
t.Run(tc.key+";"+tc.filename, func(t *testing.T) { t.Run(tc.key+";"+tc.filename, func(t *testing.T) {
w := postObjectBase(hc, ns, bktName, tc.key, tc.filename, tc.content) w := postObjectBase(hc, ns, bktName, tc.key, tc.filename, tc.content, tc.tagging)
if tc.err { if tc.err {
assertS3Error(hc.t, w, apierr.GetAPIError(apierr.ErrInternalError)) assertStatus(hc.t, w, http.StatusBadRequest)
return return
} }
assertStatus(hc.t, w, http.StatusNoContent) assertStatus(hc.t, w, http.StatusNoContent)
content, _ := getObject(hc, bktName, tc.objName) content, _ := getObject(hc, bktName, tc.objName)
require.Equal(t, tc.content, string(content)) require.Equal(t, tc.content, string(content))
if tc.tagging != "" {
tagging := getObjectTagging(t, hc, bktName, tc.objName, "")
strtags, err := xml.Marshal(tagging)
require.NoError(t, err)
require.Equal(t, tc.tagging, string(strtags))
}
}) })
} }
} }
@ -377,13 +410,34 @@ func TestPutObjectCheckContentSHA256(t *testing.T) {
} }
} }
func TestPutObjectWithStreamBodyAWSExample(t *testing.T) { func TestPutObjectWithStreamUnsignedBodySmall(t *testing.T) {
hc := prepareHandlerContext(t)
bktName, objName := "test2", "tmp.txt"
createTestBucket(hc, bktName)
w, req, chunk := getChunkedRequestUnsignedTrailingSmall(hc.context, t, bktName, objName)
hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
w, req = prepareTestRequest(hc, bktName, objName, nil)
hc.Handler().HeadObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
require.Equal(t, "5", w.Header().Get(api.ContentLength))
data := getObjectRange(t, hc, bktName, objName, 0, 5)
for i := range chunk {
require.Equal(t, chunk[i], data[i])
}
}
func TestPutObjectWithStreamUnsignedBody(t *testing.T) {
hc := prepareHandlerContext(t) hc := prepareHandlerContext(t)
bktName, objName := "examplebucket", "chunkObject.txt" bktName, objName := "examplebucket", "chunkObject.txt"
createTestBucket(hc, bktName) createTestBucket(hc, bktName)
w, req, chunk := getChunkedRequest(hc.context, t, bktName, objName) w, req, chunk := getChunkedRequestUnsignedTrailing(hc.context, t, bktName, objName)
hc.Handler().PutObjectHandler(w, req) hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK) assertStatus(t, w, http.StatusOK)
@ -398,13 +452,67 @@ func TestPutObjectWithStreamBodyAWSExample(t *testing.T) {
} }
} }
func TestPutObjectWithStreamEmptyBodyAWSExample(t *testing.T) { func TestPutObjectWithStreamBodyAWSExampleTrailing(t *testing.T) {
hc := prepareHandlerContext(t)
bktName, objName := "examplebucket", "chunkObject.txt"
createTestBucket(hc, bktName)
t.Run("valid trailer signature", func(t *testing.T) {
w, req, chunk := getChunkedRequestAWSExampleTrailing(t, bktName, objName)
hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
w, req = prepareTestRequest(hc, bktName, objName, nil)
hc.Handler().HeadObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
require.Equal(t, strconv.Itoa(awsChunkedRequestExampleDecodedContentLength), w.Header().Get(api.ContentLength))
data := getObjectRange(t, hc, bktName, objName, 0, awsChunkedRequestExampleDecodedContentLength)
equalDataSlices(t, chunk, data)
})
t.Run("invalid trailer signature", func(t *testing.T) {
w, req, _ := getChunkedRequestAWSExampleTrailing(t, bktName, objName)
body := req.Body.(*customNopCloser)
body.Bytes()[body.Len()-2] = 'a'
hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusForbidden)
})
}
func TestPutObjectWithStreamBodyAWSExample(t *testing.T) {
hc := prepareHandlerContext(t)
bktName, objName := "examplebucket", "chunkObject.txt"
createTestBucket(hc, bktName)
w, req, chunk := getChunkedRequestAWSExample(t, bktName, objName)
hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
w, req = prepareTestRequest(hc, bktName, objName, nil)
hc.Handler().HeadObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
require.Equal(t, strconv.Itoa(awsChunkedRequestExampleDecodedContentLength), w.Header().Get(api.ContentLength))
data := getObjectRange(t, hc, bktName, objName, 0, awsChunkedRequestExampleDecodedContentLength)
for i := range chunk {
require.Equal(t, chunk[i], data[i])
}
}
func TestPutObjectWithStreamEmptyBodyAWSExampleWithContentType(t *testing.T) {
hc := prepareHandlerContext(t) hc := prepareHandlerContext(t)
bktName, objName := "dkirillov", "tmp" bktName, objName := "dkirillov", "tmp"
createTestBucket(hc, bktName) createTestBucket(hc, bktName)
w, req := getEmptyChunkedRequest(hc.context, t, bktName, objName) signTime, err := time.Parse("20060102T150405Z", "20241003T100055Z")
require.NoError(t, err)
extra := [2]string{api.ContentType, "text/plain; charset=UTF-8"}
w, req := getChunkedRequestBase(t, bktName, objName, nil, api.StreamingContentSHA256, signTime, extra)
hc.Handler().PutObjectHandler(w, req) hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK) assertStatus(t, w, http.StatusOK)
@ -418,13 +526,94 @@ func TestPutObjectWithStreamEmptyBodyAWSExample(t *testing.T) {
require.Empty(t, res.Contents[0].Size) require.Empty(t, res.Contents[0].Size)
} }
func TestPutObjectWithStreamEmptyBody(t *testing.T) {
hc := prepareHandlerContext(t)
bktName := "bucket"
createTestBucket(hc, bktName)
signTime, err := time.Parse("20060102T150405Z", "20241003T100055Z")
require.NoError(t, err)
t.Run("unsigned", func(t *testing.T) {
t.Run("trailer", func(t *testing.T) {
objName := "unsigned trailer"
w, req := getEmptyChunkedRequestUnsigned(t, bktName, objName)
req.Header.Del(api.ContentType)
hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
d, h := getObject(hc, bktName, objName)
require.Empty(t, d)
require.Equal(t, "0", h.Get(api.ContentLength))
})
})
t.Run("sigv4", func(t *testing.T) {
t.Run("trailer", func(t *testing.T) {
objName := "sigv4 trailer"
w, req := getChunkedRequestBase(t, bktName, objName, nil, api.StreamingContentSHA256Trailer, signTime)
req.Header.Del(api.ContentType)
hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
d, h := getObject(hc, bktName, objName)
require.Empty(t, d)
require.Equal(t, "0", h.Get(api.ContentLength))
})
t.Run("no trailer", func(t *testing.T) {
objName := "sigv4 no trailer"
w, req := getChunkedRequestBase(t, bktName, objName, nil, api.StreamingContentSHA256, signTime)
req.Header.Del(api.ContentType)
hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
d, h := getObject(hc, bktName, objName)
require.Empty(t, d)
require.Equal(t, "0", h.Get(api.ContentLength))
})
})
t.Run("sigv4a", func(t *testing.T) {
t.Run("trailer", func(t *testing.T) {
objName := "sigv4a trailer"
w, req := getEmptyChunkedRequestSigv4aWithTrailers(t, bktName, objName)
req.Header.Del(api.ContentType)
hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
d, h := getObject(hc, bktName, objName)
require.Empty(t, d)
require.Equal(t, "0", h.Get(api.ContentLength))
})
t.Run("no trailer", func(t *testing.T) {
objName := "sigv4a no trailer"
w, req := getEmptyChunkedRequestSigv4a(t, bktName, objName)
req.Header.Del(api.ContentType)
hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK)
d, h := getObject(hc, bktName, objName)
require.Empty(t, d)
require.Equal(t, "0", h.Get(api.ContentLength))
})
})
}
func TestPutChunkedTestContentEncoding(t *testing.T) { func TestPutChunkedTestContentEncoding(t *testing.T) {
hc := prepareHandlerContext(t) hc := prepareHandlerContext(t)
bktName, objName := "examplebucket", "chunkObject.txt" bktName, objName := "examplebucket", "chunkObject.txt"
createTestBucket(hc, bktName) createTestBucket(hc, bktName)
w, req, _ := getChunkedRequest(hc.context, t, bktName, objName) w, req, _ := getChunkedRequestAWSExample(t, bktName, objName)
req.Header.Set(api.ContentEncoding, api.AwsChunked+",gzip") req.Header.Set(api.ContentEncoding, api.AwsChunked+",gzip")
hc.Handler().PutObjectHandler(w, req) hc.Handler().PutObjectHandler(w, req)
@ -433,13 +622,13 @@ func TestPutChunkedTestContentEncoding(t *testing.T) {
resp := headObjectBase(hc, bktName, objName, emptyVersion) resp := headObjectBase(hc, bktName, objName, emptyVersion)
require.Equal(t, "gzip", resp.Header().Get(api.ContentEncoding)) require.Equal(t, "gzip", resp.Header().Get(api.ContentEncoding))
w, req, _ = getChunkedRequest(hc.context, t, bktName, objName) w, req, _ = getChunkedRequestAWSExample(t, bktName, objName)
req.Header.Set(api.ContentEncoding, "gzip") req.Header.Set(api.ContentEncoding, "gzip")
hc.Handler().PutObjectHandler(w, req) hc.Handler().PutObjectHandler(w, req)
assertS3Error(t, w, apierr.GetAPIError(apierr.ErrInvalidEncodingMethod)) assertS3Error(t, w, apierr.GetAPIError(apierr.ErrInvalidEncodingMethod))
hc.config.bypassContentEncodingInChunks = true hc.config.bypassContentEncodingInChunks = true
w, req, _ = getChunkedRequest(hc.context, t, bktName, objName) w, req, _ = getChunkedRequestAWSExample(t, bktName, objName)
req.Header.Set(api.ContentEncoding, "gzip") req.Header.Set(api.ContentEncoding, "gzip")
hc.Handler().PutObjectHandler(w, req) hc.Handler().PutObjectHandler(w, req)
assertStatus(t, w, http.StatusOK) assertStatus(t, w, http.StatusOK)
@ -448,9 +637,9 @@ func TestPutChunkedTestContentEncoding(t *testing.T) {
require.Equal(t, "gzip", resp.Header().Get(api.ContentEncoding)) require.Equal(t, "gzip", resp.Header().Get(api.ContentEncoding))
} }
// getChunkedRequest implements request example from // getChunkedRequestAWSExample implements request example from
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html // https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming.html
func getChunkedRequest(ctx context.Context, t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request, []byte) { func getChunkedRequestAWSExample(t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request, []byte) {
chunk := make([]byte, 65*1024) chunk := make([]byte, 65*1024)
for i := range chunk { for i := range chunk {
chunk[i] = 'a' chunk[i] = 'a'
@ -458,12 +647,8 @@ func getChunkedRequest(ctx context.Context, t *testing.T, bktName, objName strin
chunk1 := chunk[:64*1024] chunk1 := chunk[:64*1024]
chunk2 := chunk[64*1024:] chunk2 := chunk[64*1024:]
AWSAccessKeyID := "AKIAIOSFODNN7EXAMPLE"
AWSSecretAccessKey := "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" AWSSecretAccessKey := "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
awsCreds := aws.Credentials{AccessKeyID: AWSAccessKeyID, SecretAccessKey: AWSSecretAccessKey}
signer := v4.NewSigner()
reqBody := bytes.NewBufferString("10000;chunk-signature=ad80c730a21e5b8d04586a2213dd63b9a0e99e0e2307b0ade35a65485a288648\r\n") reqBody := bytes.NewBufferString("10000;chunk-signature=ad80c730a21e5b8d04586a2213dd63b9a0e99e0e2307b0ade35a65485a288648\r\n")
_, err := reqBody.Write(chunk1) _, err := reqBody.Write(chunk1)
require.NoError(t, err) require.NoError(t, err)
@ -476,79 +661,334 @@ func getChunkedRequest(ctx context.Context, t *testing.T, bktName, objName strin
req, err := http.NewRequest("PUT", "https://s3.amazonaws.com/"+bktName+"/"+objName, nil) req, err := http.NewRequest("PUT", "https://s3.amazonaws.com/"+bktName+"/"+objName, nil)
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("content-encoding", "aws-chunked") req.Header.Set("content-encoding", api.AwsChunked)
req.Header.Set("content-length", strconv.Itoa(awsChunkedRequestExampleContentLength)) req.Header.Set("content-length", strconv.Itoa(awsChunkedRequestExampleContentLength))
req.Header.Set("x-amz-content-sha256", "STREAMING-AWS4-HMAC-SHA256-PAYLOAD") req.Header.Set("x-amz-content-sha256", api.StreamingContentSHA256)
req.Header.Set("x-amz-decoded-content-length", strconv.Itoa(awsChunkedRequestExampleDecodedContentLength)) req.Header.Set("x-amz-decoded-content-length", strconv.Itoa(awsChunkedRequestExampleDecodedContentLength))
req.Header.Set("x-amz-storage-class", "REDUCED_REDUNDANCY") req.Header.Set("x-amz-storage-class", "REDUCED_REDUNDANCY")
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=content-encoding;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-storage-class,Signature=4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9")
signTime, err := time.Parse("20060102T150405Z", "20130524T000000Z") signTime, err := time.Parse("20060102T150405Z", "20130524T000000Z")
require.NoError(t, err) require.NoError(t, err)
err = signer.SignHTTP(ctx, awsCreds, req, auth.UnsignedPayload, "s3", "us-east-1", signTime) req.Body = io.NopCloser(reqBody)
w, req := prepareReqMiddlewares(req, signTime, AWSSecretAccessKey)
return w, req, chunk
}
type customNopCloser struct {
*bytes.Buffer
}
func (c *customNopCloser) Close() error {
return nil
}
// getChunkedRequestAWSExampleTrailing implements request example from
// https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-streaming-trailers.html
func getChunkedRequestAWSExampleTrailing(t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request, []byte) {
chunk := make([]byte, 65*1024)
for i := range chunk {
chunk[i] = 'a'
}
chunk1 := chunk[:64*1024]
chunk2 := chunk[64*1024:]
AWSSecretAccessKey := "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
reqBody := bytes.NewBufferString("10000;chunk-signature=b474d8862b1487a5145d686f57f013e54db672cee1c953b3010fb58501ef5aa2\r\n")
_, err := reqBody.Write(chunk1)
require.NoError(t, err)
_, err = reqBody.WriteString("\r\n400;chunk-signature=1c1344b170168f8e65b41376b44b20fe354e373826ccbbe2c1d40a8cae51e5c7\r\n")
require.NoError(t, err)
_, err = reqBody.Write(chunk2)
require.NoError(t, err)
_, err = reqBody.WriteString("\r\n0;chunk-signature=2ca2aba2005185cf7159c6277faf83795951dd77a3a99e6e65d5c9f85863f992\r\n")
require.NoError(t, err)
_, err = reqBody.WriteString("x-amz-checksum-crc32c:sOO8/Q==\n")
require.NoError(t, err)
// original signature is 63bddb248ad2590c92712055f51b8e78ab024eead08276b24f010b0efd74843f,
// but we use d81f82fc3505edab99d459891051a732e8730629a2e4a59689829ca17fe2e435
// because original signature is incorrect
// it was calculated using the`AWS4-HMAC-SHA256-PAYLOAD` constant in canonical string instead of
// `AWS4-HMAC-SHA256-TRAILER` that actually must be used by spec
// (java sdk use correct `AWS4-HMAC-SHA256-TRAILER` string).
_, err = reqBody.WriteString("x-amz-trailer-signature:d81f82fc3505edab99d459891051a732e8730629a2e4a59689829ca17fe2e435")
require.NoError(t, err)
req, err := http.NewRequest("PUT", "https://s3.amazonaws.com/"+bktName+"/"+objName, nil)
require.NoError(t, err)
req.Header.Set("content-encoding", api.AwsChunked)
req.Header.Set("content-length", strconv.Itoa(awsChunkedRequestExampleContentLength))
req.Header.Set("x-amz-content-sha256", api.StreamingContentSHA256Trailer)
req.Header.Set("x-amz-decoded-content-length", strconv.Itoa(awsChunkedRequestExampleDecodedContentLength))
req.Header.Set("x-amz-storage-class", "REDUCED_REDUNDANCY")
req.Header.Set("x-amz-trailer", "x-amz-checksum-crc32c")
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20130524/us-east-1/s3/aws4_request,SignedHeaders=content-encoding;content-length;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-storage-class,Signature=106e2a8a18243abcf37539882f36619c00e2dfc72633413f02d3b74544bfeb8e")
signTime, err := time.Parse("20060102T150405Z", "20130524T000000Z")
require.NoError(t, err)
req.Body = &customNopCloser{Buffer: reqBody}
w, req := prepareReqMiddlewares(req, signTime, AWSSecretAccessKey)
return w, req, chunk
}
func getChunkedRequestUnsignedTrailing(ctx context.Context, t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request, []byte) {
chunk := make([]byte, 65*1024)
for i := range chunk {
chunk[i] = 'a'
}
AWSAccessKeyID := "9uEm8zMrGWsEDWiPCnVuQLKTiGtCEXpYXt8eBG7agupw0JDySJZMFuej7PTcPzRqBUyPtFowNu1RtvHULU8XHjie6"
AWSSecretAccessKey := "9f546428957ed7e189b7be928906ce7d1d9cb3042dd4d2d5194e28ce8c4c3b8e"
awsCreds := aws.Credentials{AccessKeyID: AWSAccessKeyID, SecretAccessKey: AWSSecretAccessKey}
signer := v4.NewSigner()
reqBody := bytes.NewBufferString("10400\r\n")
_, err := reqBody.Write(chunk)
require.NoError(t, err)
_, err = reqBody.WriteString("\r\n0\r\n")
require.NoError(t, err)
_, err = reqBody.WriteString("\r\nx-amz-checksum-crc64nvme:pRf+emrnL+A=\r\n\r\n")
require.NoError(t, err)
req, err := http.NewRequest("PUT", "https://localhost:8184/"+bktName+"/"+objName, nil)
require.NoError(t, err)
req.Header.Set("x-amz-sdk-checksum-algorithm", "CRC64NVME")
req.Header.Set("content-encoding", api.AwsChunked)
req.Header.Set("x-amz-trailer", "x-amz-checksum-crc64nvme")
req.Header.Set("x-amz-content-sha256", api.StreamingUnsignedPayloadTrailer)
req.Header.Set("x-amz-decoded-content-length", strconv.Itoa(awsChunkedRequestExampleDecodedContentLength))
req.Header.Set("x-amz-storage-class", "REDUCED_REDUNDANCY")
signTime, err := time.Parse("20060102T150405Z", "20250131T140527Z")
require.NoError(t, err)
err = signer.SignHTTP(ctx, awsCreds, req, api.StreamingContentSHA256Trailer, "s3", "ru", signTime)
require.NoError(t, err) require.NoError(t, err)
req.Body = io.NopCloser(reqBody) req.Body = io.NopCloser(reqBody)
w := httptest.NewRecorder() w, req := prepareReqMiddlewares(req, signTime, AWSSecretAccessKey)
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,
AuthHeaders: &middleware.AuthHeader{
AccessKeyID: AWSAccessKeyID,
SignatureV4: "4f232c4386841ef735655705268965c44a0e4690baa4adea153f7db9fa80a0a9",
Region: "us-east-1",
},
AccessBox: &accessbox.Box{
Gate: &accessbox.GateData{
SecretKey: AWSSecretAccessKey,
},
},
}))
return w, req, chunk return w, req, chunk
} }
func getEmptyChunkedRequest(ctx context.Context, t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request) { func getChunkedRequestUnsignedTrailingSmall(ctx context.Context, t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request, []byte) {
AWSAccessKeyID := "48c1K4PLVb7SvmV3PjDKEuXaMh8yZMXZ8Wx9msrkKcYw06dZeaxeiPe8vyFm2WsoeVaNt7UWEjNsVkagDs8oX4XXh" AWSAccessKeyID := "9uEm8zMrGWsEDWiPCnVuQLKTiGtCEXpYXt8eBG7agupw0JDySJZMFuej7PTcPzRqBUyPtFowNu1RtvHULU8XHjie6"
AWSSecretAccessKey := "09260955b4eb0279dc017ba20a1ddac909cbd226c86cbb2d868e55534c8e64b0" AWSSecretAccessKey := "9f546428957ed7e189b7be928906ce7d1d9cb3042dd4d2d5194e28ce8c4c3b8e"
reqBody := bytes.NewBufferString("0;chunk-signature=311a7142c8f3a07972c3aca65c36484b513a8fee48ab7178c7225388f2ae9894\r\n\r\n") awsCreds := aws.Credentials{AccessKeyID: AWSAccessKeyID, SecretAccessKey: AWSSecretAccessKey}
signer := v4.NewSigner()
chunk := "tmp2\n"
reqBody := bytes.NewBufferString("5\r\n")
_, err := reqBody.WriteString(chunk)
require.NoError(t, err)
_, err = reqBody.WriteString("\r\n0\r\n")
require.NoError(t, err)
_, err = reqBody.WriteString("x-amz-checksum-crc64nvme:q1EYl4rI0TU=\r\n\r\n")
require.NoError(t, err)
req, err := http.NewRequest("PUT", "https://localhost:8184/"+bktName+"/"+objName, nil)
require.NoError(t, err)
req.Header.Set("x-amz-sdk-checksum-algorithm", "CRC64NVME")
req.Header.Set("content-encoding", api.AwsChunked)
req.Header.Set("x-amz-trailer", "x-amz-checksum-crc64nvme")
req.Header.Set("x-amz-content-sha256", api.StreamingUnsignedPayloadTrailer)
req.Header.Set("x-amz-decoded-content-length", "5")
req.Header.Set("x-amz-storage-class", "REDUCED_REDUNDANCY")
signTime, err := time.Parse("20060102T150405Z", "20250203T063745Z")
require.NoError(t, err)
err = signer.SignHTTP(ctx, awsCreds, req, api.StreamingContentSHA256Trailer, "s3", "ru", signTime)
require.NoError(t, err)
req.Body = io.NopCloser(reqBody)
w, req := prepareReqMiddlewares(req, signTime, AWSSecretAccessKey)
return w, req, []byte(chunk)
}
func getChunkedRequestBase(t *testing.T, bktName, objName string, chunks [][]byte, shaType string, signTime time.Time, extraHeaders ...[2]string) (*httptest.ResponseRecorder, *http.Request) {
creds := aws.Credentials{
AccessKeyID: "48c1K4PLVb7SvmV3PjDKEuXaMh8yZMXZ8Wx9msrkKcYw06dZeaxeiPe8vyFm2WsoeVaNt7UWEjNsVkagDs8oX4XXh",
SecretAccessKey: "09260955b4eb0279dc017ba20a1ddac909cbd226c86cbb2d868e55534c8e64b0",
}
region := "us-east-1"
service := "s3"
req, err := http.NewRequest("PUT", "http://localhost:8084/"+bktName+"/"+objName, nil)
require.NoError(t, err)
payloadLength := 0
for _, chunk := range chunks {
payloadLength += len(chunk)
}
for _, kv := range extraHeaders {
req.Header.Set(kv[0], kv[1])
}
req.Header.Set(api.ContentEncoding, api.AwsChunked)
req.Header.Set(api.AmzDecodedContentLength, strconv.Itoa(payloadLength))
req.Header.Set(api.AmzDate, signTime.Format("20060102T150405Z"))
req.Header.Set(api.AmzContentSha256, shaType)
if shaType == api.StreamingContentSHA256Trailer {
req.Header.Set(api.AmzTrailer, "x-amz-checksum-crc32")
}
signer := v4.NewSigner()
err = signer.SignHTTP(req.Context(), creds, req, shaType, service, region, signTime)
require.NoError(t, err)
seedSignature := strings.Split(req.Header.Get(api.Authorization), "Signature=")[1]
seed, err := hex.DecodeString(seedSignature)
require.NoError(t, err)
var reqBody bytes.Buffer
hash := crc32.NewIEEE()
newStreamSigner := v4.NewStreamSigner(creds, service, region, seed)
for _, chunk := range chunks {
_, err = hash.Write(chunk)
require.NoError(t, err)
signature, err := newStreamSigner.GetSignature(req.Context(), nil, chunk, signTime)
require.NoError(t, err)
reqBody.WriteString(fmt.Sprintf("%x;chunk-signature=%x\r\n", len(chunk), signature))
reqBody.Write(chunk)
reqBody.WriteString("\r\n")
}
signature, err := newStreamSigner.GetSignature(req.Context(), nil, nil, signTime)
require.NoError(t, err)
reqBody.WriteString(fmt.Sprintf("0;chunk-signature=%x\r\n", signature))
if shaType == api.StreamingContentSHA256Trailer {
crc32Res := hash.Sum(nil)
checksumStr := "x-amz-checksum-crc32:" + base64.StdEncoding.EncodeToString(crc32Res)
reqBody.WriteString(fmt.Sprintf("%s\r\n", checksumStr))
trailerSignature, err := newStreamSigner.GetTrailerSignature([]byte(checksumStr+"\n"), signTime)
require.NoError(t, err)
reqBody.WriteString(fmt.Sprintf("x-amz-trailer-signature:%x\r\n", trailerSignature))
}
reqBody.WriteString("\r\n")
req.Body = io.NopCloser(&reqBody)
return prepareReqMiddlewares(req, signTime, creds.SecretAccessKey)
}
func prepareReqMiddlewares(req *http.Request, signTime time.Time, secretAccessKey string) (*httptest.ResponseRecorder, *http.Request) {
authHeader := req.Header.Get(api.Authorization)
var parsed map[string]string
var region string
if strings.HasPrefix(authHeader, auth.SignaturePreambleSigV4) {
parsed = auth.NewRegexpMatcher(auth.AuthorizationFieldRegexp).GetSubmatches(authHeader)
region = parsed["region"]
} else {
parsed = auth.NewRegexpMatcher(auth.AuthorizationFieldV4aRegexp).GetSubmatches(authHeader)
region = req.Header.Get("X-Amz-Region-Set")
}
bktObj := strings.Split(req.URL.Path, "/")
box := &middleware.Box{
ClientTime: signTime,
AuthHeaders: &middleware.AuthHeader{
AccessKeyID: parsed["access_key_id"],
SignatureV4: parsed["v4_signature"],
Region: region,
},
AccessBox: &accessbox.Box{Gate: &accessbox.GateData{SecretKey: secretAccessKey}},
}
w := httptest.NewRecorder()
reqInfo := middleware.NewReqInfo(w, req, middleware.ObjectRequest{Bucket: bktObj[1], Object: bktObj[2]}, "")
req = req.WithContext(middleware.SetReqInfo(req.Context(), reqInfo))
req = req.WithContext(middleware.SetBox(req.Context(), box))
return w, req
}
func getEmptyChunkedRequestUnsigned(t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request) {
AWSSecretAccessKey := "f1a0d650b650149f1a83140418e88a3c5572a0103e912e326492a91c19c4488a"
reqBody := bytes.NewBufferString("0\r\nx-amz-checksum-crc64nvme:AAAAAAAAAAA=\r\n\r\n")
req, err := http.NewRequest("PUT", "http://localhost:8084/"+bktName+"/"+objName, reqBody) req, err := http.NewRequest("PUT", "http://localhost:8084/"+bktName+"/"+objName, reqBody)
require.NoError(t, err) require.NoError(t, err)
req.Header.Set("Amz-Sdk-Invocation-Id", "8a8cd4be-aef8-8034-f08d-a6144ade41f9") req.Header.Set(api.Authorization, "AWS4-HMAC-SHA256 Credential=3jNrmDtHtuj1uLcixaSMA4KNUhNYhv1EpUNdFnbTXgUP071pGdSZfHSLtoC8gzjF5HoD6sC3Scq33t1WvvEvjmPnt/20250213/ru/s3/aws4_request, SignedHeaders=content-encoding;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-sdk-checksum-algorithm;x-amz-trailer, Signature=1231b012c0ac313770c5a95ccf77b95b6c9b1c3760d6aa24cb8309801d56eb4a")
req.Header.Set("Amz-Sdk-Request", "attempt=1; max=2") req.Header.Set(api.ContentEncoding, api.AwsChunked)
req.Header.Set(api.Authorization, "AWS4-HMAC-SHA256 Credential=48c1K4PLVb7SvmV3PjDKEuXaMh8yZMXZ8Wx9msrkKcYw06dZeaxeiPe8vyFm2WsoeVaNt7UWEjNsVkagDs8oX4XXh/20241003/us-east-1/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-encoding;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length, Signature=4b530ab4af2381f214941af591266b209968264a2c94337fa1efc048c7dff352") req.Header.Set(api.AmzDate, "20250213T124858Z")
req.Header.Set(api.ContentEncoding, "aws-chunked") req.Header.Set(api.AmzContentSha256, api.StreamingUnsignedPayloadTrailer)
req.Header.Set(api.ContentLength, "86")
req.Header.Set(api.ContentType, "text/plain; charset=UTF-8")
req.Header.Set(api.AmzDate, "20241003T100055Z")
req.Header.Set(api.AmzContentSha256, "STREAMING-AWS4-HMAC-SHA256-PAYLOAD")
req.Header.Set(api.AmzDecodedContentLength, "0") req.Header.Set(api.AmzDecodedContentLength, "0")
req.Header.Set("X-Amz-Trailer", "x-amz-checksum-crc64nvme")
req.Header.Set("X-Amz-Sdk-Checksum-Algorithm", "CRC64NVME")
signTime, err := time.Parse("20060102T150405Z", "20241003T100055Z") signTime, err := time.Parse("20060102T150405Z", req.Header.Get(api.AmzDate))
require.NoError(t, err) require.NoError(t, err)
w := httptest.NewRecorder() return prepareReqMiddlewares(req, signTime, AWSSecretAccessKey)
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,
AuthHeaders: &middleware.AuthHeader{
AccessKeyID: AWSAccessKeyID,
SignatureV4: "4b530ab4af2381f214941af591266b209968264a2c94337fa1efc048c7dff352",
Region: "us-east-1",
},
AccessBox: &accessbox.Box{
Gate: &accessbox.GateData{
SecretKey: AWSSecretAccessKey,
},
},
}))
return w, req func getEmptyChunkedRequestSigv4aWithTrailers(t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request) {
AWSSecretAccessKey := "f1a0d650b650149f1a83140418e88a3c5572a0103e912e326492a91c19c4488a"
body := "0;chunk-signature=3046022100ab9229a80d70f4d004768992881821a441a4ad4102e18de567e68216659bf497022100ec47a7a445351683557eedf893e6ed250c97af4b0415814671770b83766d69be\r\n" +
"x-amz-checksum-crc32:AAAAAA==\r\n" +
"x-amz-trailer-signature:3046022100a0a66c1adcee8d99460b4631b23c95fbad9eb4e6c56f1afb9e255715ba141169022100b2cfc8adc8036eb985f1ab0e770b575284c5fc8ca75c226558d3142cbaab83ce\r\n\r\n"
req, err := http.NewRequest("PUT", "http://localhost:8084/"+bktName+"/"+objName, bytes.NewBufferString(body))
require.NoError(t, err)
req.Header.Set(api.Authorization, "AWS4-ECDSA-P256-SHA256 Credential=3jNrmDtHtuj1uLcixaSMA4KNUhNYhv1EpUNdFnbTXgUP071pGdSZfHSLtoC8gzjF5HoD6sC3Scq33t1WvvEvjmPnt/20250213/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-region-set;x-amz-sdk-checksum-algorithm;x-amz-trailer, Signature=304402202e1f1efcc56c588d9a94a3d8f20368686df8bfd5e8aad01fc4eff569ff38f1800220215198e3f1ba785492fe6703c4722872909ce8a09e8c9a13da90a9230c7a24b7")
req.Header.Set("Amz-Sdk-Invocation-Id", "d42dc16d-7899-55fb-5b72-a654bd482f4f")
req.Header.Set("Amz-Sdk-Request", "attempt=1; max=2")
req.Header.Set(api.ContentEncoding, api.AwsChunked)
req.Header.Set(api.AmzDate, "20250213T132401Z")
req.Header.Set(api.AmzContentSha256, api.StreamingContentV4aSHA256Trailer)
req.Header.Set(api.AmzDecodedContentLength, "0")
req.Header.Set(api.ContentLength, "367")
req.Header.Set(api.ContentType, "text/plain: charset=UTF-8")
req.Header.Set("X-Amz-Region-Set", "us-east-1")
req.Header.Set("X-Amz-Trailer", "x-amz-checksum-crc32")
req.Header.Set("X-Amz-Sdk-Checksum-Algorithm", "CRC32")
signTime, err := time.Parse("20060102T150405Z", req.Header.Get(api.AmzDate))
require.NoError(t, err)
return prepareReqMiddlewares(req, signTime, AWSSecretAccessKey)
}
func getEmptyChunkedRequestSigv4a(t *testing.T, bktName, objName string) (*httptest.ResponseRecorder, *http.Request) {
AWSSecretAccessKey := "f1a0d650b650149f1a83140418e88a3c5572a0103e912e326492a91c19c4488a"
body := "0;chunk-signature=304502203f7c598a2e9a6673bf1ca30f5f6bebd0d76a4e9d3c16531448e96c2cda22d16a0221009e7ed578da0a9781366f1461a1484e64f15707f26d4310e59514db6ff9f7e0f1**\r\n\r\n"
req, err := http.NewRequest("PUT", "http://localhost:8084/"+bktName+"/"+objName, bytes.NewBufferString(body))
require.NoError(t, err)
req.Header.Set(api.Authorization, "AWS4-ECDSA-P256-SHA256 Credential=3jNrmDtHtuj1uLcixaSMA4KNUhNYhv1EpUNdFnbTXgUP071pGdSZfHSLtoC8gzjF5HoD6sC3Scq33t1WvvEvjmPnt/20250213/s3/aws4_request, SignedHeaders=amz-sdk-invocation-id;amz-sdk-request;content-length;content-type;host;x-amz-content-sha256;x-amz-date;x-amz-decoded-content-length;x-amz-region-set, Signature=3046022100dc589ea513448b996809db4b314a0b8a4a775c1165c6203c7104b2f1aae1243c0221009bf3a256e7c33415eaad20c1dbfb4e14cb00b362758bc4d2aaf94ca96a5f13f9")
req.Header.Set("Amz-Sdk-Invocation-Id", "f0814a40-0d74-066f-d01f-ed14f28ebfa4")
req.Header.Set("Amz-Sdk-Request", "attempt=1; max=2")
req.Header.Set(api.ContentEncoding, api.AwsChunked)
req.Header.Set(api.AmzDate, "20250213T135717Z")
req.Header.Set(api.AmzContentSha256, api.StreamingContentV4aSHA256)
req.Header.Set(api.AmzDecodedContentLength, "0")
req.Header.Set(api.ContentLength, "166")
req.Header.Set(api.ContentType, "text/plain: charset=UTF-8")
req.Header.Set("X-Amz-Region-Set", "use-east-1")
signTime, err := time.Parse("20060102T150405Z", req.Header.Get(api.AmzDate))
require.NoError(t, err)
return prepareReqMiddlewares(req, signTime, AWSSecretAccessKey)
} }
func TestCreateBucket(t *testing.T) { func TestCreateBucket(t *testing.T) {
@ -797,6 +1237,104 @@ func TestFormEncryptionParamsBase(t *testing.T) {
} }
} }
func TestCheckContentLength(t *testing.T) {
contentLength := "content-length-range"
notFallError := "length of the content did not fall within the range specified in the condition"
parseError := "invalid condition"
for _, tc := range []struct {
name string
matching string
key string
value string
size uint64
errMsg string
emptyPolicy bool
}{
{
name: "valid",
matching: contentLength,
key: "0",
value: "1000",
size: 50,
},
{
name: "valid lower limit",
matching: contentLength,
key: "5",
value: "100",
size: 5,
},
{
name: "valid upper limit",
matching: contentLength,
key: "5",
value: "100",
size: 100,
},
{
name: "invalid size value (too small)",
matching: contentLength,
key: "5",
value: "100",
size: 2,
errMsg: notFallError,
},
{
name: "invalid size value (to high)",
matching: contentLength,
key: "5",
value: "100",
size: 200,
errMsg: notFallError,
},
{
name: "no matching",
},
{
name: "invalid key type",
matching: contentLength,
key: "invalid",
value: "100",
size: 10,
errMsg: parseError,
},
{
name: "invalid value type",
matching: contentLength,
key: "5",
value: "invalid",
size: 10,
errMsg: parseError,
},
{
name: "empty policy",
emptyPolicy: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
policy := &postPolicy{
Conditions: []*policyCondition{
{
Matching: tc.matching,
Key: tc.key,
Value: tc.value,
},
},
empty: tc.emptyPolicy,
}
err := policy.CheckContentLength(tc.size)
if tc.errMsg != "" {
require.Error(t, err)
require.Contains(t, err.Error(), tc.errMsg)
return
}
require.NoError(t, err)
})
}
}
func prepareRequestForEncryption(hc *handlerContext, algo, key, md5, tlsTermination string, reqWithoutTLS, reqWithoutSSE, isCopySource bool) *http.Request { func prepareRequestForEncryption(hc *handlerContext, algo, key, md5, tlsTermination string, reqWithoutTLS, reqWithoutSSE, isCopySource bool) *http.Request {
r := httptest.NewRequest(http.MethodPost, "/", nil) r := httptest.NewRequest(http.MethodPost, "/", nil)
@ -825,8 +1363,8 @@ func prepareRequestForEncryption(hc *handlerContext, algo, key, md5, tlsTerminat
return r return r
} }
func postObjectBase(hc *handlerContext, ns, bktName, key, filename, content string) *httptest.ResponseRecorder { func postObjectBase(hc *handlerContext, ns, bktName, key, filename, content, tagging string) *httptest.ResponseRecorder {
policy := "eyJleHBpcmF0aW9uIjogIjIwMjUtMTItMDFUMTI6MDA6MDAuMDAwWiIsImNvbmRpdGlvbnMiOiBbCiBbInN0YXJ0cy13aXRoIiwgIiR4LWFtei1jcmVkZW50aWFsIiwgIiJdLAogWyJzdGFydHMtd2l0aCIsICIkeC1hbXotZGF0ZSIsICIiXSwKIFsic3RhcnRzLXdpdGgiLCAiJGtleSIsICIiXQpdfQ==" policy := "eyJleHBpcmF0aW9uIjogIjIwMjUtMTItMDFUMTI6MDA6MDAuMDAwWiIsImNvbmRpdGlvbnMiOiBbCiBbInN0YXJ0cy13aXRoIiwgIiR4LWFtei1jcmVkZW50aWFsIiwgIiJdLAogWyJzdGFydHMtd2l0aCIsICIkeC1hbXotZGF0ZSIsICIiXSwKIFsic3RhcnRzLXdpdGgiLCAiJGtleSIsICIiXSwKIFsic3RhcnRzLXdpdGgiLCAiJHRhZ2dpbmciLCAiIl0KXX0K"
timeToSign := time.Now() timeToSign := time.Now()
timeToSignStr := timeToSign.Format("20060102T150405Z") timeToSignStr := timeToSign.Format("20060102T150405Z")
@ -839,7 +1377,7 @@ func postObjectBase(hc *handlerContext, ns, bktName, key, filename, content stri
creds := getCredsStr(accessKeyID, timeToSignStr, region, service) creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
sign := auth.SignStr(secretKey, service, region, timeToSign, policy) sign := auth.SignStr(secretKey, service, region, timeToSign, policy)
body, contentType, err := getMultipartFormBody(policy, creds, timeToSignStr, sign, key, filename, content) body, contentType, err := getMultipartFormBody(policy, creds, timeToSignStr, sign, key, filename, content, tagging)
require.NoError(hc.t, err) require.NoError(hc.t, err)
w, r := prepareTestPostRequest(hc, bktName, body) w, r := prepareTestPostRequest(hc, bktName, body)
@ -857,7 +1395,7 @@ func getCredsStr(accessKeyID, timeToSign, region, service string) string {
return accessKeyID + "/" + timeToSign + "/" + region + "/" + service + "/aws4_request" return accessKeyID + "/" + timeToSign + "/" + region + "/" + service + "/aws4_request"
} }
func getMultipartFormBody(policy, creds, date, sign, key, filename, content string) (io.Reader, string, error) { func getMultipartFormBody(policy, creds, date, sign, key, filename, content, tagging string) (io.Reader, string, error) {
body := &bytes.Buffer{} body := &bytes.Buffer{}
writer := multipart.NewWriter(body) writer := multipart.NewWriter(body)
defer writer.Close() defer writer.Close()
@ -878,6 +1416,9 @@ func getMultipartFormBody(policy, creds, date, sign, key, filename, content stri
if err := writer.WriteField(strings.ToLower(auth.AmzSignature), sign); err != nil { if err := writer.WriteField(strings.ToLower(auth.AmzSignature), sign); err != nil {
return nil, "", err return nil, "", err
} }
if err := writer.WriteField("tagging", tagging); err != nil {
return nil, "", err
}
file, err := writer.CreateFormFile("file", filename) file, err := writer.CreateFormFile("file", filename)
if err != nil { if err != nil {

View file

@ -15,6 +15,9 @@ type ListBucketsResponse struct {
Buckets struct { Buckets struct {
Buckets []Bucket `xml:"Bucket"` Buckets []Bucket `xml:"Bucket"`
} // Buckets are nested } // Buckets are nested
ContinuationToken string `xml:"ContinuationToken,omitempty"`
Prefix string `xml:"Prefix,omitempty"`
} }
// ListObjectsV1Response -- format for ListObjectsV1 response. // ListObjectsV1Response -- format for ListObjectsV1 response.
@ -51,8 +54,9 @@ type ListObjectsV2Response struct {
// Bucket container for bucket metadata. // Bucket container for bucket metadata.
type Bucket struct { type Bucket struct {
Name string Name string `xml:"Name"`
CreationDate string // time string of format "2006-01-02T15:04:05.000Z" CreationDate string `xml:"CreationDate"` // time string of format "2006-01-02T15:04:05.000Z"
BucketRegion string `xml:"BucketRegion,omitempty"`
} }
// PolicyStatus contains status of bucket policy. // PolicyStatus contains status of bucket policy.
@ -63,9 +67,12 @@ type PolicyStatus struct {
type PolicyStatusIsPublic string type PolicyStatusIsPublic string
// According to the AWS documentation (https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetBucketPolicyStatus.html),
// the `IsPublic` tag value should be in uppercase. However, the `aws-cli` utility interprets such responses
// as always `false`. To avoid incorrect interpretation, we return the tag value in lowercase.
const ( const (
PolicyStatusIsPublicFalse = "FALSE" PolicyStatusIsPublicFalse = "false"
PolicyStatusIsPublicTrue = "TRUE" PolicyStatusIsPublicTrue = "true"
) )
// AccessControlPolicy contains ACL. // AccessControlPolicy contains ACL.

View file

@ -8,6 +8,8 @@ import (
"errors" "errors"
"io" "io"
"net/http" "net/http"
"slices"
"strings"
"time" "time"
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4" v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4"
@ -27,16 +29,19 @@ type (
reader *bufio.Reader reader *bufio.Reader
streamSigner *v4.StreamSigner streamSigner *v4.StreamSigner
requestTime time.Time trailerHeaders []string
buffer []byte trailers map[string]string
offset int requestTime time.Time
err error buffer []byte
offset int
err error
} }
) )
var ( var (
errGiantChunk = errors.New("chunk too big: choose chunk size <= 16MiB") errGiantChunk = errors.New("chunk too big: choose chunk size <= 16MiB")
errMalformedChunkedEncoding = errors.New("malformed chunked encoding") errMalformedChunkedEncoding = errors.New("malformed chunked encoding")
errMalformedTrailerHeaders = errors.New("malformed trailer headers")
) )
func (c *s3ChunkReader) Close() (err error) { func (c *s3ChunkReader) Close() (err error) {
@ -54,6 +59,10 @@ func (c *s3ChunkReader) Read(buf []byte) (num int, err error) {
buf = buf[num:] buf = buf[num:]
} }
if c.err != nil {
return 0, c.err
}
var size int var size int
for { for {
b, err := c.reader.ReadByte() b, err := c.reader.ReadByte()
@ -107,29 +116,9 @@ func (c *s3ChunkReader) Read(buf []byte) (num int, err error) {
c.err = errMalformedChunkedEncoding c.err = errMalformedChunkedEncoding
return num, c.err return num, c.err
} }
b, err := c.reader.ReadByte()
if err == io.EOF { if err = c.readCRLF(); err != nil {
err = io.ErrUnexpectedEOF return num, err
}
if err != nil {
c.err = err
return num, c.err
}
if b != '\r' {
c.err = errMalformedChunkedEncoding
return num, c.err
}
b, err = c.reader.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
if err != nil {
c.err = err
return num, c.err
}
if b != '\n' {
c.err = errMalformedChunkedEncoding
return num, c.err
} }
if cap(c.buffer) < size { if cap(c.buffer) < size {
@ -147,23 +136,6 @@ func (c *s3ChunkReader) Read(buf []byte) (num int, err error) {
c.err = err c.err = err
return num, c.err return num, c.err
} }
b, err = c.reader.ReadByte()
if b != '\r' || err != nil {
c.err = errMalformedChunkedEncoding
return num, c.err
}
b, err = c.reader.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
if err != nil {
c.err = err
return num, c.err
}
if b != '\n' {
c.err = errMalformedChunkedEncoding
return num, c.err
}
// Once we have read the entire chunk successfully, we verify // Once we have read the entire chunk successfully, we verify
// that the received signature matches our computed signature. // that the received signature matches our computed signature.
@ -181,16 +153,99 @@ func (c *s3ChunkReader) Read(buf []byte) (num int, err error) {
// If the chunk size is zero we return io.EOF. As specified by AWS, // If the chunk size is zero we return io.EOF. As specified by AWS,
// only the last chunk is zero-sized. // only the last chunk is zero-sized.
if size == 0 { if size == 0 {
if len(c.trailerHeaders) != 0 {
if err = c.readTrailers(); err != nil {
c.err = err
return num, c.err
}
} else if err = c.readCRLF(); err != nil {
return num, err
}
c.err = io.EOF c.err = io.EOF
return num, c.err return num, c.err
} }
if err = c.readCRLF(); err != nil {
return num, err
}
c.offset = copy(buf, c.buffer) c.offset = copy(buf, c.buffer)
num += c.offset num += c.offset
return num, err return num, err
} }
func newSignV4ChunkedReader(req *http.Request) (io.ReadCloser, error) { func (c *s3ChunkReader) readCRLF() error {
for _, ch := range [2]byte{'\r', '\n'} {
b, err := c.reader.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
if err != nil {
c.err = err
return c.err
}
if b != ch {
c.err = errMalformedChunkedEncoding
return c.err
}
}
return nil
}
func (c *s3ChunkReader) readTrailers() error {
var k, v []byte
var err error
for err == nil {
k, err = c.reader.ReadBytes(':')
if err != nil {
if err == io.EOF {
break
}
c.err = errMalformedTrailerHeaders
return c.err
}
v, err = c.reader.ReadBytes('\n')
if err != nil && err != io.EOF {
c.err = errMalformedTrailerHeaders
return c.err
}
if len(v) >= 2 && v[len(v)-2] == '\r' {
v[len(v)-2] = '\n'
v = v[:len(v)-1]
}
switch {
case slices.Contains(c.trailerHeaders, string(k[:len(k)-1])):
c.buffer = append(append(c.buffer, k...), v...) // todo use copy
case string(k) == "x-amz-trailer-signature:":
calculatedSignature, err := c.streamSigner.GetTrailerSignature(c.buffer, c.requestTime)
if err != nil {
c.err = err
return c.err
}
if string(v[:64]) != hex.EncodeToString(calculatedSignature) {
c.err = errs.GetAPIError(errs.ErrSignatureDoesNotMatch)
return c.err
}
default:
c.err = errMalformedTrailerHeaders
return c.err
}
c.trailers[string(k[:len(k)-1])] = string(v[:len(v)-1])
}
return nil
}
func (c *s3ChunkReader) TrailerHeaders() map[string]string {
return c.trailers
}
func newSignV4ChunkedReader(req *http.Request) (*s3ChunkReader, error) {
ctx := req.Context() ctx := req.Context()
box, err := middleware.GetBoxData(ctx) box, err := middleware.GetBoxData(ctx)
if err != nil { if err != nil {
@ -214,11 +269,19 @@ func newSignV4ChunkedReader(req *http.Request) (io.ReadCloser, error) {
} }
newStreamSigner := v4.NewStreamSigner(currentCredentials, "s3", authHeaders.Region, seed) newStreamSigner := v4.NewStreamSigner(currentCredentials, "s3", authHeaders.Region, seed)
var trailerHeaders []string
trailer := req.Header.Get("x-amz-trailer")
if trailer != "" {
trailerHeaders = strings.Split(trailer, ";")
}
return &s3ChunkReader{ return &s3ChunkReader{
ctx: ctx, ctx: ctx,
reader: bufio.NewReader(req.Body), reader: bufio.NewReader(req.Body),
streamSigner: newStreamSigner, streamSigner: newStreamSigner,
requestTime: reqTime, requestTime: reqTime,
buffer: make([]byte, 64*1024), buffer: make([]byte, 64*1024),
trailerHeaders: trailerHeaders,
trailers: make(map[string]string, len(trailerHeaders)),
}, nil }, nil
} }

View file

@ -2,8 +2,12 @@ package handler
import ( import (
"bytes" "bytes"
"context"
"fmt"
"io" "io"
"net/http" "net/http"
"strconv"
"strings"
"testing" "testing"
"time" "time"
@ -12,22 +16,102 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestSigV4AStreaming(t *testing.T) { func TestSigV4AChunkedReader(t *testing.T) {
accessKeyID := "2XEbqH4M3ym7a3E3esxfZ2gRLnMwDXrCN4y1SkQg5fHa09sThVmVL3EE6xeKsyMzaqu5jPi41YCaVbnwbwCTF3bx1" t.Run("with trailers", func(t *testing.T) {
secretKey := "00637f53f842573aaa06c2164c598973cd986880987111416cf71f1619def537" accessKeyID := "9uEm8zMrGWsEDWiPCnVuQLKTiGtCEXpYXt8eBG7agupw0JDySJZMFuej7PTcPzRqBUyPtFowNu1RtvHULU8XHjie6"
secretKey := "9f546428957ed7e189b7be928906ce7d1d9cb3042dd4d2d5194e28ce8c4c3b8e"
chunk1 := "Testing with the {sdk-java}" chunk1 := "Testing with the {sdk-java}"
reqBody := bytes.NewBufferString("1b;chunk-signature=3045022100b63692a1b20759bdabd342011823427a8952df75c93174d98ad043abca8052e002201695228a91ba986171b8d0ad20856d3d94ca3614d0a90a50a531ba8e52447b9b**\r\n") body := "1b;chunk-signature=3045022100956ca03d2166100b455b532de542892f73925fbcea2f6498674a39a61bb4860902202977c1d47aea548d434540f89640ce97e605d18353cbbd75a619874f02e3dd22**\r\n" +
_, err := reqBody.WriteString(chunk1) chunk1 +
require.NoError(t, err) "\r\n0;chunk-signature=304502210097dcc1721675469910ef8712fc2af0678eb90c12216dd6228c6b621fb6f805a0022047d27d21ae2af8a8172f2ef83c81ce9d4746aa88fc9ee0ca783eaa5e71aaef6c**\r\n" +
_, err = reqBody.WriteString("\r\n0;chunk-signature=30440220455885a2d4e9f705256ca6b0a5a22f7f784780ccbd1c0a371e5db3059c91745b022073259dd44746cbd63261d628a04d25be5a32a974c077c5c2d83c8157fb323b9f****\r\n\r\n") "x-amz-checksum-crc32:Np6zMg==\r\n" +
require.NoError(t, err) "x-amz-trailer-signature:304502200ecacd9aa2c432af5a2327c22a2ff9b32f44ab8559de00309219aef105eaaac102210092cbc0e78c4bcd56490a73da8ceed1934be80f3affeffb14d8c743fc292dda4f**\r\n\r\n"
req, err := http.NewRequest("PUT", "http://localhost:8084/test/tmp", reqBody) reqBody := bytes.NewBufferString(body)
require.NoError(t, err) req, err := http.NewRequest("PUT", "https://localhost:8184/test2/tmp", reqBody)
require.NoError(t, err)
req.Header.Set("x-amz-trailer", "x-amz-checksum-crc32")
signature := "30440220574244c5ff5deba388c4e3b0541a42113179b6839b3e6b4212d255a118fa9089022056f7b9b72c93f67dbcd25fe9ca67950b5913fc00bb7a62bc276c21e828c0b6c7" signature := "3045022100ddbc6ab11785d7f23d299de7db97379116f543377a44e38170a4e43b38b0d62b02201d8dca13c67f04f45491345152db4b704768eb8bb89b5215fd59bb4a4d9d7b61"
signingTime, err := time.Parse("20060102T150405Z", "20240904T133253Z") signingTime, err := time.Parse("20060102T150405Z", "20250203T144621Z")
require.NoError(t, err)
key, err := keys.NewPrivateKey()
require.NoError(t, err)
accessBox, err := newTestAccessBox(key)
require.NoError(t, err)
accessBox.Gate.SecretKey = secretKey
ctx := middleware.SetBox(req.Context(), &middleware.Box{
AccessBox: accessBox,
AuthHeaders: &middleware.AuthHeader{
AccessKeyID: accessKeyID,
SignatureV4: signature,
},
ClientTime: signingTime,
})
req = req.WithContext(ctx)
r, err := newSignV4aChunkedReader(req)
require.NoError(t, err)
data, err := io.ReadAll(r)
require.NoError(t, err)
require.Equal(t, chunk1, string(data))
})
t.Run("without trailers", func(t *testing.T) {
accessKeyID := "2XEbqH4M3ym7a3E3esxfZ2gRLnMwDXrCN4y1SkQg5fHa09sThVmVL3EE6xeKsyMzaqu5jPi41YCaVbnwbwCTF3bx1"
secretKey := "00637f53f842573aaa06c2164c598973cd986880987111416cf71f1619def537"
chunk1 := "Testing with the {sdk-java}"
reqBody := bytes.NewBufferString("1b;chunk-signature=3045022100b63692a1b20759bdabd342011823427a8952df75c93174d98ad043abca8052e002201695228a91ba986171b8d0ad20856d3d94ca3614d0a90a50a531ba8e52447b9b**\r\n")
_, err := reqBody.WriteString(chunk1)
require.NoError(t, err)
_, err = reqBody.WriteString("\r\n0;chunk-signature=30440220455885a2d4e9f705256ca6b0a5a22f7f784780ccbd1c0a371e5db3059c91745b022073259dd44746cbd63261d628a04d25be5a32a974c077c5c2d83c8157fb323b9f****\r\n\r\n")
require.NoError(t, err)
req, err := http.NewRequest("PUT", "http://localhost:8084/test/tmp", reqBody)
require.NoError(t, err)
signature := "30440220574244c5ff5deba388c4e3b0541a42113179b6839b3e6b4212d255a118fa9089022056f7b9b72c93f67dbcd25fe9ca67950b5913fc00bb7a62bc276c21e828c0b6c7"
signingTime, err := time.Parse("20060102T150405Z", "20240904T133253Z")
require.NoError(t, err)
key, err := keys.NewPrivateKey()
require.NoError(t, err)
accessBox, err := newTestAccessBox(key)
require.NoError(t, err)
accessBox.Gate.SecretKey = secretKey
ctx := middleware.SetBox(req.Context(), &middleware.Box{
AccessBox: accessBox,
AuthHeaders: &middleware.AuthHeader{
AccessKeyID: accessKeyID,
SignatureV4: signature,
},
ClientTime: signingTime,
})
req = req.WithContext(ctx)
r, err := newSignV4aChunkedReader(req)
require.NoError(t, err)
data, err := io.ReadAll(r)
require.NoError(t, err)
require.Equal(t, chunk1, string(data))
})
}
func TestSigV4ChunkedReader(t *testing.T) {
accessKeyID := "9uEm8zMrGWsEDWiPCnVuQLKTiGtCEXpYXt8eBG7agupw0JDySJZMFuej7PTcPzRqBUyPtFowNu1RtvHULU8XHjie6"
secretKey := "9f546428957ed7e189b7be928906ce7d1d9cb3042dd4d2d5194e28ce8c4c3b8e"
signature := "b740b3b2a08c541c3fc4bd155a448e25408b509a29af98a86356b894930b93e8"
signingTime, err := time.Parse("20060102T150405Z", "20250203T134442Z")
require.NoError(t, err) require.NoError(t, err)
key, err := keys.NewPrivateKey() key, err := keys.NewPrivateKey()
@ -37,21 +121,117 @@ func TestSigV4AStreaming(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
accessBox.Gate.SecretKey = secretKey accessBox.Gate.SecretKey = secretKey
ctx := middleware.SetBox(req.Context(), &middleware.Box{ setBoxFn := func(ctx context.Context) context.Context {
AccessBox: accessBox, return middleware.SetBox(ctx, &middleware.Box{
AuthHeaders: &middleware.AuthHeader{ AccessBox: accessBox,
AccessKeyID: accessKeyID, AuthHeaders: &middleware.AuthHeader{
SignatureV4: signature, AccessKeyID: accessKeyID,
}, SignatureV4: signature,
ClientTime: signingTime, Region: "us-east-1",
},
ClientTime: signingTime,
})
}
chunk1 := "Testing with the {sdk-java}"
t.Run("with trailers", func(t *testing.T) {
body := "1b;chunk-signature=a6a9be5fff05db0b542aedb2203d892b4162250885d06b1422b173ee0ea92ba5\r\n" +
chunk1 +
"\r\n0;chunk-signature=31afd083a57c416c46afaf101649d7f0c6c0627cfa60c0f93d1f7ea84396ee42\r\n" +
"x-amz-checksum-crc32:Np6zMg==\r\n" +
"x-amz-trailer-signature:40ec0046ac730fa27a1451d00d849056c49553ee753f5d158306d05671a42125\r\n\r\n"
reqBody := bytes.NewBufferString(body)
req, err := http.NewRequest("PUT", "https://localhost:8184/test2/tmp", reqBody)
require.NoError(t, err)
req.Header.Set("x-amz-trailer", "x-amz-checksum-crc32")
req = req.WithContext(setBoxFn(req.Context()))
r, err := newSignV4ChunkedReader(req)
require.NoError(t, err)
data, err := io.ReadAll(r)
require.NoError(t, err)
require.Equal(t, chunk1, string(data))
}) })
req = req.WithContext(ctx)
r, err := newSignV4aChunkedReader(req) t.Run("without trailers", func(t *testing.T) {
require.NoError(t, err) body := "1b;chunk-signature=a6a9be5fff05db0b542aedb2203d892b4162250885d06b1422b173ee0ea92ba5\r\n" +
chunk1 +
"\r\n0;chunk-signature=31afd083a57c416c46afaf101649d7f0c6c0627cfa60c0f93d1f7ea84396ee42\r\n\r\n"
reqBody := bytes.NewBufferString(body)
req, err := http.NewRequest("PUT", "https://localhost:8184/test2/tmp", reqBody)
require.NoError(t, err)
data, err := io.ReadAll(r) req = req.WithContext(setBoxFn(req.Context()))
require.NoError(t, err)
require.Equal(t, chunk1, string(data)) r, err := newSignV4ChunkedReader(req)
require.NoError(t, err)
data, err := io.ReadAll(r)
require.NoError(t, err)
require.Equal(t, chunk1, string(data))
})
}
func TestUnsignedChunkReader(t *testing.T) {
chunk1 := "chunk1"
chunk2 := "chunk2"
t.Run("with trailer", func(t *testing.T) {
chunks := []string{chunk1, chunk2}
trailer := map[string]string{"x-amz-checksum-crc64nvme": "q1EYl4rI0TU="}
body, expected := getChunkedBody(t, chunks, trailer)
r, err := newUnsignedChunkedReader(body)
require.NoError(t, err)
data, err := io.ReadAll(r)
require.NoError(t, err)
require.Equal(t, expected, string(data))
require.EqualValues(t, trailer, r.TrailerHeaders())
})
t.Run("without trailer", func(t *testing.T) {
chunks := []string{chunk1, chunk2}
body, expected := getChunkedBody(t, chunks, nil)
r, err := newUnsignedChunkedReader(body)
require.NoError(t, err)
data, err := io.ReadAll(r)
require.NoError(t, err)
require.Equal(t, expected, string(data))
})
}
func getChunkedBody(t *testing.T, chunks []string, trailers map[string]string) (*bytes.Buffer, string) {
res := bytes.NewBufferString("")
for i, chunk := range chunks {
meta := strconv.FormatInt(int64(len(chunk)), 16) + "\r\n"
if i != 0 {
meta = "\r\n" + meta
}
_, err := res.WriteString(meta)
require.NoError(t, err)
_, err = res.WriteString(chunk)
require.NoError(t, err)
}
_, err := res.WriteString("\r\n0\r\n")
require.NoError(t, err)
for k, v := range trailers {
_, err := res.WriteString(fmt.Sprintf("%s:%s\n", k, v))
require.NoError(t, err)
}
_, err = res.WriteString("\r\n")
require.NoError(t, err)
return res, strings.Join(chunks, "")
} }

View file

@ -0,0 +1,165 @@
package handler
import (
"bufio"
"io"
)
type (
s3UnsignedChunkReader struct {
reader *bufio.Reader
trailers map[string]string
buffer []byte
offset int
err error
}
)
func (c *s3UnsignedChunkReader) Close() (err error) {
return nil
}
func (c *s3UnsignedChunkReader) Read(buf []byte) (num int, err error) {
if c.offset > 0 {
num = copy(buf, c.buffer[c.offset:])
if num == len(buf) {
c.offset += num
return num, nil
}
c.offset = 0
buf = buf[num:]
}
if c.err != nil {
return 0, c.err
}
var size int
var b byte
for {
b, err = c.reader.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
if err != nil {
c.err = err
return num, c.err
}
if b == '\r' {
break
}
// Manually deserialize the size since AWS specified
// the chunk size to be of variable width. In particular,
// a size of 16 is encoded as `10` while a size of 64 KB
// is `10000`.
switch {
case b >= '0' && b <= '9':
size = size<<4 | int(b-'0')
case b >= 'a' && b <= 'f':
size = size<<4 | int(b-('a'-10))
case b >= 'A' && b <= 'F':
size = size<<4 | int(b-('A'-10))
default:
c.err = errMalformedChunkedEncoding
return num, c.err
}
if size > maxChunkSize {
c.err = errGiantChunk
return num, c.err
}
}
if b != '\r' {
c.err = errMalformedChunkedEncoding
return num, c.err
}
b, err = c.reader.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
if err != nil {
c.err = err
return num, c.err
}
if b != '\n' {
c.err = errMalformedChunkedEncoding
return num, c.err
}
if cap(c.buffer) < size {
c.buffer = make([]byte, size)
} else {
c.buffer = c.buffer[:size]
}
// Now, we read the payload and compute its SHA-256 hash.
_, err = io.ReadFull(c.reader, c.buffer)
if err == io.EOF && size != 0 {
err = io.ErrUnexpectedEOF
}
if err != nil && err != io.EOF {
c.err = err
return num, c.err
}
// If the chunk size is zero we return io.EOF. As specified by AWS,
// only the last chunk is zero-sized.
if size == 0 {
var k, v string
for err == nil {
k, err = c.reader.ReadString(':')
if err != nil {
if err == io.EOF {
break
}
c.err = errMalformedTrailerHeaders
return num, c.err
}
v, err = c.reader.ReadString('\n')
if err != nil {
c.err = errMalformedTrailerHeaders
return num, c.err
}
c.trailers[k[:len(k)-1]] = v[:len(v)-1]
}
c.err = io.EOF
return num, c.err
}
b, err = c.reader.ReadByte()
if b != '\r' || err != nil {
c.err = errMalformedChunkedEncoding
return num, c.err
}
b, err = c.reader.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
if err != nil {
c.err = err
return num, c.err
}
if b != '\n' {
c.err = errMalformedChunkedEncoding
return num, c.err
}
c.offset = copy(buf, c.buffer)
num += c.offset
return num, err
}
func (c *s3UnsignedChunkReader) TrailerHeaders() map[string]string {
return c.trailers
}
func newUnsignedChunkedReader(body io.Reader) (*s3UnsignedChunkReader, error) {
return &s3UnsignedChunkReader{
reader: bufio.NewReader(body),
trailers: map[string]string{},
buffer: make([]byte, 64*1024),
}, nil
}

View file

@ -7,6 +7,8 @@ import (
"fmt" "fmt"
"io" "io"
"net/http" "net/http"
"slices"
"strings"
"time" "time"
v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2" v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2"
@ -20,10 +22,12 @@ type (
reader *bufio.Reader reader *bufio.Reader
streamSigner *v4a.StreamSigner streamSigner *v4a.StreamSigner
requestTime time.Time trailerHeaders []string
buffer []byte trailers map[string]string
offset int requestTime time.Time
err error buffer []byte
offset int
err error
} }
) )
@ -42,6 +46,10 @@ func (c *s3v4aChunkReader) Read(buf []byte) (num int, err error) {
buf = buf[num:] buf = buf[num:]
} }
if c.err != nil {
return 0, c.err
}
var size int var size int
for { for {
b, err := c.reader.ReadByte() b, err := c.reader.ReadByte()
@ -87,21 +95,9 @@ func (c *s3v4aChunkReader) Read(buf []byte) (num int, err error) {
c.err = errMalformedChunkedEncoding c.err = errMalformedChunkedEncoding
return num, c.err return num, c.err
} }
b, err := c.reader.ReadByte()
if err != nil { if err = c.readCRLF(); err != nil {
return c.handleErr(num, err) return num, err
}
if b != '\r' {
c.err = errMalformedChunkedEncoding
return num, c.err
}
b, err = c.reader.ReadByte()
if err != nil {
return c.handleErr(num, err)
}
if b != '\n' {
c.err = errMalformedChunkedEncoding
return num, c.err
} }
if cap(c.buffer) < size { if cap(c.buffer) < size {
@ -119,19 +115,6 @@ func (c *s3v4aChunkReader) Read(buf []byte) (num int, err error) {
c.err = err c.err = err
return num, c.err return num, c.err
} }
b, err = c.reader.ReadByte()
if b != '\r' || err != nil {
c.err = errMalformedChunkedEncoding
return num, c.err
}
b, err = c.reader.ReadByte()
if err != nil {
return c.handleErr(num, err)
}
if b != '\n' {
c.err = errMalformedChunkedEncoding
return num, c.err
}
// Once we have read the entire chunk successfully, we verify // Once we have read the entire chunk successfully, we verify
// that the received signature is valid. // that the received signature is valid.
@ -150,10 +133,23 @@ func (c *s3v4aChunkReader) Read(buf []byte) (num int, err error) {
// If the chunk size is zero we return io.EOF. As specified by AWS, // If the chunk size is zero we return io.EOF. As specified by AWS,
// only the last chunk is zero-sized. // only the last chunk is zero-sized.
if size == 0 { if size == 0 {
if len(c.trailerHeaders) != 0 {
if err = c.readTrailers(); err != nil {
c.err = err
return num, c.err
}
} else if err = c.readCRLF(); err != nil {
return num, err
}
c.err = io.EOF c.err = io.EOF
return num, c.err return num, c.err
} }
if err = c.readCRLF(); err != nil {
return num, err
}
c.offset = copy(buf, c.buffer) c.offset = copy(buf, c.buffer)
num += c.offset num += c.offset
return num, err return num, err
@ -168,7 +164,78 @@ func (c *s3v4aChunkReader) handleErr(num int, err error) (int, error) {
return num, c.err return num, c.err
} }
func newSignV4aChunkedReader(req *http.Request) (io.ReadCloser, error) { func (c *s3v4aChunkReader) readCRLF() error {
for _, ch := range [2]byte{'\r', '\n'} {
b, err := c.reader.ReadByte()
if err == io.EOF {
err = io.ErrUnexpectedEOF
}
if err != nil {
c.err = err
return c.err
}
if b != ch {
c.err = errMalformedChunkedEncoding
return c.err
}
}
return nil
}
func (c *s3v4aChunkReader) readTrailers() error {
var k, v []byte
var err error
for err == nil {
k, err = c.reader.ReadBytes(':')
if err != nil {
if err == io.EOF {
break
}
c.err = errMalformedTrailerHeaders
return c.err
}
v, err = c.reader.ReadBytes('\n')
if err != nil && err != io.EOF {
c.err = errMalformedTrailerHeaders
return c.err
}
if len(v) >= 2 && v[len(v)-2] == '\r' {
v[len(v)-2] = '\n'
v = v[:len(v)-1]
}
switch {
case slices.Contains(c.trailerHeaders, string(k[:len(k)-1])):
c.buffer = append(append(c.buffer, k...), v...) // todo use copy
case string(k) == "x-amz-trailer-signature:":
n, err := hex.Decode(v[:], bytes.TrimRight(v[:], "*\n"))
if err != nil {
c.err = errMalformedChunkedEncoding
return c.err
}
if err = c.streamSigner.VerifyTrailerSignature(c.buffer, c.requestTime, v[:n]); err != nil {
c.err = fmt.Errorf("%w: %s", errs.GetAPIError(errs.ErrSignatureDoesNotMatch), err.Error())
return c.err
}
default:
c.err = errMalformedTrailerHeaders
return c.err
}
c.trailers[string(k[:len(k)-1])] = string(v[:len(v)-1])
}
return nil
}
func (c *s3v4aChunkReader) TrailerHeaders() map[string]string {
return c.trailers
}
func newSignV4aChunkedReader(req *http.Request) (*s3v4aChunkReader, error) {
box, err := middleware.GetBoxData(req.Context()) box, err := middleware.GetBoxData(req.Context())
if err != nil { if err != nil {
return nil, errs.GetAPIError(errs.ErrAuthorizationHeaderMalformed) return nil, errs.GetAPIError(errs.ErrAuthorizationHeaderMalformed)
@ -200,10 +267,18 @@ func newSignV4aChunkedReader(req *http.Request) (io.ReadCloser, error) {
newStreamSigner := v4a.NewStreamSigner(creds, "s3", seed) newStreamSigner := v4a.NewStreamSigner(creds, "s3", seed)
var trailerHeaders []string
trailer := req.Header.Get("x-amz-trailer")
if trailer != "" {
trailerHeaders = strings.Split(trailer, ";")
}
return &s3v4aChunkReader{ return &s3v4aChunkReader{
reader: bufio.NewReader(req.Body), reader: bufio.NewReader(req.Body),
streamSigner: newStreamSigner, streamSigner: newStreamSigner,
requestTime: reqTime, requestTime: reqTime,
buffer: make([]byte, 64*1024), buffer: make([]byte, 64*1024),
trailerHeaders: trailerHeaders,
trailers: make(map[string]string, len(trailerHeaders)),
}, nil }, nil
} }

View file

@ -6,6 +6,7 @@ import (
"strings" "strings"
"unicode" "unicode"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -21,7 +22,9 @@ const (
) )
func (h *handler) PutObjectTaggingHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutObjectTagging")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
tagSet, err := h.readTagSet(reqInfo.Tagging) tagSet, err := h.readTagSet(reqInfo.Tagging)
@ -54,7 +57,9 @@ func (h *handler) PutObjectTaggingHandler(w http.ResponseWriter, r *http.Request
} }
func (h *handler) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetObjectTagging")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -92,7 +97,9 @@ func (h *handler) GetObjectTaggingHandler(w http.ResponseWriter, r *http.Request
} }
func (h *handler) DeleteObjectTaggingHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) DeleteObjectTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.DeleteObjectTagging")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -116,7 +123,9 @@ func (h *handler) DeleteObjectTaggingHandler(w http.ResponseWriter, r *http.Requ
} }
func (h *handler) PutBucketTaggingHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutBucketTagging")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
tagSet, err := h.readTagSet(reqInfo.Tagging) tagSet, err := h.readTagSet(reqInfo.Tagging)
@ -138,7 +147,9 @@ func (h *handler) PutBucketTaggingHandler(w http.ResponseWriter, r *http.Request
} }
func (h *handler) GetBucketTaggingHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetBucketTagging")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
@ -160,7 +171,9 @@ func (h *handler) GetBucketTaggingHandler(w http.ResponseWriter, r *http.Request
} }
func (h *handler) DeleteBucketTaggingHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) DeleteBucketTaggingHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.DeleteBucketTagging")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)

View file

@ -39,9 +39,40 @@ func (h *handler) logAndSendError(ctx context.Context, w http.ResponseWriter, lo
zap.String("object", reqInfo.ObjectName), zap.String("object", reqInfo.ObjectName),
zap.String("description", logText), zap.String("description", logText),
zap.String("user", reqInfo.User), zap.String("user", reqInfo.User),
zap.Error(err)} zap.Error(err),
}
fields = append(fields, additional...) fields = append(fields, additional...)
h.reqLogger(ctx).Error(logs.RequestFailed, fields...) h.reqLogger(ctx).Error(logs.RequestFailed, append(fields, logs.TagField(logs.TagDatapath))...)
}
func (h *handler) logAndSendErrorNoHeader(ctx context.Context, w http.ResponseWriter, logText string, reqInfo *middleware.ReqInfo, err error, additional ...zap.Field) {
err = handleDeleteMarker(w, err)
if wrErr := middleware.WriteErrorResponseNoHeader(w, reqInfo, apierr.TransformToS3Error(err)); wrErr != nil {
additional = append(additional, zap.NamedError("write_response_error", wrErr))
}
fields := []zap.Field{
zap.String("method", reqInfo.API),
zap.String("bucket", reqInfo.BucketName),
zap.String("object", reqInfo.ObjectName),
zap.String("description", logText),
zap.String("user", reqInfo.User),
zap.Error(err),
}
fields = append(fields, additional...)
h.reqLogger(ctx).Error(logs.RequestFailed, append(fields, logs.TagField(logs.TagDatapath))...)
}
func (h *handler) logError(ctx context.Context, logText string, reqInfo *middleware.ReqInfo, err error, additional ...zap.Field) {
fields := []zap.Field{
zap.String("method", reqInfo.API),
zap.String("bucket", reqInfo.BucketName),
zap.String("object", reqInfo.ObjectName),
zap.String("description", logText),
zap.String("user", reqInfo.User),
zap.Error(err),
}
fields = append(fields, additional...)
h.reqLogger(ctx).Error(logs.RequestFailed, append(fields, logs.TagField(logs.TagDatapath))...)
} }
func handleDeleteMarker(w http.ResponseWriter, err error) error { func handleDeleteMarker(w http.ResponseWriter, err error) error {

View file

@ -3,6 +3,7 @@ package handler
import ( import (
"net/http" "net/http"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
@ -10,7 +11,9 @@ import (
) )
func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.PutBucketVersioning")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
configuration := new(VersioningConfiguration) configuration := new(VersioningConfiguration)
@ -57,7 +60,9 @@ func (h *handler) PutBucketVersioningHandler(w http.ResponseWriter, r *http.Requ
// GetBucketVersioningHandler implements bucket versioning getter handler. // GetBucketVersioningHandler implements bucket versioning getter handler.
func (h *handler) GetBucketVersioningHandler(w http.ResponseWriter, r *http.Request) { func (h *handler) GetBucketVersioningHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "handler.GetBucketVersioning")
defer span.End()
reqInfo := middleware.GetReqInfo(ctx) reqInfo := middleware.GetReqInfo(ctx)
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName) bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)

View file

@ -63,6 +63,7 @@ const (
AmzPartNumberMarker = "X-Amz-Part-Number-Marker" AmzPartNumberMarker = "X-Amz-Part-Number-Marker"
AmzStorageClass = "X-Amz-Storage-Class" AmzStorageClass = "X-Amz-Storage-Class"
AmzForceBucketDelete = "X-Amz-Force-Delete-Bucket" AmzForceBucketDelete = "X-Amz-Force-Delete-Bucket"
AmzTrailer = "X-Amz-Trailer"
AmzServerSideEncryptionCustomerAlgorithm = "x-amz-server-side-encryption-customer-algorithm" AmzServerSideEncryptionCustomerAlgorithm = "x-amz-server-side-encryption-customer-algorithm"
AmzServerSideEncryptionCustomerKey = "x-amz-server-side-encryption-customer-key" AmzServerSideEncryptionCustomerKey = "x-amz-server-side-encryption-customer-key"
@ -94,8 +95,11 @@ const (
DefaultLocationConstraint = "default" DefaultLocationConstraint = "default"
StreamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" StreamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
StreamingContentV4aSHA256 = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD" StreamingContentSHA256Trailer = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER"
StreamingContentV4aSHA256 = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD"
StreamingContentV4aSHA256Trailer = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER"
StreamingUnsignedPayloadTrailer = "STREAMING-UNSIGNED-PAYLOAD-TRAILER"
DefaultStorageClass = "STANDARD" DefaultStorageClass = "STANDARD"
) )
@ -129,6 +133,8 @@ var SystemMetadata = map[string]struct{}{
func IsSignedStreamingV4(r *http.Request) (string, bool) { func IsSignedStreamingV4(r *http.Request) (string, bool) {
shaHeader := r.Header.Get(AmzContentSha256) shaHeader := r.Header.Get(AmzContentSha256)
return shaHeader, return shaHeader,
(shaHeader == StreamingContentSHA256 || shaHeader == StreamingContentV4aSHA256) && (shaHeader == StreamingContentSHA256 || shaHeader == StreamingContentSHA256Trailer ||
shaHeader == StreamingContentV4aSHA256 || shaHeader == StreamingContentV4aSHA256Trailer ||
shaHeader == StreamingUnsignedPayloadTrailer) &&
r.Method == http.MethodPut r.Method == http.MethodPut
} }

View file

@ -76,7 +76,8 @@ func (c *Cache) PutBucket(bktInfo *data.BucketInfo) {
zap.String("zone", bktInfo.Zone), zap.String("zone", bktInfo.Zone),
zap.String("bucket name", bktInfo.Name), zap.String("bucket name", bktInfo.Name),
zap.Stringer("bucket cid", bktInfo.CID), zap.Stringer("bucket cid", bktInfo.CID),
zap.Error(err)) zap.Error(err),
logs.TagField(logs.TagDatapath))
} }
} }
@ -118,11 +119,12 @@ func (c *Cache) PutObject(owner user.ID, extObjInfo *data.ExtendedObjectInfo) {
if err := c.objCache.PutObject(extObjInfo); err != nil { if err := c.objCache.PutObject(extObjInfo); err != nil {
c.logger.Warn(logs.CouldntAddObjectToCache, zap.Error(err), c.logger.Warn(logs.CouldntAddObjectToCache, zap.Error(err),
zap.String("object_name", extObjInfo.ObjectInfo.Name), zap.String("bucket_name", extObjInfo.ObjectInfo.Bucket), zap.String("object_name", extObjInfo.ObjectInfo.Name), zap.String("bucket_name", extObjInfo.ObjectInfo.Bucket),
zap.String("cid", extObjInfo.ObjectInfo.CID.EncodeToString()), zap.String("oid", extObjInfo.ObjectInfo.ID.EncodeToString())) zap.String("cid", extObjInfo.ObjectInfo.CID.EncodeToString()), zap.String("oid", extObjInfo.ObjectInfo.ID.EncodeToString()),
logs.TagField(logs.TagDatapath))
} }
if err := c.accessCache.Put(owner, extObjInfo.ObjectInfo.Address().EncodeToString()); err != nil { if err := c.accessCache.Put(owner, extObjInfo.ObjectInfo.Address().EncodeToString()); err != nil {
c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err)) c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
} }
@ -132,7 +134,8 @@ func (c *Cache) PutObjectWithName(owner user.ID, extObjInfo *data.ExtendedObject
if err := c.namesCache.Put(extObjInfo.ObjectInfo.NiceName(), extObjInfo.ObjectInfo.Address()); err != nil { if err := c.namesCache.Put(extObjInfo.ObjectInfo.NiceName(), extObjInfo.ObjectInfo.Address()); err != nil {
c.logger.Warn(logs.CouldntPutObjAddressToNameCache, c.logger.Warn(logs.CouldntPutObjAddressToNameCache,
zap.String("obj nice name", extObjInfo.ObjectInfo.NiceName()), zap.String("obj nice name", extObjInfo.ObjectInfo.NiceName()),
zap.Error(err)) zap.Error(err),
logs.TagField(logs.TagDatapath))
} }
} }
@ -146,11 +149,11 @@ func (c *Cache) GetList(owner user.ID, key cache.ObjectsListKey) []*data.NodeVer
func (c *Cache) PutList(owner user.ID, key cache.ObjectsListKey, list []*data.NodeVersion) { func (c *Cache) PutList(owner user.ID, key cache.ObjectsListKey, list []*data.NodeVersion) {
if err := c.listsCache.PutVersions(key, list); err != nil { if err := c.listsCache.PutVersions(key, list); err != nil {
c.logger.Warn(logs.CouldntCacheListOfObjects, zap.Error(err)) c.logger.Warn(logs.CouldntCacheListOfObjects, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
if err := c.accessCache.Put(owner, key.String()); err != nil { if err := c.accessCache.Put(owner, key.String()); err != nil {
c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err)) c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
} }
@ -164,11 +167,11 @@ func (c *Cache) GetListSession(owner user.ID, key cache.ListSessionKey) *data.Li
func (c *Cache) PutListSession(owner user.ID, key cache.ListSessionKey, session *data.ListSession) { func (c *Cache) PutListSession(owner user.ID, key cache.ListSessionKey, session *data.ListSession) {
if err := c.sessionListCache.PutListSession(key, session); err != nil { if err := c.sessionListCache.PutListSession(key, session); err != nil {
c.logger.Warn(logs.CouldntCacheListSession, zap.Error(err)) c.logger.Warn(logs.CouldntCacheListSession, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
if err := c.accessCache.Put(owner, key.String()); err != nil { if err := c.accessCache.Put(owner, key.String()); err != nil {
c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err)) c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
} }
@ -187,11 +190,11 @@ func (c *Cache) GetTagging(owner user.ID, key string) map[string]string {
func (c *Cache) PutTagging(owner user.ID, key string, tags map[string]string) { func (c *Cache) PutTagging(owner user.ID, key string, tags map[string]string) {
if err := c.systemCache.PutTagging(key, tags); err != nil { if err := c.systemCache.PutTagging(key, tags); err != nil {
c.logger.Error(logs.CouldntCacheTags, zap.Error(err)) c.logger.Error(logs.CouldntCacheTags, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
if err := c.accessCache.Put(owner, key); err != nil { if err := c.accessCache.Put(owner, key); err != nil {
c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err)) c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
} }
@ -209,11 +212,11 @@ func (c *Cache) GetLockInfo(owner user.ID, key string) *data.LockInfo {
func (c *Cache) PutLockInfo(owner user.ID, key string, lockInfo *data.LockInfo) { func (c *Cache) PutLockInfo(owner user.ID, key string, lockInfo *data.LockInfo) {
if err := c.systemCache.PutLockInfo(key, lockInfo); err != nil { if err := c.systemCache.PutLockInfo(key, lockInfo); err != nil {
c.logger.Error(logs.CouldntCacheLockInfo, zap.Error(err)) c.logger.Error(logs.CouldntCacheLockInfo, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
if err := c.accessCache.Put(owner, key); err != nil { if err := c.accessCache.Put(owner, key); err != nil {
c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err)) c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
} }
@ -230,11 +233,11 @@ func (c *Cache) GetSettings(owner user.ID, bktInfo *data.BucketInfo) *data.Bucke
func (c *Cache) PutSettings(owner user.ID, bktInfo *data.BucketInfo, settings *data.BucketSettings) { func (c *Cache) PutSettings(owner user.ID, bktInfo *data.BucketInfo, settings *data.BucketSettings) {
key := bktInfo.Name + bktInfo.SettingsObjectName() key := bktInfo.Name + bktInfo.SettingsObjectName()
if err := c.systemCache.PutSettings(key, settings); err != nil { if err := c.systemCache.PutSettings(key, settings); err != nil {
c.logger.Warn(logs.CouldntCacheBucketSettings, zap.String("bucket", bktInfo.Name), zap.Error(err)) c.logger.Warn(logs.CouldntCacheBucketSettings, zap.String("bucket", bktInfo.Name), zap.Error(err), logs.TagField(logs.TagDatapath))
} }
if err := c.accessCache.Put(owner, key); err != nil { if err := c.accessCache.Put(owner, key); err != nil {
c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err)) c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
} }
@ -252,11 +255,11 @@ func (c *Cache) PutCORS(owner user.ID, bkt *data.BucketInfo, cors *data.CORSConf
key := bkt.CORSObjectName() key := bkt.CORSObjectName()
if err := c.systemCache.PutCORS(key, cors); err != nil { if err := c.systemCache.PutCORS(key, cors); err != nil {
c.logger.Warn(logs.CouldntCacheCors, zap.String("bucket", bkt.Name), zap.Error(err)) c.logger.Warn(logs.CouldntCacheCors, zap.String("bucket", bkt.Name), zap.Error(err), logs.TagField(logs.TagDatapath))
} }
if err := c.accessCache.Put(owner, key); err != nil { if err := c.accessCache.Put(owner, key); err != nil {
c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err)) c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
} }
@ -278,11 +281,11 @@ func (c *Cache) PutLifecycleConfiguration(owner user.ID, bkt *data.BucketInfo, c
key := bkt.LifecycleConfigurationObjectName() key := bkt.LifecycleConfigurationObjectName()
if err := c.systemCache.PutLifecycleConfiguration(key, cfg); err != nil { if err := c.systemCache.PutLifecycleConfiguration(key, cfg); err != nil {
c.logger.Warn(logs.CouldntCacheLifecycleConfiguration, zap.String("bucket", bkt.Name), zap.Error(err)) c.logger.Warn(logs.CouldntCacheLifecycleConfiguration, zap.String("bucket", bkt.Name), zap.Error(err), logs.TagField(logs.TagDatapath))
} }
if err := c.accessCache.Put(owner, key); err != nil { if err := c.accessCache.Put(owner, key); err != nil {
c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err)) c.logger.Warn(logs.CouldntCacheAccessControlOperation, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
} }
@ -296,7 +299,7 @@ func (c *Cache) GetNetworkInfo() *netmap.NetworkInfo {
func (c *Cache) PutNetworkInfo(info netmap.NetworkInfo) { func (c *Cache) PutNetworkInfo(info netmap.NetworkInfo) {
if err := c.networkCache.PutNetworkInfo(info); err != nil { if err := c.networkCache.PutNetworkInfo(info); err != nil {
c.logger.Warn(logs.CouldntCacheNetworkInfo, zap.Error(err)) c.logger.Warn(logs.CouldntCacheNetworkInfo, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
} }
@ -306,7 +309,7 @@ func (c *Cache) GetNetmap() *netmap.NetMap {
func (c *Cache) PutNetmap(nm netmap.NetMap) { func (c *Cache) PutNetmap(nm netmap.NetMap) {
if err := c.networkCache.PutNetmap(nm); err != nil { if err := c.networkCache.PutNetmap(nm); err != nil {
c.logger.Warn(logs.CouldntCacheNetmap, zap.Error(err)) c.logger.Warn(logs.CouldntCacheNetmap, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
} }

View file

@ -5,12 +5,16 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree"
) )
func (n *Layer) GetObjectTaggingAndLock(ctx context.Context, objVersion *data.ObjectVersion, nodeVersion *data.NodeVersion) (map[string]string, data.LockInfo, error) { func (n *Layer) GetObjectTaggingAndLock(ctx context.Context, objVersion *data.ObjectVersion, nodeVersion *data.NodeVersion) (map[string]string, data.LockInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetObjectTaggingAndLock")
defer span.End()
var err error var err error
owner := n.BearerOwner(ctx) owner := n.BearerOwner(ctx)

View file

@ -3,7 +3,9 @@ package layer
import ( import (
"context" "context"
"fmt" "fmt"
"sort"
"strconv" "strconv"
"strings"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
@ -62,6 +64,7 @@ func (n *Layer) containerInfo(ctx context.Context, prm frostfs.PrmContainer) (*d
log.Error(logs.CouldNotParseContainerObjectLockEnabledAttribute, log.Error(logs.CouldNotParseContainerObjectLockEnabledAttribute,
zap.String("lock_enabled", attrLockEnabled), zap.String("lock_enabled", attrLockEnabled),
zap.Error(err), zap.Error(err),
logs.TagField(logs.TagDatapath),
) )
} }
} }
@ -76,7 +79,7 @@ func (n *Layer) containerInfo(ctx context.Context, prm frostfs.PrmContainer) (*d
return info, nil return info, nil
} }
func (n *Layer) containerList(ctx context.Context) ([]*data.BucketInfo, error) { func (n *Layer) containerList(ctx context.Context, listParams ListBucketsParams) ([]*data.BucketInfo, error) {
stoken := n.SessionTokenForRead(ctx) stoken := n.SessionTokenForRead(ctx)
prm := frostfs.PrmUserContainers{ prm := frostfs.PrmUserContainers{
@ -86,7 +89,7 @@ func (n *Layer) containerList(ctx context.Context) ([]*data.BucketInfo, error) {
res, err := n.frostFS.UserContainers(ctx, prm) res, err := n.frostFS.UserContainers(ctx, prm)
if err != nil { if err != nil {
n.reqLogger(ctx).Error(logs.CouldNotListUserContainers, zap.Error(err)) n.reqLogger(ctx).Error(logs.CouldNotListUserContainers, zap.Error(err), logs.TagField(logs.TagExternalStorage))
return nil, err return nil, err
} }
@ -98,14 +101,38 @@ func (n *Layer) containerList(ctx context.Context) ([]*data.BucketInfo, error) {
} }
info, err := n.containerInfo(ctx, getPrm) info, err := n.containerInfo(ctx, getPrm)
if err != nil { if err != nil {
n.reqLogger(ctx).Error(logs.CouldNotFetchContainerInfo, zap.Error(err)) n.reqLogger(ctx).Error(logs.CouldNotFetchContainerInfo, zap.Error(err), logs.TagField(logs.TagExternalStorage))
continue
}
if shouldSkipBucket(info, listParams) {
continue continue
} }
list = append(list, info) list = append(list, info)
} }
return list, nil sort.Slice(list, func(i, j int) bool {
return list[i].Name < list[j].Name
})
for i, info := range list {
if listParams.ContinuationToken != "" && info.Name != listParams.ContinuationToken {
continue
}
return list[i:], nil
}
return nil, nil
}
func shouldSkipBucket(info *data.BucketInfo, prm ListBucketsParams) bool {
if !strings.HasPrefix(info.Name, prm.Prefix) ||
(prm.BucketRegion != "" && info.LocationConstraint != prm.BucketRegion) {
return true
}
return false
} }
func (n *Layer) createContainer(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) { func (n *Layer) createContainer(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) {

View file

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"io" "io"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
@ -23,6 +24,9 @@ const wildcard = "*"
var supportedMethods = map[string]struct{}{"GET": {}, "HEAD": {}, "POST": {}, "PUT": {}, "DELETE": {}} var supportedMethods = map[string]struct{}{"GET": {}, "HEAD": {}, "POST": {}, "PUT": {}, "DELETE": {}}
func (n *Layer) PutBucketCORS(ctx context.Context, p *PutCORSParams) error { func (n *Layer) PutBucketCORS(ctx context.Context, p *PutCORSParams) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.PutBucketCORS")
defer span.End()
var ( var (
buf bytes.Buffer buf bytes.Buffer
tee = io.TeeReader(p.Reader, &buf) tee = io.TeeReader(p.Reader, &buf)
@ -42,41 +46,36 @@ func (n *Layer) PutBucketCORS(ctx context.Context, p *PutCORSParams) error {
} }
prm := frostfs.PrmObjectCreate{ prm := frostfs.PrmObjectCreate{
Container: n.corsCnrInfo.CID,
Payload: &buf, Payload: &buf,
Filepath: p.BktInfo.CORSObjectName(), Filepath: p.BktInfo.CORSObjectFilePath(),
CreationTime: TimeNow(ctx), CreationTime: TimeNow(ctx),
CopiesNumber: p.CopiesNumbers,
PrmAuth: frostfs.PrmAuth{
PrivateKey: &n.gateKey.PrivateKey,
},
} }
var corsBkt *data.BucketInfo _, err := n.objectPutAndHash(ctx, prm, n.corsCnrInfo)
if n.corsCnrInfo == nil {
corsBkt = p.BktInfo
prm.CopiesNumber = p.CopiesNumbers
} else {
corsBkt = n.corsCnrInfo
prm.PrmAuth.PrivateKey = &n.gateKey.PrivateKey
}
prm.Container = corsBkt.CID
createdObj, err := n.objectPutAndHash(ctx, prm, corsBkt)
if err != nil { if err != nil {
return fmt.Errorf("put cors object: %w", err) return fmt.Errorf("put cors object: %w", err)
} }
objsToDelete, err := n.treeService.PutBucketCORS(ctx, p.BktInfo, newAddress(corsBkt.CID, createdObj.ID)) n.cache.PutCORS(n.BearerOwner(ctx), p.BktInfo, cors)
objToDeleteNotFound := errors.Is(err, tree.ErrNoNodeToRemove)
if err != nil && !objToDeleteNotFound { objs, err := n.treeService.DeleteBucketCORS(ctx, p.BktInfo)
return err objNotFound := errors.Is(err, tree.ErrNoNodeToRemove)
if err != nil && !objNotFound {
n.reqLogger(ctx).Error(logs.CouldntDeleteBucketCORS, zap.Error(err), logs.TagField(logs.TagExternalStorageTree))
return nil
} }
if !objToDeleteNotFound { if !objNotFound {
for _, addr := range objsToDelete { for _, addr := range objs {
n.deleteCORSObject(ctx, p.BktInfo, addr) n.deleteCORSObject(ctx, p.BktInfo, addr)
} }
} }
n.cache.PutCORS(n.BearerOwner(ctx), p.BktInfo, cors)
return nil return nil
} }
@ -92,11 +91,15 @@ func (n *Layer) deleteCORSObject(ctx context.Context, bktInfo *data.BucketInfo,
if err := n.objectDeleteWithAuth(ctx, corsBkt, addr.Object(), prmAuth); err != nil { if err := n.objectDeleteWithAuth(ctx, corsBkt, addr.Object(), prmAuth); err != nil {
n.reqLogger(ctx).Error(logs.CouldntDeleteCorsObject, zap.Error(err), n.reqLogger(ctx).Error(logs.CouldntDeleteCorsObject, zap.Error(err),
zap.String("cnrID", corsBkt.CID.EncodeToString()), zap.String("cnrID", corsBkt.CID.EncodeToString()),
zap.String("objID", addr.Object().EncodeToString())) zap.String("objID", addr.Object().EncodeToString()),
logs.TagField(logs.TagExternalStorage))
} }
} }
func (n *Layer) GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo, decoder func(io.Reader, string) *xml.Decoder) (*data.CORSConfiguration, error) { func (n *Layer) GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo, decoder func(io.Reader, string) *xml.Decoder) (*data.CORSConfiguration, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetBucketCORS")
defer span.End()
cors, err := n.getCORS(ctx, bktInfo, decoder) cors, err := n.getCORS(ctx, bktInfo, decoder)
if err != nil { if err != nil {
return nil, err return nil, err
@ -106,10 +109,25 @@ func (n *Layer) GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo, dec
} }
func (n *Layer) DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) error { func (n *Layer) DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.DeleteBucketCORS")
defer span.End()
corsVersions, err := n.getCORSVersions(ctx, bktInfo)
if err != nil {
return fmt.Errorf("get cors versions: %w", err)
}
sortedVersions := corsVersions.GetSorted()
for _, version := range sortedVersions {
if err = n.objectDeleteWithAuth(ctx, n.corsCnrInfo, version.ObjID, frostfs.PrmAuth{PrivateKey: &n.gateKey.PrivateKey}); err != nil {
return fmt.Errorf("delete cors object '%s': %w", version.VersionID(), err)
}
}
objs, err := n.treeService.DeleteBucketCORS(ctx, bktInfo) objs, err := n.treeService.DeleteBucketCORS(ctx, bktInfo)
objNotFound := errors.Is(err, tree.ErrNoNodeToRemove) objNotFound := errors.Is(err, tree.ErrNoNodeToRemove)
if err != nil && !objNotFound { if err != nil && !objNotFound {
return err return fmt.Errorf("delete cors from tree: %w", err)
} }
if !objNotFound { if !objNotFound {
@ -123,6 +141,22 @@ func (n *Layer) DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo)
return nil return nil
} }
func (n *Layer) deleteCORSVersions(ctx context.Context, bktInfo *data.BucketInfo) {
corsVersions, err := n.getCORSVersions(ctx, bktInfo)
if err != nil {
n.reqLogger(ctx).Error(logs.CouldntGetCORSObjectVersions, zap.Error(err), logs.TagField(logs.TagExternalStorage))
return
}
var addr oid.Address
addr.SetContainer(n.corsCnrInfo.CID)
sortedVersions := corsVersions.GetSorted()
for _, version := range sortedVersions {
addr.SetObject(version.ObjID)
n.deleteCORSObject(ctx, bktInfo, addr)
}
}
func checkCORS(cors *data.CORSConfiguration) error { func checkCORS(cors *data.CORSConfiguration) error {
for _, r := range cors.CORSRules { for _, r := range cors.CORSRules {
for _, m := range r.AllowedMethods { for _, m := range r.AllowedMethods {

View file

@ -11,6 +11,7 @@ import (
"strings" "strings"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
v2container "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container" v2container "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/container"
@ -74,25 +75,34 @@ func (k *FeatureSettingsMock) FormContainerZone(ns string) string {
var _ frostfs.FrostFS = (*TestFrostFS)(nil) var _ frostfs.FrostFS = (*TestFrostFS)(nil)
type offsetError struct {
offset int
err error
}
type TestFrostFS struct { type TestFrostFS struct {
objects map[string]*object.Object objects map[string]*object.Object
objectErrors map[string]error copiesNumbers map[string][]uint32
objectPutErrors map[string]error objectErrors map[string]error
containers map[string]*container.Container objectStreamErrors map[string]offsetError
chains map[string][]chain.Chain objectPutErrors map[string]error
currentEpoch uint64 containers map[string]*container.Container
key *keys.PrivateKey chains map[string][]chain.Chain
tombstoneOIDCount int currentEpoch uint64
key *keys.PrivateKey
tombstoneOIDCount int
} }
func NewTestFrostFS(key *keys.PrivateKey) *TestFrostFS { func NewTestFrostFS(key *keys.PrivateKey) *TestFrostFS {
return &TestFrostFS{ return &TestFrostFS{
objects: make(map[string]*object.Object), objects: make(map[string]*object.Object),
objectErrors: make(map[string]error), copiesNumbers: make(map[string][]uint32),
objectPutErrors: make(map[string]error), objectErrors: make(map[string]error),
containers: make(map[string]*container.Container), objectStreamErrors: make(map[string]offsetError),
chains: make(map[string][]chain.Chain), objectPutErrors: make(map[string]error),
key: key, containers: make(map[string]*container.Container),
chains: make(map[string][]chain.Chain),
key: key,
} }
} }
@ -108,6 +118,14 @@ func (t *TestFrostFS) SetObjectError(addr oid.Address, err error) {
} }
} }
func (t *TestFrostFS) SetObjectStreamError(addr oid.Address, offset int, err error) {
if err == nil {
delete(t.objectStreamErrors, addr.EncodeToString())
} else {
t.objectStreamErrors[addr.EncodeToString()] = offsetError{offset: offset, err: err}
}
}
func (t *TestFrostFS) SetObjectPutError(fileName string, err error) { func (t *TestFrostFS) SetObjectPutError(fileName string, err error) {
if err == nil { if err == nil {
delete(t.objectPutErrors, fileName) delete(t.objectPutErrors, fileName)
@ -126,6 +144,10 @@ func (t *TestFrostFS) Objects() []*object.Object {
return res return res
} }
func (t *TestFrostFS) CopiesNumbers(addr string) []uint32 {
return t.copiesNumbers[addr]
}
func (t *TestFrostFS) ObjectExists(objID oid.ID) bool { func (t *TestFrostFS) ObjectExists(objID oid.ID) bool {
for _, obj := range t.objects { for _, obj := range t.objects {
if id, _ := obj.ID(); id.Equals(objID) { if id, _ := obj.ID(); id.Equals(objID) {
@ -154,6 +176,34 @@ func (t *TestFrostFS) SetContainer(cnrID cid.ID, cnr *container.Container) {
t.containers[cnrID.EncodeToString()] = cnr t.containers[cnrID.EncodeToString()] = cnr
} }
func (t *TestFrostFS) SetObject(addr oid.Address, obj *object.Object) {
t.objects[addr.EncodeToString()] = obj
}
func (t *TestFrostFS) AddCORSObject(bkt *data.BucketInfo, corsCnrID cid.ID, cors string) {
a := object.NewAttribute()
a.SetKey(object.AttributeFilePath)
a.SetValue(bkt.CORSObjectFilePath())
var owner user.ID
user.IDFromKey(&owner, t.key.PrivateKey.PublicKey)
objID := oidtest.ID()
obj := object.New()
obj.SetContainerID(corsCnrID)
obj.SetID(objID)
obj.SetPayloadSize(uint64(len(cors)))
obj.SetPayload([]byte(cors))
obj.SetAttributes(*a)
obj.SetCreationEpoch(t.currentEpoch)
obj.SetOwnerID(owner)
t.currentEpoch++
addr := newAddress(corsCnrID, objID)
t.objects[addr.EncodeToString()] = obj
}
func (t *TestFrostFS) CreateContainer(_ context.Context, prm frostfs.PrmContainerCreate) (*frostfs.ContainerCreateResult, error) { func (t *TestFrostFS) CreateContainer(_ context.Context, prm frostfs.PrmContainerCreate) (*frostfs.ContainerCreateResult, error) {
var cnr container.Container var cnr container.Container
cnr.Init() cnr.Init()
@ -255,11 +305,33 @@ func (t *TestFrostFS) GetObject(ctx context.Context, prm frostfs.PrmObjectGet) (
} }
return &frostfs.Object{ return &frostfs.Object{
Header: *obj, Header: *obj,
Payload: io.NopCloser(bytes.NewReader(obj.Payload())), Payload: &objPayload{
r: bytes.NewReader(obj.Payload()),
offsetErr: t.objectStreamErrors[prm.Container.EncodeToString()+"/"+prm.Object.EncodeToString()],
},
}, nil }, nil
} }
type objPayload struct {
offset int
r io.Reader
offsetErr offsetError
}
func (o *objPayload) Read(p []byte) (n int, err error) {
n, err = o.r.Read(p)
if o.offsetErr.err != nil && o.offset+n > o.offsetErr.offset {
return o.offsetErr.offset - o.offset, o.offsetErr.err
}
o.offset += n
return n, err
}
func (o *objPayload) Close() error {
return nil
}
func (t *TestFrostFS) RangeObject(ctx context.Context, prm frostfs.PrmObjectRange) (io.ReadCloser, error) { func (t *TestFrostFS) RangeObject(ctx context.Context, prm frostfs.PrmObjectRange) (io.ReadCloser, error) {
obj, err := t.retrieveObject(ctx, prm.Container, prm.Object) obj, err := t.retrieveObject(ctx, prm.Container, prm.Object)
if err != nil { if err != nil {
@ -346,6 +418,8 @@ func (t *TestFrostFS) CreateObject(ctx context.Context, prm frostfs.PrmObjectCre
addr := newAddress(cnrID, objID) addr := newAddress(cnrID, objID)
t.objects[addr.EncodeToString()] = obj t.objects[addr.EncodeToString()] = obj
t.copiesNumbers[addr.EncodeToString()] = prm.CopiesNumber
return &frostfs.CreateObjectResult{ return &frostfs.CreateObjectResult{
ObjectID: objID, ObjectID: objID,
CreationEpoch: t.currentEpoch - 1, CreationEpoch: t.currentEpoch - 1,

View file

@ -15,6 +15,7 @@ import (
"strings" "strings"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -193,6 +194,19 @@ type (
Prefix string Prefix string
VersionIDMarker string VersionIDMarker string
Encode string Encode string
Chan chan<- struct{}
}
ListBucketsParams struct {
MaxBuckets int
Prefix string
ContinuationToken string
BucketRegion string
}
ListBucketsResult struct {
Containers []*data.BucketInfo
ContinuationToken string
} }
// VersionedObject stores info about objects to delete. // VersionedObject stores info about objects to delete.
@ -324,6 +338,9 @@ func (n *Layer) prepareAuthParameters(ctx context.Context, prm *frostfs.PrmAuth,
// GetBucketInfo returns bucket info by name. // GetBucketInfo returns bucket info by name.
func (n *Layer) GetBucketInfo(ctx context.Context, name string) (*data.BucketInfo, error) { func (n *Layer) GetBucketInfo(ctx context.Context, name string) (*data.BucketInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetBucketInfo")
defer span.End()
name, err := url.QueryUnescape(name) name, err := url.QueryUnescape(name)
if err != nil { if err != nil {
return nil, fmt.Errorf("unescape bucket name: %w", err) return nil, fmt.Errorf("unescape bucket name: %w", err)
@ -371,12 +388,34 @@ func (n *Layer) ResolveCID(ctx context.Context, name string) (cid.ID, error) {
// ListBuckets returns all user containers. The name of the bucket is a container // ListBuckets returns all user containers. The name of the bucket is a container
// id. Timestamp is omitted since it is not saved in frostfs container. // id. Timestamp is omitted since it is not saved in frostfs container.
func (n *Layer) ListBuckets(ctx context.Context) ([]*data.BucketInfo, error) { func (n *Layer) ListBuckets(ctx context.Context, params ListBucketsParams) (ListBucketsResult, error) {
return n.containerList(ctx) ctx, span := tracing.StartSpanFromContext(ctx, "layer.ListBuckets")
defer span.End()
var result ListBucketsResult
var err error
if params.MaxBuckets == 0 {
return result, nil
}
result.Containers, err = n.containerList(ctx, params)
if err != nil {
return ListBucketsResult{}, err
}
if len(result.Containers) > params.MaxBuckets {
result.ContinuationToken = result.Containers[params.MaxBuckets].Name
result.Containers = result.Containers[:params.MaxBuckets]
}
return result, nil
} }
// GetObject from storage. // GetObject from storage.
func (n *Layer) GetObject(ctx context.Context, p *GetObjectParams) (*ObjectPayload, error) { func (n *Layer) GetObject(ctx context.Context, p *GetObjectParams) (*ObjectPayload, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetObject")
defer span.End()
var params getParams var params getParams
params.objInfo = p.ObjectInfo params.objInfo = p.ObjectInfo
@ -491,6 +530,9 @@ func getDecrypter(p *GetObjectParams) (*encryption.Decrypter, error) {
// GetObjectInfo returns meta information about the object. // GetObjectInfo returns meta information about the object.
func (n *Layer) GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ObjectInfo, error) { func (n *Layer) GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ObjectInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetObjectInfo")
defer span.End()
extendedObjectInfo, err := n.GetExtendedObjectInfo(ctx, p) extendedObjectInfo, err := n.GetExtendedObjectInfo(ctx, p)
if err != nil { if err != nil {
return nil, err return nil, err
@ -501,8 +543,13 @@ func (n *Layer) GetObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.O
// GetExtendedObjectInfo returns meta information and corresponding info from the tree service about the object. // GetExtendedObjectInfo returns meta information and corresponding info from the tree service about the object.
func (n *Layer) GetExtendedObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ExtendedObjectInfo, error) { func (n *Layer) GetExtendedObjectInfo(ctx context.Context, p *HeadObjectParams) (*data.ExtendedObjectInfo, error) {
var objInfo *data.ExtendedObjectInfo ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetExtendedObjectInfo")
var err error defer span.End()
var (
objInfo *data.ExtendedObjectInfo
err error
)
if p.Versioned() { if p.Versioned() {
objInfo, err = n.headVersion(ctx, p.BktInfo, p) objInfo, err = n.headVersion(ctx, p.BktInfo, p)
@ -515,13 +562,18 @@ func (n *Layer) GetExtendedObjectInfo(ctx context.Context, p *HeadObjectParams)
n.reqLogger(ctx).Debug(logs.GetObject, n.reqLogger(ctx).Debug(logs.GetObject,
zap.Stringer("cid", p.BktInfo.CID), zap.Stringer("cid", p.BktInfo.CID),
zap.Stringer("oid", objInfo.ObjectInfo.ID)) zap.Stringer("oid", objInfo.ObjectInfo.ID),
logs.TagField(logs.TagDatapath),
)
return objInfo, nil return objInfo, nil
} }
// CopyObject from one bucket into another bucket. // CopyObject from one bucket into another bucket.
func (n *Layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*data.ExtendedObjectInfo, error) { func (n *Layer) CopyObject(ctx context.Context, p *CopyObjectParams) (*data.ExtendedObjectInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.CopyObject")
defer span.End()
objPayload, err := n.GetObject(ctx, &GetObjectParams{ objPayload, err := n.GetObject(ctx, &GetObjectParams{
ObjectInfo: p.SrcObject, ObjectInfo: p.SrcObject,
Versioned: p.SrcVersioned, Versioned: p.SrcVersioned,
@ -568,8 +620,8 @@ func (n *Layer) deleteObject(ctx context.Context, bkt *data.BucketInfo, settings
if !client.IsErrObjectAlreadyRemoved(obj.Error) && !client.IsErrObjectNotFound(obj.Error) { if !client.IsErrObjectAlreadyRemoved(obj.Error) && !client.IsErrObjectNotFound(obj.Error) {
return obj return obj
} }
n.reqLogger(ctx).Debug(logs.CouldntDeleteObjectFromStorageContinueDeleting, n.reqLogger(ctx).Debug(logs.CouldntDeleteObjectFromStorageContinueDeleting, zap.Stringer("cid", bkt.CID),
zap.Stringer("cid", bkt.CID), zap.String("oid", obj.VersionID), zap.Error(obj.Error)) zap.String("oid", obj.VersionID), zap.Error(obj.Error), logs.TagField(logs.TagExternalStorage))
} }
if obj.Error = n.treeService.RemoveVersion(ctx, bkt, nodeVersion.ID); obj.Error != nil { if obj.Error = n.treeService.RemoveVersion(ctx, bkt, nodeVersion.ID); obj.Error != nil {
@ -607,8 +659,8 @@ func (n *Layer) deleteObject(ctx context.Context, bkt *data.BucketInfo, settings
if !client.IsErrObjectAlreadyRemoved(obj.Error) && !client.IsErrObjectNotFound(obj.Error) { if !client.IsErrObjectAlreadyRemoved(obj.Error) && !client.IsErrObjectNotFound(obj.Error) {
return obj return obj
} }
n.reqLogger(ctx).Debug(logs.CouldntDeleteObjectFromStorageContinueDeleting, n.reqLogger(ctx).Debug(logs.CouldntDeleteObjectFromStorageContinueDeleting, zap.Stringer("cid", bkt.CID),
zap.Stringer("cid", bkt.CID), zap.String("oid", obj.VersionID), zap.Error(obj.Error)) zap.String("oid", obj.VersionID), zap.Error(obj.Error), logs.TagField(logs.TagExternalStorage))
} }
} }
@ -723,7 +775,7 @@ func (n *Layer) getNodeVersionsToDelete(ctx context.Context, bkt *data.BucketInf
return nil, fmt.Errorf("%w: there isn't tree node with requested version id", apierr.GetAPIError(apierr.ErrNoSuchVersion)) return nil, fmt.Errorf("%w: there isn't tree node with requested version id", apierr.GetAPIError(apierr.ErrNoSuchVersion))
} }
n.reqLogger(ctx).Debug(logs.GetTreeNodeToDelete, zap.Stringer("cid", bkt.CID), zap.Strings("oids", oids)) n.reqLogger(ctx).Debug(logs.GetTreeNodeToDelete, zap.Stringer("cid", bkt.CID), zap.Strings("oids", oids), logs.TagField(logs.TagDatapath))
return versionsToDelete, nil return versionsToDelete, nil
} }
@ -790,10 +842,13 @@ func (n *Layer) removeCombinedObject(ctx context.Context, bkt *data.BucketInfo,
// DeleteObjects from the storage. // DeleteObjects from the storage.
func (n *Layer) DeleteObjects(ctx context.Context, p *DeleteObjectParams) []*VersionedObject { func (n *Layer) DeleteObjects(ctx context.Context, p *DeleteObjectParams) []*VersionedObject {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.DeleteObjects")
defer span.End()
for i, obj := range p.Objects { for i, obj := range p.Objects {
p.Objects[i] = n.deleteObject(ctx, p.BktInfo, p.Settings, obj, p.NetworkInfo) p.Objects[i] = n.deleteObject(ctx, p.BktInfo, p.Settings, obj, p.NetworkInfo)
if p.IsMultiple && p.Objects[i].Error != nil { if p.IsMultiple && p.Objects[i].Error != nil {
n.reqLogger(ctx).Error(logs.CouldntDeleteObject, zap.String("object", obj.String()), zap.Error(p.Objects[i].Error)) n.reqLogger(ctx).Error(logs.CouldntDeleteObject, zap.String("object", obj.String()), zap.Error(p.Objects[i].Error), logs.TagField(logs.TagExternalStorage))
} }
} }
@ -801,6 +856,9 @@ func (n *Layer) DeleteObjects(ctx context.Context, p *DeleteObjectParams) []*Ver
} }
func (n *Layer) CreateBucket(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) { func (n *Layer) CreateBucket(ctx context.Context, p *CreateBucketParams) (*data.BucketInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.CreateBucket")
defer span.End()
bktInfo, err := n.GetBucketInfo(ctx, p.Name) bktInfo, err := n.GetBucketInfo(ctx, p.Name)
if err != nil { if err != nil {
if apierr.IsS3Error(err, apierr.ErrNoSuchBucket) { if apierr.IsS3Error(err, apierr.ErrNoSuchBucket) {
@ -823,13 +881,16 @@ func (n *Layer) ResolveBucket(ctx context.Context, zone, name string) (cid.ID, e
return cid.ID{}, err return cid.ID{}, err
} }
n.reqLogger(ctx).Info(logs.ResolveBucket, zap.Stringer("cid", cnrID)) n.reqLogger(ctx).Info(logs.ResolveBucket, zap.Stringer("cid", cnrID), logs.TagField(logs.TagDatapath))
} }
return cnrID, nil return cnrID, nil
} }
func (n *Layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error { func (n *Layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.DeleteBucket")
defer span.End()
if !p.SkipCheck { if !p.SkipCheck {
res, _, err := n.getAllObjectsVersions(ctx, commonVersionsListingParams{ res, _, err := n.getAllObjectsVersions(ctx, commonVersionsListingParams{
BktInfo: p.BktInfo, BktInfo: p.BktInfo,
@ -846,14 +907,14 @@ func (n *Layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error {
n.cache.DeleteBucket(p.BktInfo) n.cache.DeleteBucket(p.BktInfo)
corsObj, err := n.treeService.GetBucketCORS(ctx, p.BktInfo) corsObjs, err := n.treeService.GetAllBucketCORS(ctx, p.BktInfo)
if err != nil { if err != nil {
n.reqLogger(ctx).Error(logs.GetBucketCors, zap.Error(err)) n.reqLogger(ctx).Error(logs.GetAllBucketCorsFromTree, zap.Error(err), logs.TagField(logs.TagExternalStorageTree))
} }
lifecycleObj, treeErr := n.treeService.GetBucketLifecycleConfiguration(ctx, p.BktInfo) lifecycleObj, treeErr := n.treeService.GetBucketLifecycleConfiguration(ctx, p.BktInfo)
if treeErr != nil { if treeErr != nil {
n.reqLogger(ctx).Error(logs.GetBucketLifecycle, zap.Error(treeErr)) n.reqLogger(ctx).Error(logs.GetBucketLifecycle, zap.Error(treeErr), logs.TagField(logs.TagExternalStorageTree))
} }
err = n.frostFS.DeleteContainer(ctx, p.BktInfo.CID, p.SessionToken) err = n.frostFS.DeleteContainer(ctx, p.BktInfo.CID, p.SessionToken)
@ -861,10 +922,14 @@ func (n *Layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error {
return fmt.Errorf("delete container: %w", err) return fmt.Errorf("delete container: %w", err)
} }
if !corsObj.Container().Equals(p.BktInfo.CID) && !corsObj.Container().Equals(cid.ID{}) { for _, corsObj := range corsObjs {
n.deleteCORSObject(ctx, p.BktInfo, corsObj) if !corsObj.Container().Equals(p.BktInfo.CID) && !corsObj.Container().Equals(cid.ID{}) {
n.deleteCORSObject(ctx, p.BktInfo, corsObj)
}
} }
n.deleteCORSVersions(ctx, p.BktInfo)
if treeErr == nil && !lifecycleObj.Container().Equals(p.BktInfo.CID) { if treeErr == nil && !lifecycleObj.Container().Equals(p.BktInfo.CID) {
n.deleteLifecycleObject(ctx, p.BktInfo, lifecycleObj) n.deleteLifecycleObject(ctx, p.BktInfo, lifecycleObj)
} }
@ -873,6 +938,9 @@ func (n *Layer) DeleteBucket(ctx context.Context, p *DeleteBucketParams) error {
} }
func (n *Layer) DeleteContainer(ctx context.Context, p *DeleteBucketParams) error { func (n *Layer) DeleteContainer(ctx context.Context, p *DeleteBucketParams) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.DeleteContainer")
defer span.End()
n.cache.DeleteBucket(p.BktInfo) n.cache.DeleteBucket(p.BktInfo)
if err := n.frostFS.DeleteContainer(ctx, p.BktInfo.CID, p.SessionToken); err != nil { if err := n.frostFS.DeleteContainer(ctx, p.BktInfo.CID, p.SessionToken); err != nil {
return fmt.Errorf("delete container: %w", err) return fmt.Errorf("delete container: %w", err)
@ -881,6 +949,9 @@ func (n *Layer) DeleteContainer(ctx context.Context, p *DeleteBucketParams) erro
} }
func (n *Layer) GetNetworkInfo(ctx context.Context) (netmap.NetworkInfo, error) { func (n *Layer) GetNetworkInfo(ctx context.Context) (netmap.NetworkInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetNetworkInfo")
defer span.End()
cachedInfo := n.cache.GetNetworkInfo() cachedInfo := n.cache.GetNetworkInfo()
if cachedInfo != nil { if cachedInfo != nil {
return *cachedInfo, nil return *cachedInfo, nil

View file

@ -7,6 +7,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
@ -23,6 +24,9 @@ type PutBucketLifecycleParams struct {
} }
func (n *Layer) PutBucketLifecycleConfiguration(ctx context.Context, p *PutBucketLifecycleParams) error { func (n *Layer) PutBucketLifecycleConfiguration(ctx context.Context, p *PutBucketLifecycleParams) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.PutBucketLifecycleConfiguration")
defer span.End()
cfgBytes, err := xml.Marshal(p.LifecycleCfg) cfgBytes, err := xml.Marshal(p.LifecycleCfg)
if err != nil { if err != nil {
return fmt.Errorf("marshal lifecycle configuration: %w", err) return fmt.Errorf("marshal lifecycle configuration: %w", err)
@ -79,11 +83,16 @@ func (n *Layer) deleteLifecycleObject(ctx context.Context, bktInfo *data.BucketI
if err := n.objectDeleteWithAuth(ctx, lifecycleBkt, addr.Object(), prmAuth); err != nil { if err := n.objectDeleteWithAuth(ctx, lifecycleBkt, addr.Object(), prmAuth); err != nil {
n.reqLogger(ctx).Error(logs.CouldntDeleteLifecycleObject, zap.Error(err), n.reqLogger(ctx).Error(logs.CouldntDeleteLifecycleObject, zap.Error(err),
zap.String("cid", lifecycleBkt.CID.EncodeToString()), zap.String("cid", lifecycleBkt.CID.EncodeToString()),
zap.String("oid", addr.Object().EncodeToString())) zap.String("oid", addr.Object().EncodeToString()),
logs.TagField(logs.TagExternalStorage),
)
} }
} }
func (n *Layer) GetBucketLifecycleConfiguration(ctx context.Context, bktInfo *data.BucketInfo) (*data.LifecycleConfiguration, error) { func (n *Layer) GetBucketLifecycleConfiguration(ctx context.Context, bktInfo *data.BucketInfo) (*data.LifecycleConfiguration, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetBucketLifecycleConfiguration")
defer span.End()
owner := n.BearerOwner(ctx) owner := n.BearerOwner(ctx)
if cfg := n.cache.GetLifecycleConfiguration(owner, bktInfo); cfg != nil { if cfg := n.cache.GetLifecycleConfiguration(owner, bktInfo); cfg != nil {
return cfg, nil return cfg, nil
@ -129,6 +138,9 @@ func (n *Layer) GetBucketLifecycleConfiguration(ctx context.Context, bktInfo *da
} }
func (n *Layer) DeleteBucketLifecycleConfiguration(ctx context.Context, bktInfo *data.BucketInfo) error { func (n *Layer) DeleteBucketLifecycleConfiguration(ctx context.Context, bktInfo *data.BucketInfo) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.DeleteBucketLifecycleConfiguration")
defer span.End()
objs, err := n.treeService.DeleteBucketLifecycleConfiguration(ctx, bktInfo) objs, err := n.treeService.DeleteBucketLifecycleConfiguration(ctx, bktInfo)
objsNotFound := errors.Is(err, tree.ErrNoNodeToRemove) objsNotFound := errors.Is(err, tree.ErrNoNodeToRemove)
if err != nil && !objsNotFound { if err != nil && !objsNotFound {

View file

@ -9,6 +9,7 @@ import (
"strings" "strings"
"sync" "sync"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -26,6 +27,7 @@ type (
Encode string Encode string
MaxKeys int MaxKeys int
Prefix string Prefix string
Chan chan<- struct{}
} }
// ListObjectsParamsV1 contains params for ListObjectsV1. // ListObjectsParamsV1 contains params for ListObjectsV1.
@ -80,6 +82,8 @@ type (
MaxKeys int MaxKeys int
Marker string Marker string
Bookmark string Bookmark string
// Chan is a channel to prevent client from context canceling during long listing.
Chan chan<- struct{}
} }
commonLatestVersionsListingParams struct { commonLatestVersionsListingParams struct {
@ -97,6 +101,9 @@ const (
// ListObjectsV1 returns objects in a bucket for requests of Version 1. // ListObjectsV1 returns objects in a bucket for requests of Version 1.
func (n *Layer) ListObjectsV1(ctx context.Context, p *ListObjectsParamsV1) (*ListObjectsInfoV1, error) { func (n *Layer) ListObjectsV1(ctx context.Context, p *ListObjectsParamsV1) (*ListObjectsInfoV1, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.ListObjectsV1")
defer span.End()
var result ListObjectsInfoV1 var result ListObjectsInfoV1
prm := commonLatestVersionsListingParams{ prm := commonLatestVersionsListingParams{
@ -107,6 +114,7 @@ func (n *Layer) ListObjectsV1(ctx context.Context, p *ListObjectsParamsV1) (*Lis
MaxKeys: p.MaxKeys, MaxKeys: p.MaxKeys,
Marker: p.Marker, Marker: p.Marker,
Bookmark: p.Marker, Bookmark: p.Marker,
Chan: p.Chan,
}, },
ListType: ListObjectsV1Type, ListType: ListObjectsV1Type,
} }
@ -128,6 +136,9 @@ func (n *Layer) ListObjectsV1(ctx context.Context, p *ListObjectsParamsV1) (*Lis
// ListObjectsV2 returns objects in a bucket for requests of Version 2. // ListObjectsV2 returns objects in a bucket for requests of Version 2.
func (n *Layer) ListObjectsV2(ctx context.Context, p *ListObjectsParamsV2) (*ListObjectsInfoV2, error) { func (n *Layer) ListObjectsV2(ctx context.Context, p *ListObjectsParamsV2) (*ListObjectsInfoV2, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.ListObjectsV2")
defer span.End()
var result ListObjectsInfoV2 var result ListObjectsInfoV2
prm := commonLatestVersionsListingParams{ prm := commonLatestVersionsListingParams{
@ -138,6 +149,7 @@ func (n *Layer) ListObjectsV2(ctx context.Context, p *ListObjectsParamsV2) (*Lis
MaxKeys: p.MaxKeys, MaxKeys: p.MaxKeys,
Marker: p.StartAfter, Marker: p.StartAfter,
Bookmark: p.ContinuationToken, Bookmark: p.ContinuationToken,
Chan: p.Chan,
}, },
ListType: ListObjectsV2Type, ListType: ListObjectsV2Type,
} }
@ -158,6 +170,9 @@ func (n *Layer) ListObjectsV2(ctx context.Context, p *ListObjectsParamsV2) (*Lis
} }
func (n *Layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) { func (n *Layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsParams) (*ListObjectVersionsInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.ListObjectVersions")
defer span.End()
prm := commonVersionsListingParams{ prm := commonVersionsListingParams{
BktInfo: p.BktInfo, BktInfo: p.BktInfo,
Delimiter: p.Delimiter, Delimiter: p.Delimiter,
@ -165,6 +180,7 @@ func (n *Layer) ListObjectVersions(ctx context.Context, p *ListObjectVersionsPar
MaxKeys: p.MaxKeys, MaxKeys: p.MaxKeys,
Marker: p.KeyMarker, Marker: p.KeyMarker,
Bookmark: p.VersionIDMarker, Bookmark: p.VersionIDMarker,
Chan: p.Chan,
} }
objects, isTruncated, err := n.getAllObjectsVersions(ctx, prm) objects, isTruncated, err := n.getAllObjectsVersions(ctx, prm)
@ -208,6 +224,10 @@ func (n *Layer) getLatestObjectsVersions(ctx context.Context, p commonLatestVers
objects = append(objects, session.Next...) objects = append(objects, session.Next...)
for obj := range objOutCh { for obj := range objOutCh {
objects = append(objects, obj) objects = append(objects, obj)
select {
case p.Chan <- struct{}{}:
default:
}
} }
if err = <-errorCh; err != nil { if err = <-errorCh; err != nil {
@ -277,6 +297,11 @@ func handleGeneratedVersions(objOutCh <-chan *data.ExtendedNodeVersion, p common
allObjects = append(allObjects, eoi) allObjects = append(allObjects, eoi)
} }
lastName = name lastName = name
select {
case p.Chan <- struct{}{}:
default:
}
} }
formVersionsListRow(allObjects, listRowStartIndex, session) formVersionsListRow(allObjects, listRowStartIndex, session)
@ -541,7 +566,7 @@ func (n *Layer) initWorkerPool(ctx context.Context, size int, p commonVersionsLi
realSize, err := GetObjectSize(oi) realSize, err := GetObjectSize(oi)
if err != nil { if err != nil {
reqLog.Debug(logs.FailedToGetRealObjectSize, zap.Error(err)) reqLog.Debug(logs.FailedToGetRealObjectSize, zap.Error(err), logs.TagField(logs.TagDatapath))
realSize = oi.Size realSize = oi.Size
} }
@ -554,7 +579,7 @@ func (n *Layer) initWorkerPool(ctx context.Context, size int, p commonVersionsLi
}) })
if err != nil { if err != nil {
wg.Done() wg.Done()
reqLog.Warn(logs.FailedToSubmitTaskToPool, zap.Error(err)) reqLog.Warn(logs.FailedToSubmitTaskToPool, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
}(node) }(node)
} }
@ -645,7 +670,7 @@ func (n *Layer) objectInfoFromObjectsCacheOrFrostFS(ctx context.Context, bktInfo
meta, err := n.objectHead(ctx, bktInfo, node.OID) meta, err := n.objectHead(ctx, bktInfo, node.OID)
if err != nil { if err != nil {
n.reqLogger(ctx).Warn(logs.CouldNotFetchObjectMeta, zap.Error(err)) n.reqLogger(ctx).Warn(logs.CouldNotFetchObjectMeta, zap.Error(err), logs.TagField(logs.TagExternalStorage))
return nil return nil
} }

View file

@ -15,6 +15,7 @@ import (
"strings" "strings"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
@ -58,10 +59,9 @@ type (
} }
CreateMultipartParams struct { CreateMultipartParams struct {
Info *UploadInfoParams Info *UploadInfoParams
Header map[string]string Header map[string]string
Data *UploadData Data *UploadData
CopiesNumbers []uint32
} }
UploadData struct { UploadData struct {
@ -75,6 +75,7 @@ type (
Reader io.Reader Reader io.Reader
ContentMD5 string ContentMD5 string
ContentSHA256Hash string ContentSHA256Hash string
CopiesNumbers []uint32
} }
UploadCopyParams struct { UploadCopyParams struct {
@ -85,11 +86,13 @@ type (
SrcEncryption encryption.Params SrcEncryption encryption.Params
PartNumber int PartNumber int
Range *RangeParams Range *RangeParams
CopiesNumbers []uint32
} }
CompleteMultipartParams struct { CompleteMultipartParams struct {
Info *UploadInfoParams Info *UploadInfoParams
Parts []*CompletedPart Parts []*CompletedPart
CopiesNumbers []uint32
} }
CompletedPart struct { CompletedPart struct {
@ -149,6 +152,9 @@ type (
) )
func (n *Layer) CreateMultipartUpload(ctx context.Context, p *CreateMultipartParams) error { func (n *Layer) CreateMultipartUpload(ctx context.Context, p *CreateMultipartParams) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.CreateMultipartUpload")
defer span.End()
metaSize := len(p.Header) metaSize := len(p.Header)
if p.Data != nil { if p.Data != nil {
metaSize += len(p.Data.TagSet) metaSize += len(p.Data.TagSet)
@ -165,7 +171,6 @@ func (n *Layer) CreateMultipartUpload(ctx context.Context, p *CreateMultipartPar
Owner: n.gateOwner, Owner: n.gateOwner,
Created: TimeNow(ctx), Created: TimeNow(ctx),
Meta: make(map[string]string, metaSize), Meta: make(map[string]string, metaSize),
CopiesNumbers: p.CopiesNumbers,
CreationEpoch: networkInfo.CurrentEpoch(), CreationEpoch: networkInfo.CurrentEpoch(),
} }
@ -189,6 +194,9 @@ func (n *Layer) CreateMultipartUpload(ctx context.Context, p *CreateMultipartPar
} }
func (n *Layer) UploadPart(ctx context.Context, p *UploadPartParams) (string, error) { func (n *Layer) UploadPart(ctx context.Context, p *UploadPartParams) (string, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.UploadPart")
defer span.End()
multipartInfo, err := n.treeService.GetMultipartUpload(ctx, p.Info.Bkt, p.Info.Key, p.Info.UploadID) multipartInfo, err := n.treeService.GetMultipartUpload(ctx, p.Info.Bkt, p.Info.Key, p.Info.UploadID)
if err != nil { if err != nil {
if errors.Is(err, tree.ErrNodeNotFound) { if errors.Is(err, tree.ErrNodeNotFound) {
@ -212,7 +220,7 @@ func (n *Layer) UploadPart(ctx context.Context, p *UploadPartParams) (string, er
func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInfo, p *UploadPartParams) (*data.ObjectInfo, error) { func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInfo, p *UploadPartParams) (*data.ObjectInfo, error) {
encInfo := FormEncryptionInfo(multipartInfo.Meta) encInfo := FormEncryptionInfo(multipartInfo.Meta)
if err := p.Info.Encryption.MatchObjectEncryption(encInfo); err != nil { if err := p.Info.Encryption.MatchObjectEncryption(encInfo); err != nil {
n.reqLogger(ctx).Warn(logs.MismatchedObjEncryptionInfo, zap.Error(err)) n.reqLogger(ctx).Warn(logs.MismatchedObjEncryptionInfo, zap.Error(err), logs.TagField(logs.TagDatapath))
return nil, apierr.GetAPIError(apierr.ErrInvalidEncryptionParameters) return nil, apierr.GetAPIError(apierr.ErrInvalidEncryptionParameters)
} }
@ -222,7 +230,7 @@ func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
Attributes: make([][2]string, 2), Attributes: make([][2]string, 2),
Payload: p.Reader, Payload: p.Reader,
CreationTime: TimeNow(ctx), CreationTime: TimeNow(ctx),
CopiesNumber: multipartInfo.CopiesNumbers, CopiesNumber: p.CopiesNumbers,
} }
decSize := p.Size decSize := p.Size
@ -265,7 +273,10 @@ func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
n.prepareAuthParameters(ctx, &prm.PrmAuth, bktInfo.Owner) n.prepareAuthParameters(ctx, &prm.PrmAuth, bktInfo.Owner)
err = n.frostFS.DeleteObject(ctx, prm) err = n.frostFS.DeleteObject(ctx, prm)
if err != nil { if err != nil {
n.reqLogger(ctx).Debug(logs.FailedToDeleteObject, zap.Stringer("cid", bktInfo.CID), zap.Stringer("oid", createdObj.ID)) n.reqLogger(ctx).Debug(logs.FailedToDeleteObject,
zap.Stringer("cid", bktInfo.CID),
zap.Stringer("oid", createdObj.ID),
logs.TagField(logs.TagExternalStorage))
} }
return nil, apierr.GetAPIError(apierr.ErrInvalidDigest) return nil, apierr.GetAPIError(apierr.ErrInvalidDigest)
} }
@ -282,7 +293,10 @@ func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
if !bytes.Equal(contentHashBytes, createdObj.HashSum) { if !bytes.Equal(contentHashBytes, createdObj.HashSum) {
err = n.objectDelete(ctx, bktInfo, createdObj.ID) err = n.objectDelete(ctx, bktInfo, createdObj.ID)
if err != nil { if err != nil {
n.reqLogger(ctx).Debug(logs.FailedToDeleteObject, zap.Stringer("cid", bktInfo.CID), zap.Stringer("oid", createdObj.ID)) n.reqLogger(ctx).Debug(logs.FailedToDeleteObject,
zap.Stringer("cid", bktInfo.CID),
zap.Stringer("oid", createdObj.ID),
logs.TagField(logs.TagExternalStorage))
} }
return nil, apierr.GetAPIError(apierr.ErrContentSHA256Mismatch) return nil, apierr.GetAPIError(apierr.ErrContentSHA256Mismatch)
} }
@ -290,7 +304,7 @@ func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
n.reqLogger(ctx).Debug(logs.UploadPart, n.reqLogger(ctx).Debug(logs.UploadPart,
zap.String("multipart upload", p.Info.UploadID), zap.Int("part number", p.PartNumber), zap.String("multipart upload", p.Info.UploadID), zap.Int("part number", p.PartNumber),
zap.Stringer("cid", bktInfo.CID), zap.Stringer("oid", createdObj.ID)) zap.Stringer("cid", bktInfo.CID), zap.Stringer("oid", createdObj.ID), logs.TagField(logs.TagDatapath))
partInfo := &data.PartInfo{ partInfo := &data.PartInfo{
Key: p.Info.Key, Key: p.Info.Key,
@ -313,7 +327,8 @@ func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
if err = n.objectDelete(ctx, bktInfo, oldPartID); err != nil { if err = n.objectDelete(ctx, bktInfo, oldPartID); err != nil {
n.reqLogger(ctx).Error(logs.CouldntDeleteOldPartObject, zap.Error(err), n.reqLogger(ctx).Error(logs.CouldntDeleteOldPartObject, zap.Error(err),
zap.String("cid", bktInfo.CID.EncodeToString()), zap.String("cid", bktInfo.CID.EncodeToString()),
zap.String("oid", oldPartID.EncodeToString())) zap.String("oid", oldPartID.EncodeToString()),
logs.TagField(logs.TagExternalStorage))
} }
} }
} }
@ -334,6 +349,9 @@ func (n *Layer) uploadPart(ctx context.Context, multipartInfo *data.MultipartInf
} }
func (n *Layer) UploadPartCopy(ctx context.Context, p *UploadCopyParams) (*data.ObjectInfo, error) { func (n *Layer) UploadPartCopy(ctx context.Context, p *UploadCopyParams) (*data.ObjectInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.UploadPartCopy")
defer span.End()
multipartInfo, err := n.treeService.GetMultipartUpload(ctx, p.Info.Bkt, p.Info.Key, p.Info.UploadID) multipartInfo, err := n.treeService.GetMultipartUpload(ctx, p.Info.Bkt, p.Info.Key, p.Info.UploadID)
if err != nil { if err != nil {
if errors.Is(err, tree.ErrNodeNotFound) { if errors.Is(err, tree.ErrNodeNotFound) {
@ -372,16 +390,20 @@ func (n *Layer) UploadPartCopy(ctx context.Context, p *UploadCopyParams) (*data.
} }
params := &UploadPartParams{ params := &UploadPartParams{
Info: p.Info, Info: p.Info,
PartNumber: p.PartNumber, PartNumber: p.PartNumber,
Size: size, Size: size,
Reader: objPayload, Reader: objPayload,
CopiesNumbers: p.CopiesNumbers,
} }
return n.uploadPart(ctx, multipartInfo, params) return n.uploadPart(ctx, multipartInfo, params)
} }
func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipartParams) (*UploadData, *data.ExtendedObjectInfo, error) { func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipartParams) (*UploadData, *data.ExtendedObjectInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.CompleteMultipartUpload")
defer span.End()
for i := 1; i < len(p.Parts); i++ { for i := 1; i < len(p.Parts); i++ {
if p.Parts[i].PartNumber <= p.Parts[i-1].PartNumber { if p.Parts[i].PartNumber <= p.Parts[i-1].PartNumber {
return nil, nil, apierr.GetAPIError(apierr.ErrInvalidPartOrder) return nil, nil, apierr.GetAPIError(apierr.ErrInvalidPartOrder)
@ -472,14 +494,15 @@ func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar
Header: initMetadata, Header: initMetadata,
Size: &multipartObjetSize, Size: &multipartObjetSize,
Encryption: p.Info.Encryption, Encryption: p.Info.Encryption,
CopiesNumbers: multipartInfo.CopiesNumbers, CopiesNumbers: p.CopiesNumbers,
CompleteMD5Hash: hex.EncodeToString(md5Hash.Sum(nil)) + "-" + strconv.Itoa(len(p.Parts)), CompleteMD5Hash: hex.EncodeToString(md5Hash.Sum(nil)) + "-" + strconv.Itoa(len(p.Parts)),
}) })
if err != nil { if err != nil {
n.reqLogger(ctx).Error(logs.CouldNotPutCompletedObject, n.reqLogger(ctx).Error(logs.CouldNotPutCompletedObject,
zap.String("uploadID", p.Info.UploadID), zap.String("uploadID", p.Info.UploadID),
zap.String("uploadKey", p.Info.Key), zap.String("uploadKey", p.Info.Key),
zap.Error(err)) zap.Error(err),
logs.TagField(logs.TagExternalStorage))
return nil, nil, apierr.GetAPIError(apierr.ErrInternalError) return nil, nil, apierr.GetAPIError(apierr.ErrInternalError)
} }
@ -491,7 +514,8 @@ func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar
if err = n.objectDelete(ctx, p.Info.Bkt, partInfo.OID); err != nil { if err = n.objectDelete(ctx, p.Info.Bkt, partInfo.OID); err != nil {
n.reqLogger(ctx).Warn(logs.CouldNotDeleteUploadPart, n.reqLogger(ctx).Warn(logs.CouldNotDeleteUploadPart,
zap.Stringer("cid", p.Info.Bkt.CID), zap.Stringer("oid", &partInfo.OID), zap.Stringer("cid", p.Info.Bkt.CID), zap.Stringer("oid", &partInfo.OID),
zap.Error(err)) zap.Error(err),
logs.TagField(logs.TagExternalStorage))
} }
addr.SetObject(partInfo.OID) addr.SetObject(partInfo.OID)
n.cache.DeleteObject(addr) n.cache.DeleteObject(addr)
@ -502,6 +526,9 @@ func (n *Layer) CompleteMultipartUpload(ctx context.Context, p *CompleteMultipar
} }
func (n *Layer) ListMultipartUploads(ctx context.Context, p *ListMultipartUploadsParams) (*ListMultipartUploadsInfo, error) { func (n *Layer) ListMultipartUploads(ctx context.Context, p *ListMultipartUploadsParams) (*ListMultipartUploadsInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.ListMultipartUploads")
defer span.End()
var result ListMultipartUploadsInfo var result ListMultipartUploadsInfo
if p.MaxUploads == 0 { if p.MaxUploads == 0 {
return &result, nil return &result, nil
@ -562,6 +589,9 @@ func (n *Layer) ListMultipartUploads(ctx context.Context, p *ListMultipartUpload
} }
func (n *Layer) AbortMultipartUpload(ctx context.Context, p *UploadInfoParams) error { func (n *Layer) AbortMultipartUpload(ctx context.Context, p *UploadInfoParams) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.AbortMultipartUpload")
defer span.End()
multipartInfo, parts, err := n.getUploadParts(ctx, p) multipartInfo, parts, err := n.getUploadParts(ctx, p)
if err != nil { if err != nil {
return err return err
@ -585,7 +615,7 @@ func (n *Layer) deleteUploadedParts(ctx context.Context, bkt *data.BucketInfo, p
oids, err := relations.ListAllRelations(ctx, n.frostFS.Relations(), bkt.CID, info.OID, tokens) oids, err := relations.ListAllRelations(ctx, n.frostFS.Relations(), bkt.CID, info.OID, tokens)
if err != nil { if err != nil {
n.reqLogger(ctx).Warn(logs.FailedToListAllObjectRelations, zap.String("cid", bkt.CID.EncodeToString()), n.reqLogger(ctx).Warn(logs.FailedToListAllObjectRelations, zap.String("cid", bkt.CID.EncodeToString()),
zap.String("oid", info.OID.EncodeToString()), zap.Error(err)) zap.String("oid", info.OID.EncodeToString()), zap.Error(err), logs.TagField(logs.TagExternalStorage))
continue continue
} }
members = append(members, append(oids, info.OID)...) members = append(members, append(oids, info.OID)...)
@ -594,11 +624,14 @@ func (n *Layer) deleteUploadedParts(ctx context.Context, bkt *data.BucketInfo, p
err := n.putTombstones(ctx, bkt, networkInfo, members) err := n.putTombstones(ctx, bkt, networkInfo, members)
if err != nil { if err != nil {
n.reqLogger(ctx).Warn(logs.FailedToPutTombstones, zap.Error(err)) n.reqLogger(ctx).Warn(logs.FailedToPutTombstones, zap.Error(err), logs.TagField(logs.TagExternalStorage))
} }
} }
func (n *Layer) ListParts(ctx context.Context, p *ListPartsParams) (*ListPartsInfo, error) { func (n *Layer) ListParts(ctx context.Context, p *ListPartsParams) (*ListPartsInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.ListParts")
defer span.End()
var res ListPartsInfo var res ListPartsInfo
multipartInfo, partsInfo, err := n.getUploadParts(ctx, p.Info) multipartInfo, partsInfo, err := n.getUploadParts(ctx, p.Info)
if err != nil { if err != nil {
@ -607,7 +640,7 @@ func (n *Layer) ListParts(ctx context.Context, p *ListPartsParams) (*ListPartsIn
encInfo := FormEncryptionInfo(multipartInfo.Meta) encInfo := FormEncryptionInfo(multipartInfo.Meta)
if err = p.Info.Encryption.MatchObjectEncryption(encInfo); err != nil { if err = p.Info.Encryption.MatchObjectEncryption(encInfo); err != nil {
n.reqLogger(ctx).Warn(logs.MismatchedObjEncryptionInfo, zap.Error(err)) n.reqLogger(ctx).Warn(logs.MismatchedObjEncryptionInfo, zap.Error(err), logs.TagField(logs.TagDatapath))
return nil, apierr.GetAPIError(apierr.ErrInvalidEncryptionParameters) return nil, apierr.GetAPIError(apierr.ErrInvalidEncryptionParameters)
} }
@ -699,7 +732,8 @@ func (n *Layer) getUploadParts(ctx context.Context, p *UploadInfoParams) (*data.
zap.Stringer("cid", p.Bkt.CID), zap.Stringer("cid", p.Bkt.CID),
zap.String("upload id", p.UploadID), zap.String("upload id", p.UploadID),
zap.Ints("part numbers", partsNumbers), zap.Ints("part numbers", partsNumbers),
zap.Strings("oids", oids)) zap.Strings("oids", oids),
logs.TagField(logs.TagDatapath))
return multipartInfo, res, nil return multipartInfo, res, nil
} }

View file

@ -17,6 +17,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
@ -227,6 +228,9 @@ func ParseCompletedPartHeader(hdr string) (*Part, error) {
// PutObject stores object into FrostFS, took payload from io.Reader. // PutObject stores object into FrostFS, took payload from io.Reader.
func (n *Layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.ExtendedObjectInfo, error) { func (n *Layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.ExtendedObjectInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.PutObject")
defer span.End()
bktSettings, err := n.GetBucketSettings(ctx, p.BktInfo) bktSettings, err := n.GetBucketSettings(ctx, p.BktInfo)
if err != nil { if err != nil {
return nil, fmt.Errorf("couldn't get versioning settings object: %w", err) return nil, fmt.Errorf("couldn't get versioning settings object: %w", err)
@ -295,7 +299,11 @@ func (n *Layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.Extend
if !bytes.Equal(headerMd5Hash, createdObj.MD5Sum) { if !bytes.Equal(headerMd5Hash, createdObj.MD5Sum) {
err = n.objectDelete(ctx, p.BktInfo, createdObj.ID) err = n.objectDelete(ctx, p.BktInfo, createdObj.ID)
if err != nil { if err != nil {
n.reqLogger(ctx).Debug(logs.FailedToDeleteObject, zap.Stringer("cid", p.BktInfo.CID), zap.Stringer("oid", createdObj.ID)) n.reqLogger(ctx).Debug(logs.FailedToDeleteObject,
zap.Stringer("cid", p.BktInfo.CID),
zap.Stringer("oid", createdObj.ID),
logs.TagField(logs.TagExternalStorage),
)
} }
return nil, apierr.GetAPIError(apierr.ErrBadDigest) return nil, apierr.GetAPIError(apierr.ErrBadDigest)
} }
@ -309,13 +317,17 @@ func (n *Layer) PutObject(ctx context.Context, p *PutObjectParams) (*data.Extend
if !bytes.Equal(contentHashBytes, createdObj.HashSum) { if !bytes.Equal(contentHashBytes, createdObj.HashSum) {
err = n.objectDelete(ctx, p.BktInfo, createdObj.ID) err = n.objectDelete(ctx, p.BktInfo, createdObj.ID)
if err != nil { if err != nil {
n.reqLogger(ctx).Debug(logs.FailedToDeleteObject, zap.Stringer("cid", p.BktInfo.CID), zap.Stringer("oid", createdObj.ID)) n.reqLogger(ctx).Debug(logs.FailedToDeleteObject,
zap.Stringer("cid", p.BktInfo.CID),
zap.Stringer("oid", createdObj.ID),
logs.TagField(logs.TagExternalStorage))
} }
return nil, apierr.GetAPIError(apierr.ErrContentSHA256Mismatch) return nil, apierr.GetAPIError(apierr.ErrContentSHA256Mismatch)
} }
} }
n.reqLogger(ctx).Debug(logs.PutObject, zap.Stringer("cid", p.BktInfo.CID), zap.Stringer("oid", createdObj.ID)) n.reqLogger(ctx).Debug(logs.PutObject, zap.Stringer("cid", p.BktInfo.CID),
zap.Stringer("oid", createdObj.ID), logs.TagField(logs.TagExternalStorage))
now := TimeNow(ctx) now := TimeNow(ctx)
newVersion := &data.NodeVersion{ newVersion := &data.NodeVersion{
BaseNodeVersion: data.BaseNodeVersion{ BaseNodeVersion: data.BaseNodeVersion{
@ -410,7 +422,7 @@ func (n *Layer) headLastVersionIfNotDeleted(ctx context.Context, bkt *data.Bucke
meta, err := n.objectHead(ctx, bkt, node.OID) meta, err := n.objectHead(ctx, bkt, node.OID)
if err != nil { if err != nil {
if client.IsErrObjectNotFound(err) { if client.IsErrObjectNotFound(err) || client.IsErrObjectAlreadyRemoved(err) {
return nil, fmt.Errorf("%w: %s; %s", apierr.GetAPIError(apierr.ErrNoSuchKey), err.Error(), node.OID.EncodeToString()) return nil, fmt.Errorf("%w: %s; %s", apierr.GetAPIError(apierr.ErrNoSuchKey), err.Error(), node.OID.EncodeToString())
} }
return nil, err return nil, err
@ -467,7 +479,7 @@ func (n *Layer) headVersion(ctx context.Context, bkt *data.BucketInfo, p *HeadOb
meta, err := n.objectHead(ctx, bkt, foundVersion.OID) meta, err := n.objectHead(ctx, bkt, foundVersion.OID)
if err != nil { if err != nil {
if client.IsErrObjectNotFound(err) { if client.IsErrObjectNotFound(err) || client.IsErrObjectAlreadyRemoved(err) {
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchVersion), err.Error()) return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchVersion), err.Error())
} }
return nil, err return nil, err
@ -525,10 +537,7 @@ func (n *Layer) objectPutAndHash(ctx context.Context, prm frostfs.PrmObjectCreat
}) })
res, err := n.frostFS.CreateObject(ctx, prm) res, err := n.frostFS.CreateObject(ctx, prm)
if err != nil { if err != nil {
if _, errDiscard := io.Copy(io.Discard, prm.Payload); errDiscard != nil { n.payloadDiscard(ctx, prm.Payload)
n.reqLogger(ctx).Warn(logs.FailedToDiscardPutPayloadProbablyGoroutineLeaks, zap.Error(errDiscard))
}
return nil, err return nil, err
} }
return &data.CreatedObjectInfo{ return &data.CreatedObjectInfo{
@ -540,12 +549,20 @@ func (n *Layer) objectPutAndHash(ctx context.Context, prm frostfs.PrmObjectCreat
}, nil }, nil
} }
func (n *Layer) payloadDiscard(ctx context.Context, payload io.Reader) {
if payload != nil {
if _, errDiscard := io.Copy(io.Discard, payload); errDiscard != nil {
n.reqLogger(ctx).Warn(logs.FailedToDiscardPutPayloadProbablyGoroutineLeaks, zap.Error(errDiscard), logs.TagField(logs.TagDatapath))
}
}
}
type logWrapper struct { type logWrapper struct {
log *zap.Logger log *zap.Logger
} }
func (l *logWrapper) Printf(format string, args ...interface{}) { func (l *logWrapper) Printf(format string, args ...interface{}) {
l.log.Info(fmt.Sprintf(format, args...)) l.log.Info(fmt.Sprintf(format, args...), logs.TagField(logs.TagDatapath))
} }
func IsSystemHeader(key string) bool { func IsSystemHeader(key string) bool {

View file

@ -49,3 +49,16 @@ func TestGoroutinesDontLeakInPutAndHash(t *testing.T) {
require.ErrorIs(t, err, expErr) require.ErrorIs(t, err, expErr)
require.Empty(t, payload.Len(), "body must be read out otherwise goroutines can leak in wrapReader") require.Empty(t, payload.Len(), "body must be read out otherwise goroutines can leak in wrapReader")
} }
func TestNilPayloadPutAndHash(t *testing.T) {
tc := prepareContext(t)
prm := frostfs.PrmObjectCreate{
Filepath: tc.obj,
Payload: nil,
}
expErr := errors.New("some error")
tc.testFrostFS.SetObjectPutError(tc.obj, expErr)
_, err := tc.layer.objectPutAndHash(tc.ctx, prm, tc.bktInfo)
require.ErrorIs(t, err, expErr)
}

View file

@ -10,6 +10,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
@ -25,6 +26,9 @@ type PatchObjectParams struct {
} }
func (n *Layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.ExtendedObjectInfo, error) { func (n *Layer) PatchObject(ctx context.Context, p *PatchObjectParams) (*data.ExtendedObjectInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.PatchObject")
defer span.End()
if p.Object.ObjectInfo.Headers[AttributeDecryptedSize] != "" { if p.Object.ObjectInfo.Headers[AttributeDecryptedSize] != "" {
return nil, fmt.Errorf("patch encrypted object") return nil, fmt.Errorf("patch encrypted object")
} }

View file

@ -10,13 +10,16 @@ import (
"strconv" "strconv"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/crdt"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object" apiobject "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/api/object"
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id" cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id" oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
) )
@ -32,6 +35,9 @@ type PutLockInfoParams struct {
} }
func (n *Layer) PutLockInfo(ctx context.Context, p *PutLockInfoParams) (err error) { func (n *Layer) PutLockInfo(ctx context.Context, p *PutLockInfoParams) (err error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.PutLockInfo")
defer span.End()
newLock := p.NewLock newLock := p.NewLock
versionNode := p.NodeVersion versionNode := p.NodeVersion
// sometimes node version can be provided from executing context // sometimes node version can be provided from executing context
@ -139,6 +145,9 @@ func (n *Layer) putLockObject(ctx context.Context, bktInfo *data.BucketInfo, obj
} }
func (n *Layer) GetLockInfo(ctx context.Context, objVersion *data.ObjectVersion) (*data.LockInfo, error) { func (n *Layer) GetLockInfo(ctx context.Context, objVersion *data.ObjectVersion) (*data.LockInfo, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetLockInfo")
defer span.End()
owner := n.BearerOwner(ctx) owner := n.BearerOwner(ctx)
if lockInfo := n.cache.GetLockInfo(owner, lockObjectKey(objVersion)); lockInfo != nil { if lockInfo := n.cache.GetLockInfo(owner, lockObjectKey(objVersion)); lockInfo != nil {
return lockInfo, nil return lockInfo, nil
@ -168,24 +177,42 @@ func (n *Layer) getCORS(ctx context.Context, bkt *data.BucketInfo, decoder func(
return cors, nil return cors, nil
} }
addr, err := n.treeService.GetBucketCORS(ctx, bkt) corsVersions, err := n.getCORSVersions(ctx, bkt)
objNotFound := errors.Is(err, tree.ErrNodeNotFound) if err != nil {
if err != nil && !objNotFound {
return nil, err return nil, err
} }
if objNotFound { var (
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchCORSConfiguration), err.Error()) prmAuth frostfs.PrmAuth
} objID oid.ID
corsBkt = bkt
lastCORS = corsVersions.GetLast()
)
var prmAuth frostfs.PrmAuth if lastCORS != nil {
corsBkt := bkt
if !addr.Container().Equals(bkt.CID) && !addr.Container().Equals(cid.ID{}) {
corsBkt = &data.BucketInfo{CID: addr.Container()}
prmAuth.PrivateKey = &n.gateKey.PrivateKey prmAuth.PrivateKey = &n.gateKey.PrivateKey
corsBkt = n.corsCnrInfo
objID = lastCORS.ObjID
} else {
addr, err := n.treeService.GetBucketCORS(ctx, bkt)
objNotFound := errors.Is(err, tree.ErrNodeNotFound)
if err != nil && !objNotFound {
return nil, err
}
if objNotFound {
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrNoSuchCORSConfiguration), err.Error())
}
if !addr.Container().Equals(bkt.CID) && !addr.Container().Equals(cid.ID{}) {
corsBkt = &data.BucketInfo{CID: addr.Container()}
prmAuth.PrivateKey = &n.gateKey.PrivateKey
}
objID = addr.Object()
} }
obj, err := n.objectGetWithAuth(ctx, corsBkt, addr.Object(), prmAuth) obj, err := n.objectGetWithAuth(ctx, corsBkt, objID, prmAuth)
if err != nil { if err != nil {
return nil, fmt.Errorf("get cors object: %w", err) return nil, fmt.Errorf("get cors object: %w", err)
} }
@ -200,12 +227,65 @@ func (n *Layer) getCORS(ctx context.Context, bkt *data.BucketInfo, decoder func(
return cors, nil return cors, nil
} }
func (n *Layer) getCORSVersions(ctx context.Context, bkt *data.BucketInfo) (*crdt.ObjectVersions, error) {
corsVersions, err := n.frostFS.SearchObjects(ctx, frostfs.PrmObjectSearch{
Container: n.corsCnrInfo.CID,
ExactAttribute: [2]string{object.AttributeFilePath, bkt.CORSObjectFilePath()},
})
if err != nil {
return nil, fmt.Errorf("search cors objects: %w", err)
}
versions := crdt.NewObjectVersions(bkt.CORSObjectFilePath())
versions.SetLessFunc(func(ov1, ov2 *crdt.ObjectVersion) bool {
versionID1, versionID2 := ov1.VersionID(), ov2.VersionID()
timestamp1, timestamp2 := ov1.Headers[object.AttributeTimestamp], ov2.Headers[object.AttributeTimestamp]
if ov1.CreationEpoch != ov2.CreationEpoch {
return ov1.CreationEpoch < ov2.CreationEpoch
}
if len(timestamp1) > 0 && len(timestamp2) > 0 && timestamp1 != timestamp2 {
unixTime1, err := strconv.ParseInt(timestamp1, 10, 64)
if err != nil {
return versionID1 < versionID2
}
unixTime2, err := strconv.ParseInt(timestamp2, 10, 64)
if err != nil {
return versionID1 < versionID2
}
return unixTime1 < unixTime2
}
return versionID1 < versionID2
})
for _, id := range corsVersions {
objVersion, err := n.frostFS.HeadObject(ctx, frostfs.PrmObjectHead{
Container: n.corsCnrInfo.CID,
Object: id,
})
if err != nil {
return nil, fmt.Errorf("head cors object '%s': %w", id.EncodeToString(), err)
}
versions.AppendVersion(crdt.NewObjectVersion(objVersion))
}
return versions, nil
}
func lockObjectKey(objVersion *data.ObjectVersion) string { func lockObjectKey(objVersion *data.ObjectVersion) string {
// todo reconsider forming name since versionID can be "null" or "" // todo reconsider forming name since versionID can be "null" or ""
return ".lock." + objVersion.BktInfo.CID.EncodeToString() + "." + objVersion.ObjectName + "." + objVersion.VersionID return ".lock." + objVersion.BktInfo.CID.EncodeToString() + "." + objVersion.ObjectName + "." + objVersion.VersionID
} }
func (n *Layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) { func (n *Layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetBucketSettings")
defer span.End()
owner := n.BearerOwner(ctx) owner := n.BearerOwner(ctx)
if settings := n.cache.GetSettings(owner, bktInfo); settings != nil { if settings := n.cache.GetSettings(owner, bktInfo); settings != nil {
return settings, nil return settings, nil
@ -217,7 +297,7 @@ func (n *Layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo)
return nil, err return nil, err
} }
settings = &data.BucketSettings{Versioning: data.VersioningUnversioned} settings = &data.BucketSettings{Versioning: data.VersioningUnversioned}
n.reqLogger(ctx).Debug(logs.BucketSettingsNotFoundUseDefaults) n.reqLogger(ctx).Debug(logs.BucketSettingsNotFoundUseDefaults, logs.TagField(logs.TagDatapath))
} }
n.cache.PutSettings(owner, bktInfo, settings) n.cache.PutSettings(owner, bktInfo, settings)
@ -226,6 +306,9 @@ func (n *Layer) GetBucketSettings(ctx context.Context, bktInfo *data.BucketInfo)
} }
func (n *Layer) PutBucketSettings(ctx context.Context, p *PutSettingsParams) error { func (n *Layer) PutBucketSettings(ctx context.Context, p *PutSettingsParams) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.PutBucketSettings")
defer span.End()
if err := n.treeService.PutSettingsNode(ctx, p.BktInfo, p.Settings); err != nil { if err := n.treeService.PutSettingsNode(ctx, p.BktInfo, p.Settings); err != nil {
return fmt.Errorf("failed to get settings node: %w", err) return fmt.Errorf("failed to get settings node: %w", err)
} }
@ -261,7 +344,7 @@ func (n *Layer) attributesFromLock(ctx context.Context, lock *data.ObjectLock) (
if expEpoch != 0 { if expEpoch != 0 {
result = append(result, [2]string{ result = append(result, [2]string{
object.SysAttributeExpEpoch, strconv.FormatUint(expEpoch, 10), apiobject.SysAttributeExpEpoch, strconv.FormatUint(expEpoch, 10),
}) })
} }

View file

@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree"
@ -16,6 +17,9 @@ import (
) )
func (n *Layer) GetObjectTagging(ctx context.Context, p *data.GetObjectTaggingParams) (string, map[string]string, error) { func (n *Layer) GetObjectTagging(ctx context.Context, p *data.GetObjectTaggingParams) (string, map[string]string, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetObjectTagging")
defer span.End()
var err error var err error
owner := n.BearerOwner(ctx) owner := n.BearerOwner(ctx)
@ -52,6 +56,9 @@ func (n *Layer) GetObjectTagging(ctx context.Context, p *data.GetObjectTaggingPa
} }
func (n *Layer) PutObjectTagging(ctx context.Context, p *data.PutObjectTaggingParams) (err error) { func (n *Layer) PutObjectTagging(ctx context.Context, p *data.PutObjectTaggingParams) (err error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.PutObjectTagging")
defer span.End()
nodeVersion := p.NodeVersion nodeVersion := p.NodeVersion
if nodeVersion == nil { if nodeVersion == nil {
nodeVersion, err = n.getNodeVersionFromCacheOrFrostfs(ctx, p.ObjectVersion) nodeVersion, err = n.getNodeVersionFromCacheOrFrostfs(ctx, p.ObjectVersion)
@ -75,6 +82,9 @@ func (n *Layer) PutObjectTagging(ctx context.Context, p *data.PutObjectTaggingPa
} }
func (n *Layer) DeleteObjectTagging(ctx context.Context, p *data.ObjectVersion) error { func (n *Layer) DeleteObjectTagging(ctx context.Context, p *data.ObjectVersion) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.DeleteObjectTagging")
defer span.End()
version, err := n.getNodeVersion(ctx, p) version, err := n.getNodeVersion(ctx, p)
if err != nil { if err != nil {
return err return err
@ -96,6 +106,9 @@ func (n *Layer) DeleteObjectTagging(ctx context.Context, p *data.ObjectVersion)
} }
func (n *Layer) GetBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) (map[string]string, error) { func (n *Layer) GetBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) (map[string]string, error) {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.GetBucketTagging")
defer span.End()
owner := n.BearerOwner(ctx) owner := n.BearerOwner(ctx)
if tags := n.cache.GetTagging(owner, bucketTaggingCacheKey(bktInfo.CID)); tags != nil { if tags := n.cache.GetTagging(owner, bucketTaggingCacheKey(bktInfo.CID)); tags != nil {
@ -113,6 +126,9 @@ func (n *Layer) GetBucketTagging(ctx context.Context, bktInfo *data.BucketInfo)
} }
func (n *Layer) PutBucketTagging(ctx context.Context, bktInfo *data.BucketInfo, tagSet map[string]string) error { func (n *Layer) PutBucketTagging(ctx context.Context, bktInfo *data.BucketInfo, tagSet map[string]string) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.PutBucketTagging")
defer span.End()
if err := n.treeService.PutBucketTagging(ctx, bktInfo, tagSet); err != nil { if err := n.treeService.PutBucketTagging(ctx, bktInfo, tagSet); err != nil {
return err return err
} }
@ -123,6 +139,9 @@ func (n *Layer) PutBucketTagging(ctx context.Context, bktInfo *data.BucketInfo,
} }
func (n *Layer) DeleteBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) error { func (n *Layer) DeleteBucketTagging(ctx context.Context, bktInfo *data.BucketInfo) error {
ctx, span := tracing.StartSpanFromContext(ctx, "layer.DeleteBucketTagging")
defer span.End()
n.cache.DeleteTagging(bucketTaggingCacheKey(bktInfo.CID)) n.cache.DeleteTagging(bucketTaggingCacheKey(bktInfo.CID))
return n.treeService.DeleteBucketTagging(ctx, bktInfo) return n.treeService.DeleteBucketTagging(ctx, bktInfo)
@ -168,7 +187,8 @@ func (n *Layer) getNodeVersion(ctx context.Context, objVersion *data.ObjectVersi
if err == nil && version != nil && !version.IsDeleteMarker { if err == nil && version != nil && !version.IsDeleteMarker {
n.reqLogger(ctx).Debug(logs.GetTreeNode, n.reqLogger(ctx).Debug(logs.GetTreeNode,
zap.Stringer("cid", objVersion.BktInfo.CID), zap.Stringer("oid", version.OID)) zap.Stringer("cid", objVersion.BktInfo.CID),
zap.Stringer("oid", version.OID), logs.TagField(logs.TagExternalStorageTree))
} }
return version, err return version, err

View file

@ -65,13 +65,13 @@ func (n *Layer) submitPutTombstone(ctx context.Context, bkt *data.BucketInfo, me
defer wg.Done() defer wg.Done()
if err := n.putTombstoneObject(ctx, tomb, bkt); err != nil { if err := n.putTombstoneObject(ctx, tomb, bkt); err != nil {
n.reqLogger(ctx).Warn(logs.FailedToPutTombstoneObject, zap.String("cid", bkt.CID.EncodeToString()), zap.Error(err)) n.reqLogger(ctx).Warn(logs.FailedToPutTombstoneObject, zap.String("cid", bkt.CID.EncodeToString()), zap.Error(err), logs.TagField(logs.TagExternalStorage))
errCh <- fmt.Errorf("put tombstone object: %w", err) errCh <- fmt.Errorf("put tombstone object: %w", err)
} }
}) })
if err != nil { if err != nil {
wg.Done() wg.Done()
n.reqLogger(ctx).Warn(logs.FailedToSubmitTaskToPool, zap.Error(err)) n.reqLogger(ctx).Warn(logs.FailedToSubmitTaskToPool, zap.Error(err), logs.TagField(logs.TagDatapath))
errCh <- fmt.Errorf("submit task to pool: %w", err) errCh <- fmt.Errorf("submit task to pool: %w", err)
} }
} }
@ -106,7 +106,7 @@ func (n *Layer) getMembers(ctx context.Context, cnrID cid.ID, objID oid.ID, toke
} }
n.reqLogger(ctx).Warn(logs.FailedToListAllObjectRelations, zap.String("cid", cnrID.EncodeToString()), n.reqLogger(ctx).Warn(logs.FailedToListAllObjectRelations, zap.String("cid", cnrID.EncodeToString()),
zap.String("oid", objID.EncodeToString()), zap.Error(err)) zap.String("oid", objID.EncodeToString()), zap.Error(err), logs.TagField(logs.TagExternalStorage))
return nil, nil return nil, nil
} }
return append(oids, objID), nil return append(oids, objID), nil

View file

@ -18,19 +18,19 @@ type Service interface {
// If tree node is not found returns ErrNodeNotFound error. // If tree node is not found returns ErrNodeNotFound error.
GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error) GetSettingsNode(ctx context.Context, bktInfo *data.BucketInfo) (*data.BucketSettings, error)
// GetBucketCORS gets an object id that corresponds to object with bucket CORS. // GetBucketCORS gets an object address that corresponds to object with bucket CORS.
// //
// If object id is not found returns ErrNodeNotFound error. // If object is not found returns ErrNodeNotFound error.
GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (oid.Address, error) GetBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) (oid.Address, error)
// PutBucketCORS puts a node to a system tree and returns objectID of a previous cors config which must be deleted in FrostFS. // GetAllBucketCORS gets all object addresses that corresponds to objects with bucket CORS.
// //
// If object ids to remove is not found returns ErrNoNodeToRemove error. // If objects are not found returns ErrNodeNotFound error.
PutBucketCORS(ctx context.Context, bktInfo *data.BucketInfo, addr oid.Address) ([]oid.Address, error) GetAllBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error)
// DeleteBucketCORS removes a node from a system tree and returns objID which must be deleted in FrostFS. // DeleteBucketCORS removes a node from a system tree and returns object addresses which must be deleted in FrostFS.
// //
// If object ids to remove is not found returns ErrNoNodeToRemove error. // If objects to remove are not found returns ErrNoNodeToRemove error.
DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error) DeleteBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error)
GetObjectTagging(ctx context.Context, bktInfo *data.BucketInfo, objVersion *data.NodeVersion) (map[string]string, error) GetObjectTagging(ctx context.Context, bktInfo *data.BucketInfo, objVersion *data.NodeVersion) (map[string]string, error)

View file

@ -115,12 +115,12 @@ func (t *TreeServiceMock) GetSettingsNode(_ context.Context, bktInfo *data.Bucke
func (t *TreeServiceMock) GetBucketCORS(_ context.Context, bktInfo *data.BucketInfo) (oid.Address, error) { func (t *TreeServiceMock) GetBucketCORS(_ context.Context, bktInfo *data.BucketInfo) (oid.Address, error) {
systemMap, ok := t.system[bktInfo.CID.EncodeToString()] systemMap, ok := t.system[bktInfo.CID.EncodeToString()]
if !ok { if !ok {
return oid.Address{}, nil return oid.Address{}, tree.ErrNodeNotFound
} }
node, ok := systemMap["cors"] node, ok := systemMap["cors"]
if !ok { if !ok {
return oid.Address{}, nil return oid.Address{}, tree.ErrNodeNotFound
} }
var addr oid.Address var addr oid.Address
@ -129,19 +129,13 @@ func (t *TreeServiceMock) GetBucketCORS(_ context.Context, bktInfo *data.BucketI
return addr, nil return addr, nil
} }
func (t *TreeServiceMock) PutBucketCORS(_ context.Context, bktInfo *data.BucketInfo, addr oid.Address) ([]oid.Address, error) { func (t *TreeServiceMock) GetAllBucketCORS(ctx context.Context, bktInfo *data.BucketInfo) ([]oid.Address, error) {
systemMap, ok := t.system[bktInfo.CID.EncodeToString()] cors, err := t.GetBucketCORS(ctx, bktInfo)
if !ok { if err != nil {
systemMap = make(map[string]*data.BaseNodeVersion) return nil, err
} }
systemMap["cors"] = &data.BaseNodeVersion{ return []oid.Address{cors}, nil
OID: addr.Object(),
}
t.system[bktInfo.CID.EncodeToString()] = systemMap
return nil, tree.ErrNoNodeToRemove
} }
func (t *TreeServiceMock) DeleteBucketCORS(context.Context, *data.BucketInfo) ([]oid.Address, error) { func (t *TreeServiceMock) DeleteBucketCORS(context.Context, *data.BucketInfo) ([]oid.Address, error) {

View file

@ -18,7 +18,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
"github.com/nspcc-dev/neo-go/pkg/crypto/keys" "github.com/nspcc-dev/neo-go/pkg/crypto/keys"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap" "go.uber.org/zap/zaptest"
) )
func (tc *testContext) putObject(content []byte) *data.ObjectInfo { func (tc *testContext) putObject(content []byte) *data.ObjectInfo {
@ -139,7 +139,7 @@ type testContext struct {
} }
func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext { func prepareContext(t *testing.T, cachesConfig ...*CachesConfig) *testContext {
logger := zap.NewExample() logger := zaptest.NewLogger(t)
key, err := keys.NewPrivateKey() key, err := keys.NewPrivateKey()
require.NoError(t, err) require.NoError(t, err)

View file

@ -110,7 +110,7 @@ func preparePathStyleAddress(reqInfo *ReqInfo, r *http.Request, reqLogger *zap.L
// https://github.com/go-chi/chi/issues/641 // https://github.com/go-chi/chi/issues/641
// https://github.com/go-chi/chi/issues/642 // https://github.com/go-chi/chi/issues/642
if obj, err := url.PathUnescape(reqInfo.ObjectName); err != nil { if obj, err := url.PathUnescape(reqInfo.ObjectName); err != nil {
reqLogger.Warn(logs.FailedToUnescapeObjectName, zap.Error(err)) reqLogger.Warn(logs.FailedToUnescapeObjectName, zap.Error(err), logs.TagField(logs.TagDatapath))
} else { } else {
reqInfo.ObjectName = obj reqInfo.ObjectName = obj
} }

View file

@ -1,12 +1,14 @@
package middleware package middleware
import ( import (
"context"
"crypto/elliptic" "crypto/elliptic"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
@ -30,7 +32,9 @@ type (
Center interface { Center interface {
// Authenticate validate and authenticate request. // Authenticate validate and authenticate request.
// Must return ErrNoAuthorizationHeader if auth header is missed. // Must return ErrNoAuthorizationHeader if auth header is missed.
Authenticate(request *http.Request) (*Box, error) // Authenticate uses a separate context so that the authorization
// span middleware does not contain all subsequent spans.
Authenticate(ctx context.Context, request *http.Request) (*Box, error)
} }
//nolint:revive //nolint:revive
@ -47,34 +51,38 @@ var ErrNoAuthorizationHeader = errors.New("no authorization header")
func Auth(center Center, log *zap.Logger) Func { func Auth(center Center, log *zap.Logger) Func {
return func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() reqCtx := r.Context()
reqInfo := GetReqInfo(ctx) ctx, span := tracing.StartSpanFromContext(reqCtx, "middleware.Auth")
reqInfo := GetReqInfo(reqCtx)
reqInfo.User = "anon" reqInfo.User = "anon"
box, err := center.Authenticate(r) box, err := center.Authenticate(ctx, r)
if err != nil { if err != nil {
if errors.Is(err, ErrNoAuthorizationHeader) { if errors.Is(err, ErrNoAuthorizationHeader) {
reqLogOrDefault(ctx, log).Debug(logs.CouldntReceiveAccessBoxForGateKeyRandomKeyWillBeUsed, zap.Error(err)) reqLogOrDefault(reqCtx, log).Debug(logs.CouldntReceiveAccessBoxForGateKeyRandomKeyWillBeUsed, zap.Error(err), logs.TagField(logs.TagDatapath))
} else { } else {
reqLogOrDefault(ctx, log).Error(logs.FailedToPassAuthentication, zap.Error(err)) reqLogOrDefault(reqCtx, log).Error(logs.FailedToPassAuthentication, zap.Error(err), logs.TagField(logs.TagDatapath))
err = apierr.TransformToS3Error(err) err = apierr.TransformToS3Error(err)
if err.(apierr.Error).ErrCode == apierr.ErrInternalError { if err.(apierr.Error).ErrCode == apierr.ErrInternalError {
err = apierr.GetAPIError(apierr.ErrAccessDenied) err = apierr.GetAPIError(apierr.ErrAccessDenied)
} }
if _, wrErr := WriteErrorResponse(w, GetReqInfo(r.Context()), err); wrErr != nil { if _, wrErr := WriteErrorResponse(w, GetReqInfo(r.Context()), err); wrErr != nil {
reqLogOrDefault(ctx, log).Error(logs.FailedToWriteResponse, zap.Error(wrErr)) reqLogOrDefault(reqCtx, log).Error(logs.FailedToWriteResponse, zap.Error(wrErr), logs.TagField(logs.TagDatapath))
} }
span.End()
return return
} }
} else { } else {
ctx = SetBox(ctx, box) reqCtx = SetBox(reqCtx, box)
if box.AccessBox.Gate.BearerToken != nil { if box.AccessBox.Gate.BearerToken != nil {
reqInfo.User = bearer.ResolveIssuer(*box.AccessBox.Gate.BearerToken).String() reqInfo.User = bearer.ResolveIssuer(*box.AccessBox.Gate.BearerToken).String()
} }
reqLogOrDefault(ctx, log).Debug(logs.SuccessfulAuth, zap.String("accessKeyID", box.AuthHeaders.AccessKeyID)) reqLogOrDefault(reqCtx, log).Debug(logs.SuccessfulAuth, zap.String("accessKeyID", box.AuthHeaders.AccessKeyID), logs.TagField(logs.TagDatapath))
} }
h.ServeHTTP(w, r.WithContext(ctx)) span.End()
h.ServeHTTP(w, r.WithContext(reqCtx))
}) })
} }
} }
@ -86,22 +94,26 @@ type FrostFSIDValidator interface {
func FrostfsIDValidation(frostfsID FrostFSIDValidator, log *zap.Logger) Func { func FrostfsIDValidation(frostfsID FrostFSIDValidator, log *zap.Logger) Func {
return func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "middleware.FrostfsIDValidation")
bd, err := GetBoxData(ctx) bd, err := GetBoxData(ctx)
if err != nil || bd.Gate.BearerToken == nil { if err != nil || bd.Gate.BearerToken == nil {
reqLogOrDefault(ctx, log).Debug(logs.AnonRequestSkipFrostfsIDValidation) reqLogOrDefault(ctx, log).Debug(logs.AnonRequestSkipFrostfsIDValidation, logs.TagField(logs.TagDatapath))
span.End()
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
return return
} }
if err = validateBearerToken(frostfsID, bd.Gate.BearerToken); err != nil { if err = validateBearerToken(frostfsID, bd.Gate.BearerToken); err != nil {
reqLogOrDefault(ctx, log).Error(logs.FrostfsIDValidationFailed, zap.Error(err)) reqLogOrDefault(ctx, log).Error(logs.FrostfsIDValidationFailed, zap.Error(err), logs.TagField(logs.TagDatapath))
if _, wrErr := WriteErrorResponse(w, GetReqInfo(r.Context()), err); wrErr != nil { if _, wrErr := WriteErrorResponse(w, GetReqInfo(ctx), err); wrErr != nil {
reqLogOrDefault(ctx, log).Error(logs.FailedToWriteResponse, zap.Error(wrErr)) reqLogOrDefault(ctx, log).Error(logs.FailedToWriteResponse, zap.Error(wrErr), logs.TagField(logs.TagDatapath))
} }
span.End()
return return
} }
span.End()
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
}) })
} }

View file

@ -23,7 +23,7 @@ type (
) )
func LogHTTP(l *zap.Logger, _ LogHTTPSettings) Func { func LogHTTP(l *zap.Logger, _ LogHTTPSettings) Func {
l.Warn(logs.LogHTTPDisabledInThisBuild) l.Warn(logs.LogHTTPDisabledInThisBuild, logs.TagField(logs.TagApp))
return func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h.ServeHTTP(w, r) h.ServeHTTP(w, r)

View file

@ -139,7 +139,7 @@ func resolveCID(log *zap.Logger, resolveContainerID ContainerIDResolveFunc) cidR
containerID, err := resolveContainerID(ctx, reqInfo.BucketName) containerID, err := resolveContainerID(ctx, reqInfo.BucketName)
if err != nil { if err != nil {
reqLogOrDefault(ctx, log).Debug(logs.FailedToResolveCID, zap.Error(err)) reqLogOrDefault(ctx, log).Debug(logs.FailedToResolveCID, zap.Error(err), logs.TagField(logs.TagDatapath))
return "" return ""
} }

View file

@ -10,6 +10,7 @@ import (
"net/url" "net/url"
"strings" "strings"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors" apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
@ -88,32 +89,35 @@ type PolicyConfig struct {
func PolicyCheck(cfg PolicyConfig) Func { func PolicyCheck(cfg PolicyConfig) Func {
return func(h http.Handler) http.Handler { return func(h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context() ctx, span := tracing.StartSpanFromContext(r.Context(), "middleware.PolicyCheck")
if err := policyCheck(r, cfg); err != nil {
reqLogOrDefault(ctx, cfg.Log).Error(logs.PolicyValidationFailed, zap.Error(err)) if err := policyCheck(ctx, r, cfg); err != nil {
reqLogOrDefault(ctx, cfg.Log).Error(logs.PolicyValidationFailed, zap.Error(err), logs.TagField(logs.TagDatapath))
err = apierr.TransformToS3Error(err) err = apierr.TransformToS3Error(err)
if _, wrErr := WriteErrorResponse(w, GetReqInfo(ctx), err); wrErr != nil { if _, wrErr := WriteErrorResponse(w, GetReqInfo(ctx), err); wrErr != nil {
reqLogOrDefault(ctx, cfg.Log).Error(logs.FailedToWriteResponse, zap.Error(wrErr)) reqLogOrDefault(ctx, cfg.Log).Error(logs.FailedToWriteResponse, zap.Error(wrErr), logs.TagField(logs.TagDatapath))
} }
span.End()
return return
} }
span.End()
h.ServeHTTP(w, r) h.ServeHTTP(w, r)
}) })
} }
} }
func policyCheck(r *http.Request, cfg PolicyConfig) error { func policyCheck(ctx context.Context, r *http.Request, cfg PolicyConfig) error {
reqInfo := GetReqInfo(r.Context()) reqInfo := GetReqInfo(ctx)
req, userKey, userGroups, err := getPolicyRequest(r, cfg, reqInfo.RequestType, reqInfo.BucketName, reqInfo.ObjectName) req, userKey, userGroups, err := getPolicyRequest(ctx, r, cfg, reqInfo.RequestType, reqInfo.BucketName, reqInfo.ObjectName)
if err != nil { if err != nil {
return err return err
} }
var bktInfo *data.BucketInfo var bktInfo *data.BucketInfo
if reqInfo.RequestType != noneType && !strings.HasSuffix(req.Operation(), CreateBucketOperation) { if reqInfo.RequestType != noneType && !strings.HasSuffix(req.Operation(), CreateBucketOperation) {
bktInfo, err = cfg.BucketResolver(r.Context(), reqInfo.BucketName) bktInfo, err = cfg.BucketResolver(ctx, reqInfo.BucketName)
if err != nil { if err != nil {
return err return err
} }
@ -161,7 +165,7 @@ func policyCheck(r *http.Request, cfg PolicyConfig) error {
return nil return nil
} }
func getPolicyRequest(r *http.Request, cfg PolicyConfig, reqType ReqType, bktName string, objName string) (*testutil.Request, *keys.PublicKey, []string, error) { func getPolicyRequest(ctx context.Context, r *http.Request, cfg PolicyConfig, reqType ReqType, bktName string, objName string) (*testutil.Request, *keys.PublicKey, []string, error) {
var ( var (
owner string owner string
groups []string groups []string
@ -169,7 +173,6 @@ func getPolicyRequest(r *http.Request, cfg PolicyConfig, reqType ReqType, bktNam
pk *keys.PublicKey pk *keys.PublicKey
) )
ctx := r.Context()
bd, err := GetBoxData(ctx) bd, err := GetBoxData(ctx)
if err == nil && bd.Gate.BearerToken != nil { if err == nil && bd.Gate.BearerToken != nil {
pk, err = keys.NewPublicKeyFromBytes(bd.Gate.BearerToken.SigningKeyBytes(), elliptic.P256()) pk, err = keys.NewPublicKeyFromBytes(bd.Gate.BearerToken.SigningKeyBytes(), elliptic.P256())
@ -193,14 +196,16 @@ func getPolicyRequest(r *http.Request, cfg PolicyConfig, reqType ReqType, bktNam
res = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName) res = fmt.Sprintf(s3.ResourceFormatS3Bucket, bktName)
} }
requestProps, resourceProps, err := determineProperties(r, cfg.Decoder, cfg.BucketResolver, cfg.Tagging, reqType, op, bktName, objName, owner, groups, tags) requestProps, resourceProps, err := determineProperties(ctx, r, cfg.Decoder, cfg.BucketResolver, cfg.Tagging, reqType, op, bktName, objName, owner, groups, tags)
if err != nil { if err != nil {
return nil, nil, nil, fmt.Errorf("determine properties: %w", err) return nil, nil, nil, fmt.Errorf("determine properties: %w", err)
} }
reqLogOrDefault(r.Context(), cfg.Log).Debug(logs.PolicyRequest, zap.String("action", op), reqLogOrDefault(r.Context(), cfg.Log).Debug(logs.PolicyRequest, zap.String("action", op),
zap.String("resource", res), zap.Any("request properties", requestProps), zap.String("resource", res), zap.Any("request properties", requestProps),
zap.Any("resource properties", resourceProps)) zap.Any("resource properties", resourceProps),
logs.TagField(logs.TagDatapath),
)
return testutil.NewRequest(op, testutil.NewResource(res, resourceProps), requestProps), pk, groups, nil return testutil.NewRequest(op, testutil.NewResource(res, resourceProps), requestProps), pk, groups, nil
} }
@ -418,7 +423,7 @@ func determineGeneralOperation(r *http.Request) string {
return "UnmatchedOperation" return "UnmatchedOperation"
} }
func determineProperties(r *http.Request, decoder XMLDecoder, resolver BucketResolveFunc, tagging ResourceTagging, reqType ReqType, func determineProperties(ctx context.Context, r *http.Request, decoder XMLDecoder, resolver BucketResolveFunc, tagging ResourceTagging, reqType ReqType,
op, bktName, objName, owner string, groups []string, userClaims map[string]string) (requestProperties map[string]string, resourceProperties map[string]string, err error) { op, bktName, objName, owner string, groups []string, userClaims map[string]string) (requestProperties map[string]string, resourceProperties map[string]string, err error) {
requestProperties = map[string]string{ requestProperties = map[string]string{
s3.PropertyKeyOwner: owner, s3.PropertyKeyOwner: owner,
@ -466,7 +471,7 @@ func determineProperties(r *http.Request, decoder XMLDecoder, resolver BucketRes
requestProperties[k] = v requestProperties[k] = v
} }
resourceProperties, err = determineResourceTags(r.Context(), reqType, op, bktName, objName, queries.Get(QueryVersionID), resolver, tagging) resourceProperties, err = determineResourceTags(ctx, reqType, op, bktName, objName, queries.Get(QueryVersionID), resolver, tagging)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("determine resource tags: %w", err) return nil, nil, fmt.Errorf("determine resource tags: %w", err)
} }

View file

@ -166,7 +166,7 @@ func Request(log *zap.Logger, settings RequestSettings) Func {
// generate random UUIDv4 // generate random UUIDv4
id, err := uuid.NewRandom() id, err := uuid.NewRandom()
if err != nil { if err != nil {
log.Error(logs.FailedToGenerateRequestID, zap.Error(err)) log.Error(logs.FailedToGenerateRequestID, zap.Error(err), logs.TagField(logs.TagDatapath))
} }
// set request id into response header // set request id into response header
@ -198,7 +198,8 @@ func Request(log *zap.Logger, settings RequestSettings) Func {
r = r.WithContext(SetReqLogger(ctx, reqLogger)) r = r.WithContext(SetReqLogger(ctx, reqLogger))
reqLogger.Info(logs.RequestStart, zap.String("host", r.Host), reqLogger.Info(logs.RequestStart, zap.String("host", r.Host),
zap.String("remote_host", reqInfo.RemoteHost), zap.String("namespace", reqInfo.Namespace)) zap.String("remote_host", reqInfo.RemoteHost), zap.String("namespace", reqInfo.Namespace),
logs.TagField(logs.TagDatapath))
// continue execution // continue execution
h.ServeHTTP(lw, r) h.ServeHTTP(lw, r)

View file

@ -144,6 +144,17 @@ func WriteErrorResponse(w http.ResponseWriter, reqInfo *ReqInfo, err error) (int
return code, nil return code, nil
} }
// WriteErrorResponseNoHeader writes XML encoded error to the response body.
func WriteErrorResponseNoHeader(w http.ResponseWriter, reqInfo *ReqInfo, err error) error {
errorResponse := getAPIErrorResponse(reqInfo, err)
encodedErrorResponse, err := EncodeResponseNoHeader(errorResponse)
if err != nil {
return err
}
return WriteResponseBody(w, encodedErrorResponse)
}
// Write http common headers. // Write http common headers.
func setCommonHeaders(w http.ResponseWriter) { func setCommonHeaders(w http.ResponseWriter) {
w.Header().Set(hdrServerInfo, version.Server) w.Header().Set(hdrServerInfo, version.Server)
@ -200,6 +211,18 @@ func EncodeResponse(response interface{}) ([]byte, error) {
return bytesBuffer.Bytes(), nil return bytesBuffer.Bytes(), nil
} }
// EncodeResponseNoHeader encodes response without setting xml.Header.
// Should be used with periodicXMLWriter which sends xml.Header to the client
// with whitespaces to keep connection alive.
func EncodeResponseNoHeader(response interface{}) ([]byte, error) {
var bytesBuffer bytes.Buffer
if err := xml.NewEncoder(&bytesBuffer).Encode(response); err != nil {
return nil, err
}
return bytesBuffer.Bytes(), nil
}
// EncodeToResponse encodes the response into ResponseWriter. // EncodeToResponse encodes the response into ResponseWriter.
func EncodeToResponse(w http.ResponseWriter, response interface{}) error { func EncodeToResponse(w http.ResponseWriter, response interface{}) error {
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
@ -331,7 +354,7 @@ func LogSuccessResponse(l *zap.Logger) Func {
fields = append(fields, zap.String("user", reqInfo.User)) fields = append(fields, zap.String("user", reqInfo.User))
} }
reqLogger.Info(logs.RequestEnd, fields...) reqLogger.Info(logs.RequestEnd, append(fields, logs.TagField(logs.TagDatapath))...)
}) })
} }
} }

View file

@ -245,13 +245,14 @@ func errorResponseHandler(w http.ResponseWriter, r *http.Request) {
zap.String("method", reqInfo.API), zap.String("method", reqInfo.API),
zap.String("http method", r.Method), zap.String("http method", r.Method),
zap.String("url", r.RequestURI), zap.String("url", r.RequestURI),
logs.TagField(logs.TagDatapath),
} }
if wrErr != nil { if wrErr != nil {
fields = append(fields, zap.NamedError("write_response_error", wrErr)) fields = append(fields, zap.NamedError("write_response_error", wrErr))
} }
log.Error(logs.RequestUnmatched, fields...) log.Error(logs.RequestUnmatched, append(fields, logs.TagField(logs.TagDatapath))...)
} }
} }
@ -266,13 +267,14 @@ func notSupportedHandler() http.HandlerFunc {
fields := []zap.Field{ fields := []zap.Field{
zap.String("http method", r.Method), zap.String("http method", r.Method),
zap.String("url", r.RequestURI), zap.String("url", r.RequestURI),
logs.TagField(logs.TagDatapath),
} }
if wrErr != nil { if wrErr != nil {
fields = append(fields, zap.NamedError("write_response_error", wrErr)) fields = append(fields, zap.NamedError("write_response_error", wrErr))
} }
log.Error(logs.NotSupported, fields...) log.Error(logs.NotSupported, append(fields, logs.TagField(logs.TagDatapath))...)
} }
} }
} }

View file

@ -45,7 +45,7 @@ type centerMock struct {
key *keys.PrivateKey key *keys.PrivateKey
} }
func (c *centerMock) Authenticate(*http.Request) (*middleware.Box, error) { func (c *centerMock) Authenticate(context.Context, *http.Request) (*middleware.Box, error) {
if c.noAuthHeader { if c.noAuthHeader {
return nil, middleware.ErrNoAuthorizationHeader return nil, middleware.ErrNoAuthorizationHeader
} }

View file

@ -20,6 +20,7 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-contract/commonclient" "git.frostfs.info/TrueCloudLab/frostfs-contract/commonclient"
"git.frostfs.info/TrueCloudLab/frostfs-observability/tracing" "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing"
grpctracing "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing/grpc" grpctracing "git.frostfs.info/TrueCloudLab/frostfs-observability/tracing/grpc"
qostagging "git.frostfs.info/TrueCloudLab/frostfs-qos/tagging"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
@ -53,6 +54,7 @@ import (
"github.com/panjf2000/ants/v2" "github.com/panjf2000/ants/v2"
"github.com/spf13/viper" "github.com/spf13/viper"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore"
"golang.org/x/exp/slices" "golang.org/x/exp/slices"
"golang.org/x/text/encoding/ianaindex" "golang.org/x/text/encoding/ianaindex"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -65,7 +67,7 @@ type (
App struct { App struct {
ctr s3middleware.Center ctr s3middleware.Center
log *zap.Logger log *zap.Logger
cfg *viper.Viper cfg *appCfg
pool *pool.Pool pool *pool.Pool
treePool *treepool.Pool treePool *treepool.Pool
key *keys.PrivateKey key *keys.PrivateKey
@ -91,6 +93,10 @@ type (
wrkDone chan struct{} wrkDone chan struct{}
} }
tagsConfig struct {
tagLogs sync.Map
}
loggerSettings struct { loggerSettings struct {
mu sync.RWMutex mu sync.RWMutex
appMetrics *metrics.AppMetrics appMetrics *metrics.AppMetrics
@ -99,6 +105,7 @@ type (
appSettings struct { appSettings struct {
logLevel zap.AtomicLevel logLevel zap.AtomicLevel
httpLogging s3middleware.LogHTTPConfig httpLogging s3middleware.LogHTTPConfig
tagsConfig *tagsConfig
maxClient maxClientsConfig maxClient maxClientsConfig
defaultMaxAge int defaultMaxAge int
reconnectInterval time.Duration reconnectInterval time.Duration
@ -132,19 +139,61 @@ type (
tombstoneMembersSize int tombstoneMembersSize int
tombstoneLifetime uint64 tombstoneLifetime uint64
tlsTerminationHeader string tlsTerminationHeader string
listingKeepaliveThrottle time.Duration
} }
maxClientsConfig struct { maxClientsConfig struct {
deadline time.Duration deadline time.Duration
count int count int
} }
Logger struct {
logger *zap.Logger
lvl zap.AtomicLevel
}
) )
func (t *tagsConfig) LevelEnabled(tag string, tgtLevel zapcore.Level) bool {
lvl, ok := t.tagLogs.Load(tag)
if !ok {
return false
}
return lvl.(zapcore.Level).Enabled(tgtLevel)
}
func (t *tagsConfig) update(cfg *viper.Viper) error {
tags, err := fetchLogTagsConfig(cfg)
if err != nil {
return err
}
t.tagLogs.Range(func(key, value any) bool {
k := key.(string)
v := value.(zapcore.Level)
if lvl, ok := tags[k]; ok {
if lvl != v {
t.tagLogs.Store(key, lvl)
}
} else {
t.tagLogs.Delete(key)
delete(tags, k)
}
return true
})
for k, v := range tags {
t.tagLogs.Store(k, v)
}
return nil
}
func newTagsConfig(v *viper.Viper) *tagsConfig {
var t tagsConfig
if err := t.update(v); err != nil {
// panic here is analogue of the similar panic during common log level initialization.
panic(err.Error())
}
return &t
}
func (s *loggerSettings) DroppedLogsInc() { func (s *loggerSettings) DroppedLogsInc() {
s.mu.RLock() s.mu.RLock()
defer s.mu.RUnlock() defer s.mu.RUnlock()
@ -161,15 +210,17 @@ func (s *loggerSettings) setMetrics(appMetrics *metrics.AppMetrics) {
s.appMetrics = appMetrics s.appMetrics = appMetrics
} }
func newApp(ctx context.Context, v *viper.Viper) *App { func newApp(ctx context.Context, cfg *appCfg) *App {
logSettings := &loggerSettings{} logSettings := &loggerSettings{}
log := pickLogger(v, logSettings) tagConfig := newTagsConfig(cfg.config())
settings := newAppSettings(log, v) log := pickLogger(cfg.config(), logSettings, tagConfig)
appCache := layer.NewCache(getCacheOptions(v, log.logger)) settings := newAppSettings(log, cfg.config())
settings.tagsConfig = tagConfig
appCache := layer.NewCache(getCacheOptions(cfg.config(), log.logger))
app := &App{ app := &App{
log: log.logger, log: log.logger,
cfg: v, cfg: cfg,
cache: appCache, cache: appCache,
webDone: make(chan struct{}, 1), webDone: make(chan struct{}, 1),
@ -184,6 +235,10 @@ func newApp(ctx context.Context, v *viper.Viper) *App {
return app return app
} }
func (a *App) config() *viper.Viper {
return a.cfg.config()
}
func (a *App) init(ctx context.Context) { func (a *App) init(ctx context.Context) {
a.initPools(ctx) a.initPools(ctx)
a.initResolver() a.initResolver()
@ -198,10 +253,10 @@ func (a *App) init(ctx context.Context) {
} }
func (a *App) initAuthCenter(ctx context.Context) { func (a *App) initAuthCenter(ctx context.Context) {
if a.cfg.IsSet(cfgContainersAccessBox) { if a.config().IsSet(cfgContainersAccessBox) {
cnrID, err := a.resolveContainerID(ctx, cfgContainersAccessBox) cnrID, err := a.resolveContainerID(ctx, cfgContainersAccessBox)
if err != nil { if err != nil {
a.log.Fatal(logs.CouldNotFetchAccessBoxContainerInfo, zap.Error(err)) a.log.Fatal(logs.CouldNotFetchAccessBoxContainerInfo, zap.Error(err), logs.TagField(logs.TagApp))
} }
a.settings.accessbox = &cnrID a.settings.accessbox = &cnrID
} }
@ -209,36 +264,33 @@ func (a *App) initAuthCenter(ctx context.Context) {
cfg := tokens.Config{ cfg := tokens.Config{
FrostFS: frostfs.NewAuthmateFrostFS(frostfs.NewFrostFS(a.pool, a.key), a.log), FrostFS: frostfs.NewAuthmateFrostFS(frostfs.NewFrostFS(a.pool, a.key), a.log),
Key: a.key, Key: a.key,
CacheConfig: getAccessBoxCacheConfig(a.cfg, a.log), CacheConfig: getAccessBoxCacheConfig(a.config(), a.log),
RemovingCheckAfterDurations: fetchRemovingCheckInterval(a.cfg, a.log), RemovingCheckAfterDurations: fetchRemovingCheckInterval(a.config(), a.log),
} }
a.ctr = auth.New(tokens.New(cfg), a.cfg.GetStringSlice(cfgAllowedAccessKeyIDPrefixes), a.settings) a.ctr = auth.New(tokens.New(cfg), a.config().GetStringSlice(cfgAllowedAccessKeyIDPrefixes), a.settings)
} }
func (a *App) initLayer(ctx context.Context) { func (a *App) initLayer(ctx context.Context) {
// prepare random key for anonymous requests // prepare random key for anonymous requests
randomKey, err := keys.NewPrivateKey() randomKey, err := keys.NewPrivateKey()
if err != nil { if err != nil {
a.log.Fatal(logs.CouldntGenerateRandomKey, zap.Error(err)) a.log.Fatal(logs.CouldntGenerateRandomKey, zap.Error(err), logs.TagField(logs.TagApp))
} }
var gateOwner user.ID var gateOwner user.ID
user.IDFromKey(&gateOwner, a.key.PrivateKey.PublicKey) user.IDFromKey(&gateOwner, a.key.PrivateKey.PublicKey)
var corsCnrInfo *data.BucketInfo corsCnrInfo, err := a.fetchContainerInfo(ctx, cfgContainersCORS)
if a.cfg.IsSet(cfgContainersCORS) { if err != nil {
corsCnrInfo, err = a.fetchContainerInfo(ctx, cfgContainersCORS) a.log.Fatal(logs.CouldNotFetchCORSContainerInfo, zap.Error(err), logs.TagField(logs.TagApp))
if err != nil {
a.log.Fatal(logs.CouldNotFetchCORSContainerInfo, zap.Error(err))
}
} }
var lifecycleCnrInfo *data.BucketInfo var lifecycleCnrInfo *data.BucketInfo
if a.cfg.IsSet(cfgContainersLifecycle) { if a.config().IsSet(cfgContainersLifecycle) {
lifecycleCnrInfo, err = a.fetchContainerInfo(ctx, cfgContainersLifecycle) lifecycleCnrInfo, err = a.fetchContainerInfo(ctx, cfgContainersLifecycle)
if err != nil { if err != nil {
a.log.Fatal(logs.CouldNotFetchLifecycleContainerInfo, zap.Error(err)) a.log.Fatal(logs.CouldNotFetchLifecycleContainerInfo, zap.Error(err), logs.TagField(logs.TagApp))
} }
} }
@ -264,7 +316,7 @@ func (a *App) initLayer(ctx context.Context) {
func (a *App) initWorkerPool() *ants.Pool { func (a *App) initWorkerPool() *ants.Pool {
workerPool, err := ants.NewPool(a.settings.workerPoolSize) workerPool, err := ants.NewPool(a.settings.workerPoolSize)
if err != nil { if err != nil {
a.log.Fatal(logs.FailedToCreateWorkerPool, zap.Error(err)) a.log.Fatal(logs.FailedToCreateWorkerPool, zap.Error(err), logs.TagField(logs.TagApp))
} }
return workerPool return workerPool
} }
@ -273,6 +325,7 @@ func newAppSettings(log *Logger, v *viper.Viper) *appSettings {
settings := &appSettings{ settings := &appSettings{
logLevel: log.lvl, logLevel: log.lvl,
httpLogging: s3middleware.LogHTTPConfig{}, httpLogging: s3middleware.LogHTTPConfig{},
tagsConfig: newTagsConfig(v),
maxClient: newMaxClients(v), maxClient: newMaxClients(v),
defaultMaxAge: fetchDefaultMaxAge(v, log.logger), defaultMaxAge: fetchDefaultMaxAge(v, log.logger),
reconnectInterval: fetchReconnectInterval(v), reconnectInterval: fetchReconnectInterval(v),
@ -319,6 +372,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
tombstoneMembersSize := fetchTombstoneMembersSize(v) tombstoneMembersSize := fetchTombstoneMembersSize(v)
tombstoneLifetime := fetchTombstoneLifetime(v) tombstoneLifetime := fetchTombstoneLifetime(v)
tlsTerminationHeader := v.GetString(cfgEncryptionTLSTerminationHeader) tlsTerminationHeader := v.GetString(cfgEncryptionTLSTerminationHeader)
listingKeepaliveThrottle := v.GetDuration(cfgKludgeListingKeepAliveThrottle)
s.mu.Lock() s.mu.Lock()
defer s.mu.Unlock() defer s.mu.Unlock()
@ -352,6 +406,7 @@ func (s *appSettings) update(v *viper.Viper, log *zap.Logger) {
s.tombstoneMembersSize = tombstoneMembersSize s.tombstoneMembersSize = tombstoneMembersSize
s.tombstoneLifetime = tombstoneLifetime s.tombstoneLifetime = tombstoneLifetime
s.tlsTerminationHeader = tlsTerminationHeader s.tlsTerminationHeader = tlsTerminationHeader
s.listingKeepaliveThrottle = listingKeepaliveThrottle
} }
func (s *appSettings) prepareVHSNamespaces(v *viper.Viper, log *zap.Logger, defaultNamespaces []string) map[string]bool { func (s *appSettings) prepareVHSNamespaces(v *viper.Viper, log *zap.Logger, defaultNamespaces []string) map[string]bool {
@ -589,6 +644,12 @@ func (s *appSettings) TombstoneLifetime() uint64 {
return s.tombstoneLifetime return s.tombstoneLifetime
} }
func (s *appSettings) ListingKeepaliveThrottle() time.Duration {
s.mu.RLock()
defer s.mu.RUnlock()
return s.listingKeepaliveThrottle
}
func (a *App) initAPI(ctx context.Context) { func (a *App) initAPI(ctx context.Context) {
a.initLayer(ctx) a.initLayer(ctx)
a.initHandler() a.initHandler()
@ -599,7 +660,7 @@ func (a *App) initMetrics() {
Logger: a.log, Logger: a.log,
PoolStatistics: frostfs.NewPoolStatistic(a.pool), PoolStatistics: frostfs.NewPoolStatistic(a.pool),
TreeStatistic: a.treePool, TreeStatistic: a.treePool,
Enabled: a.cfg.GetBool(cfgPrometheusEnabled), Enabled: a.config().GetBool(cfgPrometheusEnabled),
} }
a.metrics = metrics.NewAppMetrics(cfg) a.metrics = metrics.NewAppMetrics(cfg)
@ -609,9 +670,9 @@ func (a *App) initMetrics() {
func (a *App) initFrostfsID(ctx context.Context) { func (a *App) initFrostfsID(ctx context.Context) {
cli, err := ffidcontract.New(ctx, ffidcontract.Config{ cli, err := ffidcontract.New(ctx, ffidcontract.Config{
RPCAddress: a.cfg.GetString(cfgRPCEndpoint), RPCAddress: a.config().GetString(cfgRPCEndpoint),
Contract: a.cfg.GetString(cfgFrostfsIDContract), Contract: a.config().GetString(cfgFrostfsIDContract),
ProxyContract: a.cfg.GetString(cfgProxyContract), ProxyContract: a.config().GetString(cfgProxyContract),
Key: a.key, Key: a.key,
Waiter: commonclient.WaiterOptions{ Waiter: commonclient.WaiterOptions{
IgnoreAlreadyExistsError: false, IgnoreAlreadyExistsError: false,
@ -619,24 +680,24 @@ func (a *App) initFrostfsID(ctx context.Context) {
}, },
}) })
if err != nil { if err != nil {
a.log.Fatal(logs.InitFrostfsIDContractFailed, zap.Error(err)) a.log.Fatal(logs.InitFrostfsIDFailed, zap.Error(err), logs.TagField(logs.TagApp))
} }
a.frostfsid, err = frostfsid.NewFrostFSID(frostfsid.Config{ a.frostfsid, err = frostfsid.NewFrostFSID(frostfsid.Config{
Cache: cache.NewFrostfsIDCache(getFrostfsIDCacheConfig(a.cfg, a.log)), Cache: cache.NewFrostfsIDCache(getFrostfsIDCacheConfig(a.config(), a.log)),
FrostFSID: cli, FrostFSID: cli,
Logger: a.log, Logger: a.log,
}) })
if err != nil { if err != nil {
a.log.Fatal(logs.InitFrostfsIDContractFailed, zap.Error(err)) a.log.Fatal(logs.InitFrostfsIDFailed, zap.Error(err), logs.TagField(logs.TagApp))
} }
} }
func (a *App) initPolicyStorage(ctx context.Context) { func (a *App) initPolicyStorage(ctx context.Context) {
policyContract, err := contract.New(ctx, contract.Config{ policyContract, err := contract.New(ctx, contract.Config{
RPCAddress: a.cfg.GetString(cfgRPCEndpoint), RPCAddress: a.config().GetString(cfgRPCEndpoint),
Contract: a.cfg.GetString(cfgPolicyContract), Contract: a.config().GetString(cfgPolicyContract),
ProxyContract: a.cfg.GetString(cfgProxyContract), ProxyContract: a.config().GetString(cfgProxyContract),
Key: a.key, Key: a.key,
Waiter: commonclient.WaiterOptions{ Waiter: commonclient.WaiterOptions{
IgnoreAlreadyExistsError: false, IgnoreAlreadyExistsError: false,
@ -644,12 +705,12 @@ func (a *App) initPolicyStorage(ctx context.Context) {
}, },
}) })
if err != nil { if err != nil {
a.log.Fatal(logs.InitPolicyContractFailed, zap.Error(err)) a.log.Fatal(logs.InitPolicyContractFailed, zap.Error(err), logs.TagField(logs.TagApp))
} }
a.policyStorage = policy.NewStorage(policy.StorageConfig{ a.policyStorage = policy.NewStorage(policy.StorageConfig{
Contract: policyContract, Contract: policyContract,
Cache: cache.NewMorphPolicyCache(getMorphPolicyCacheConfig(a.cfg, a.log)), Cache: cache.NewMorphPolicyCache(getMorphPolicyCacheConfig(a.config(), a.log)),
Log: a.log, Log: a.log,
}) })
} }
@ -658,26 +719,26 @@ func (a *App) initResolver() {
var err error var err error
a.bucketResolver, err = resolver.NewBucketResolver(a.getResolverOrder(), a.getResolverConfig()) a.bucketResolver, err = resolver.NewBucketResolver(a.getResolverOrder(), a.getResolverConfig())
if err != nil { if err != nil {
a.log.Fatal(logs.FailedToCreateResolver, zap.Error(err)) a.log.Fatal(logs.FailedToCreateResolver, zap.Error(err), logs.TagField(logs.TagApp))
} }
} }
func (a *App) getResolverConfig() *resolver.Config { func (a *App) getResolverConfig() *resolver.Config {
return &resolver.Config{ return &resolver.Config{
FrostFS: frostfs.NewResolverFrostFS(a.pool), FrostFS: frostfs.NewResolverFrostFS(a.pool),
RPCAddress: a.cfg.GetString(cfgRPCEndpoint), RPCAddress: a.config().GetString(cfgRPCEndpoint),
} }
} }
func (a *App) getResolverOrder() []string { func (a *App) getResolverOrder() []string {
order := a.cfg.GetStringSlice(cfgResolveOrder) order := a.config().GetStringSlice(cfgResolveOrder)
if a.cfg.GetString(cfgRPCEndpoint) == "" { if a.config().GetString(cfgRPCEndpoint) == "" {
order = remove(order, resolver.NNSResolver) order = remove(order, resolver.NNSResolver)
a.log.Warn(logs.ResolverNNSWontBeUsedSinceRPCEndpointIsntProvided) a.log.Warn(logs.ResolverNNSWontBeUsedSinceRPCEndpointIsntProvided, logs.TagField(logs.TagApp))
} }
if len(order) == 0 { if len(order) == 0 {
a.log.Info(logs.ContainerResolverWillBeDisabled) a.log.Info(logs.ContainerResolverWillBeDisabled, logs.TagField(logs.TagApp))
} }
return order return order
@ -689,42 +750,42 @@ func (a *App) initTracing(ctx context.Context) {
instanceID = a.servers[0].Address() instanceID = a.servers[0].Address()
} }
cfg := tracing.Config{ cfg := tracing.Config{
Enabled: a.cfg.GetBool(cfgTracingEnabled), Enabled: a.config().GetBool(cfgTracingEnabled),
Exporter: tracing.Exporter(a.cfg.GetString(cfgTracingExporter)), Exporter: tracing.Exporter(a.config().GetString(cfgTracingExporter)),
Endpoint: a.cfg.GetString(cfgTracingEndpoint), Endpoint: a.config().GetString(cfgTracingEndpoint),
Service: "frostfs-s3-gw", Service: "frostfs-s3-gw",
InstanceID: instanceID, InstanceID: instanceID,
Version: version.Version, Version: version.Version,
} }
if trustedCa := a.cfg.GetString(cfgTracingTrustedCa); trustedCa != "" { if trustedCa := a.config().GetString(cfgTracingTrustedCa); trustedCa != "" {
caBytes, err := os.ReadFile(trustedCa) caBytes, err := os.ReadFile(trustedCa)
if err != nil { if err != nil {
a.log.Warn(logs.FailedToInitializeTracing, zap.Error(err)) a.log.Warn(logs.FailedToInitializeTracing, zap.Error(err), logs.TagField(logs.TagApp))
return return
} }
certPool := x509.NewCertPool() certPool := x509.NewCertPool()
ok := certPool.AppendCertsFromPEM(caBytes) ok := certPool.AppendCertsFromPEM(caBytes)
if !ok { if !ok {
a.log.Warn(logs.FailedToInitializeTracing, zap.String("error", "can't fill cert pool by ca cert")) a.log.Warn(logs.FailedToInitializeTracing, zap.String("error", "can't fill cert pool by ca cert"), logs.TagField(logs.TagApp))
return return
} }
cfg.ServerCaCertPool = certPool cfg.ServerCaCertPool = certPool
} }
attributes, err := fetchTracingAttributes(a.cfg) attributes, err := fetchTracingAttributes(a.config())
if err != nil { if err != nil {
a.log.Warn(logs.FailedToInitializeTracing, zap.Error(err)) a.log.Warn(logs.FailedToInitializeTracing, zap.Error(err), logs.TagField(logs.TagApp))
return return
} }
cfg.Attributes = attributes cfg.Attributes = attributes
updated, err := tracing.Setup(ctx, cfg) updated, err := tracing.Setup(ctx, cfg)
if err != nil { if err != nil {
a.log.Warn(logs.FailedToInitializeTracing, zap.Error(err)) a.log.Warn(logs.FailedToInitializeTracing, zap.Error(err), logs.TagField(logs.TagApp))
} }
if updated { if updated {
a.log.Info(logs.TracingConfigUpdated) a.log.Info(logs.TracingConfigUpdated, logs.TagField(logs.TagApp))
} }
} }
@ -734,7 +795,7 @@ func (a *App) shutdownTracing() {
defer cancel() defer cancel()
if err := tracing.Shutdown(shdnCtx); err != nil { if err := tracing.Shutdown(shdnCtx); err != nil {
a.log.Warn(logs.FailedToShutdownTracing, zap.Error(err)) a.log.Warn(logs.FailedToShutdownTracing, zap.Error(err), logs.TagField(logs.TagApp))
} }
} }
@ -751,7 +812,7 @@ func newMaxClients(cfg *viper.Viper) maxClientsConfig {
func getDialerSource(logger *zap.Logger, cfg *viper.Viper) *internalnet.DialerSource { func getDialerSource(logger *zap.Logger, cfg *viper.Viper) *internalnet.DialerSource {
source, err := internalnet.NewDialerSource(fetchMultinetConfig(cfg, logger)) source, err := internalnet.NewDialerSource(fetchMultinetConfig(cfg, logger))
if err != nil { if err != nil {
logger.Fatal(logs.FailedToLoadMultinetConfig, zap.Error(err)) logger.Fatal(logs.FailedToLoadMultinetConfig, zap.Error(err), logs.TagField(logs.TagApp))
} }
return source return source
} }
@ -760,74 +821,75 @@ func (a *App) initPools(ctx context.Context) {
var prm pool.InitParameters var prm pool.InitParameters
var prmTree treepool.InitParameters var prmTree treepool.InitParameters
password := wallet.GetPassword(a.cfg, cfgWalletPassphrase) password := wallet.GetPassword(a.config(), cfgWalletPassphrase)
key, err := wallet.GetKeyFromPath(a.cfg.GetString(cfgWalletPath), a.cfg.GetString(cfgWalletAddress), password) key, err := wallet.GetKeyFromPath(a.config().GetString(cfgWalletPath), a.config().GetString(cfgWalletAddress), password)
if err != nil { if err != nil {
a.log.Fatal(logs.CouldNotLoadFrostFSPrivateKey, zap.Error(err)) a.log.Fatal(logs.CouldNotLoadFrostFSPrivateKey, zap.Error(err), logs.TagField(logs.TagApp))
} }
prm.SetKey(&key.PrivateKey) prm.SetKey(&key.PrivateKey)
prmTree.SetKey(key) prmTree.SetKey(key)
a.log.Info(logs.UsingCredentials, zap.String("FrostFS", hex.EncodeToString(key.PublicKey().Bytes()))) a.log.Info(logs.UsingCredentials, zap.String("FrostFS", hex.EncodeToString(key.PublicKey().Bytes())), logs.TagField(logs.TagApp))
for _, peer := range fetchPeers(a.log, a.cfg) { for _, peer := range fetchPeers(a.log, a.config()) {
prm.AddNode(peer) prm.AddNode(peer)
prmTree.AddNode(peer) prmTree.AddNode(peer)
} }
connTimeout := fetchConnectTimeout(a.cfg) connTimeout := fetchConnectTimeout(a.config())
prm.SetNodeDialTimeout(connTimeout) prm.SetNodeDialTimeout(connTimeout)
prmTree.SetNodeDialTimeout(connTimeout) prmTree.SetNodeDialTimeout(connTimeout)
streamTimeout := fetchStreamTimeout(a.cfg) prm.SetNodeStreamTimeout(fetchStreamTimeout(a.config(), cfgStreamTimeout))
prm.SetNodeStreamTimeout(streamTimeout) prmTree.SetNodeStreamTimeout(fetchStreamTimeout(a.config(), cfgTreeStreamTimeout))
prmTree.SetNodeStreamTimeout(streamTimeout)
healthCheckTimeout := fetchHealthCheckTimeout(a.cfg) healthCheckTimeout := fetchHealthCheckTimeout(a.config())
prm.SetHealthcheckTimeout(healthCheckTimeout) prm.SetHealthcheckTimeout(healthCheckTimeout)
prmTree.SetHealthcheckTimeout(healthCheckTimeout) prmTree.SetHealthcheckTimeout(healthCheckTimeout)
rebalanceInterval := fetchRebalanceInterval(a.cfg) rebalanceInterval := fetchRebalanceInterval(a.config())
prm.SetClientRebalanceInterval(rebalanceInterval) prm.SetClientRebalanceInterval(rebalanceInterval)
prmTree.SetClientRebalanceInterval(rebalanceInterval) prmTree.SetClientRebalanceInterval(rebalanceInterval)
errorThreshold := fetchErrorThreshold(a.cfg) errorThreshold := fetchErrorThreshold(a.config())
prm.SetErrorThreshold(errorThreshold) prm.SetErrorThreshold(errorThreshold)
prm.SetGracefulCloseOnSwitchTimeout(fetchSetGracefulCloseOnSwitchTimeout(a.cfg)) prm.SetGracefulCloseOnSwitchTimeout(fetchSetGracefulCloseOnSwitchTimeout(a.config()))
prm.SetLogger(a.log) prm.SetLogger(a.log.With(logs.TagField(logs.TagDatapath)))
prmTree.SetLogger(a.log) prmTree.SetLogger(a.log.With(logs.TagField(logs.TagDatapath)))
prmTree.SetMaxRequestAttempts(a.cfg.GetInt(cfgTreePoolMaxAttempts)) prmTree.SetMaxRequestAttempts(a.config().GetInt(cfgTreePoolMaxAttempts))
interceptors := []grpc.DialOption{ interceptors := []grpc.DialOption{
grpc.WithUnaryInterceptor(grpctracing.NewUnaryClientInteceptor()), grpc.WithUnaryInterceptor(grpctracing.NewUnaryClientInteceptor()),
grpc.WithStreamInterceptor(grpctracing.NewStreamClientInterceptor()), grpc.WithStreamInterceptor(grpctracing.NewStreamClientInterceptor()),
grpc.WithContextDialer(a.settings.dialerSource.GrpcContextDialer()), grpc.WithContextDialer(a.settings.dialerSource.GrpcContextDialer()),
grpc.WithChainUnaryInterceptor(qostagging.NewUnaryClientInteceptor()),
grpc.WithChainStreamInterceptor(qostagging.NewStreamClientInterceptor()),
} }
prm.SetGRPCDialOptions(interceptors...) prm.SetGRPCDialOptions(interceptors...)
prmTree.SetGRPCDialOptions(interceptors...) prmTree.SetGRPCDialOptions(interceptors...)
p, err := pool.NewPool(prm) p, err := pool.NewPool(prm)
if err != nil { if err != nil {
a.log.Fatal(logs.FailedToCreateConnectionPool, zap.Error(err)) a.log.Fatal(logs.FailedToCreateConnectionPool, zap.Error(err), logs.TagField(logs.TagApp))
} }
if err = p.Dial(ctx); err != nil { if err = p.Dial(ctx); err != nil {
a.log.Fatal(logs.FailedToDialConnectionPool, zap.Error(err)) a.log.Fatal(logs.FailedToDialConnectionPool, zap.Error(err), logs.TagField(logs.TagApp))
} }
if a.cfg.GetBool(cfgTreePoolNetmapSupport) { if a.config().GetBool(cfgTreePoolNetmapSupport) {
prmTree.SetNetMapInfoSource(frostfs.NewSource(frostfs.NewFrostFS(p, key), a.cache)) prmTree.SetNetMapInfoSource(frostfs.NewSource(frostfs.NewFrostFS(p, key), a.cache))
} }
treePool, err := treepool.NewPool(prmTree) treePool, err := treepool.NewPool(prmTree)
if err != nil { if err != nil {
a.log.Fatal(logs.FailedToCreateTreePool, zap.Error(err)) a.log.Fatal(logs.FailedToCreateTreePool, zap.Error(err), logs.TagField(logs.TagApp))
} }
if err = treePool.Dial(ctx); err != nil { if err = treePool.Dial(ctx); err != nil {
a.log.Fatal(logs.FailedToDialTreePool, zap.Error(err)) a.log.Fatal(logs.FailedToDialTreePool, zap.Error(err), logs.TagField(logs.TagApp))
} }
a.treePool = treePool a.treePool = treePool
@ -853,6 +915,7 @@ func (a *App) Wait() {
a.log.Info(logs.ApplicationStarted, a.log.Info(logs.ApplicationStarted,
zap.String("name", "frostfs-s3-gw"), zap.String("name", "frostfs-s3-gw"),
zap.String("version", version.Version), zap.String("version", version.Version),
logs.TagField(logs.TagApp),
) )
a.metrics.State().SetVersion(version.Version) a.metrics.State().SetVersion(version.Version)
@ -860,7 +923,7 @@ func (a *App) Wait() {
<-a.webDone // wait for web-server to be stopped <-a.webDone // wait for web-server to be stopped
a.log.Info(logs.ApplicationFinished) a.log.Info(logs.ApplicationFinished, logs.TagField(logs.TagApp))
} }
func (a *App) setHealthStatus() { func (a *App) setHealthStatus() {
@ -895,10 +958,10 @@ func (a *App) Serve(ctx context.Context) {
srv := new(http.Server) srv := new(http.Server)
srv.Handler = chiRouter srv.Handler = chiRouter
srv.ErrorLog = zap.NewStdLog(a.log) srv.ErrorLog = zap.NewStdLog(a.log)
srv.ReadTimeout = a.cfg.GetDuration(cfgWebReadTimeout) srv.ReadTimeout = a.config().GetDuration(cfgWebReadTimeout)
srv.ReadHeaderTimeout = a.cfg.GetDuration(cfgWebReadHeaderTimeout) srv.ReadHeaderTimeout = a.config().GetDuration(cfgWebReadHeaderTimeout)
srv.WriteTimeout = a.cfg.GetDuration(cfgWebWriteTimeout) srv.WriteTimeout = a.config().GetDuration(cfgWebWriteTimeout)
srv.IdleTimeout = a.cfg.GetDuration(cfgWebIdleTimeout) srv.IdleTimeout = a.config().GetDuration(cfgWebIdleTimeout)
a.startServices() a.startServices()
@ -906,11 +969,11 @@ func (a *App) Serve(ctx context.Context) {
for i := range servs { for i := range servs {
go func(i int) { go func(i int) {
a.log.Info(logs.StartingServer, zap.String("address", servs[i].Address())) a.log.Info(logs.StartingServer, zap.String("address", servs[i].Address()), logs.TagField(logs.TagApp))
if err := srv.Serve(servs[i].Listener()); err != nil && err != http.ErrServerClosed { if err := srv.Serve(servs[i].Listener()); err != nil && err != http.ErrServerClosed {
a.metrics.MarkUnhealthy(servs[i].Address()) a.metrics.MarkUnhealthy(servs[i].Address())
a.log.Fatal(logs.ListenAndServe, zap.Error(err)) a.log.Fatal(logs.ListenAndServe, zap.Error(err), logs.TagField(logs.TagApp))
} }
}(i) }(i)
} }
@ -935,7 +998,7 @@ LOOP:
ctx, cancel := shutdownContext() ctx, cancel := shutdownContext()
defer cancel() defer cancel()
a.log.Info(logs.StoppingServer, zap.Error(srv.Shutdown(ctx))) a.log.Info(logs.StoppingServer, zap.Error(srv.Shutdown(ctx)), logs.TagField(logs.TagApp))
a.metrics.Shutdown() a.metrics.Shutdown()
a.stopServices() a.stopServices()
@ -949,23 +1012,23 @@ func shutdownContext() (context.Context, context.CancelFunc) {
} }
func (a *App) configReload(ctx context.Context) { func (a *App) configReload(ctx context.Context) {
a.log.Info(logs.SIGHUPConfigReloadStarted) a.log.Info(logs.SIGHUPConfigReloadStarted, logs.TagField(logs.TagApp))
if !a.cfg.IsSet(cmdConfig) && !a.cfg.IsSet(cmdConfigDir) { if !a.config().IsSet(cmdConfig) && !a.config().IsSet(cmdConfigDir) {
a.log.Warn(logs.FailedToReloadConfigBecauseItsMissed) a.log.Warn(logs.FailedToReloadConfigBecauseItsMissed, logs.TagField(logs.TagApp))
return return
} }
if err := readInConfig(a.cfg); err != nil { if err := a.cfg.reload(); err != nil {
a.log.Warn(logs.FailedToReloadConfig, zap.Error(err)) a.log.Warn(logs.FailedToReloadConfig, zap.Error(err), logs.TagField(logs.TagApp))
return return
} }
if err := a.bucketResolver.UpdateResolvers(a.getResolverOrder()); err != nil { if err := a.bucketResolver.UpdateResolvers(a.getResolverOrder()); err != nil {
a.log.Warn(logs.FailedToReloadResolvers, zap.Error(err)) a.log.Warn(logs.FailedToReloadResolvers, zap.Error(err), logs.TagField(logs.TagApp))
} }
if err := a.updateServers(); err != nil { if err := a.updateServers(); err != nil {
a.log.Warn(logs.FailedToReloadServerParameters, zap.Error(err)) a.log.Warn(logs.FailedToReloadServerParameters, zap.Error(err), logs.TagField(logs.TagApp))
} }
a.setRuntimeParameters() a.setRuntimeParameters()
@ -975,41 +1038,45 @@ func (a *App) configReload(ctx context.Context) {
a.updateSettings() a.updateSettings()
a.metrics.SetEnabled(a.cfg.GetBool(cfgPrometheusEnabled)) a.metrics.SetEnabled(a.config().GetBool(cfgPrometheusEnabled))
a.initTracing(ctx) a.initTracing(ctx)
a.setHealthStatus() a.setHealthStatus()
a.log.Info(logs.SIGHUPConfigReloadCompleted) a.log.Info(logs.SIGHUPConfigReloadCompleted, logs.TagField(logs.TagApp))
} }
func (a *App) updateSettings() { func (a *App) updateSettings() {
if lvl, err := getLogLevel(a.cfg); err != nil { if lvl, err := getLogLevel(a.config()); err != nil {
a.log.Warn(logs.LogLevelWontBeUpdated, zap.Error(err)) a.log.Warn(logs.LogLevelWontBeUpdated, zap.Error(err), logs.TagField(logs.TagApp))
} else { } else {
a.settings.logLevel.SetLevel(lvl) a.settings.logLevel.SetLevel(lvl)
} }
if err := a.settings.dialerSource.Update(fetchMultinetConfig(a.cfg, a.log)); err != nil { if err := a.settings.dialerSource.Update(fetchMultinetConfig(a.config(), a.log)); err != nil {
a.log.Warn(logs.MultinetConfigWontBeUpdated, zap.Error(err)) a.log.Warn(logs.MultinetConfigWontBeUpdated, zap.Error(err), logs.TagField(logs.TagApp))
} }
a.settings.update(a.cfg, a.log) if err := a.settings.tagsConfig.update(a.config()); err != nil {
a.log.Warn(logs.TagsLogConfigWontBeUpdated, zap.Error(err), logs.TagField(logs.TagApp))
}
a.settings.update(a.config(), a.log)
} }
func (a *App) startServices() { func (a *App) startServices() {
a.services = a.services[:0] a.services = a.services[:0]
pprofService := NewPprofService(a.cfg, a.log) pprofService := NewPprofService(a.config(), a.log)
a.services = append(a.services, pprofService) a.services = append(a.services, pprofService)
go pprofService.Start() go pprofService.Start()
prometheusService := NewPrometheusService(a.cfg, a.log, a.metrics.Handler()) prometheusService := NewPrometheusService(a.config(), a.log, a.metrics.Handler())
a.services = append(a.services, prometheusService) a.services = append(a.services, prometheusService)
go prometheusService.Start() go prometheusService.Start()
} }
func (a *App) initServers(ctx context.Context) { func (a *App) initServers(ctx context.Context) {
serversInfo := fetchServers(a.cfg, a.log) serversInfo := fetchServers(a.config(), a.log)
a.servers = make([]Server, 0, len(serversInfo)) a.servers = make([]Server, 0, len(serversInfo))
for _, serverInfo := range serversInfo { for _, serverInfo := range serversInfo {
@ -1021,22 +1088,22 @@ func (a *App) initServers(ctx context.Context) {
if err != nil { if err != nil {
a.unbindServers = append(a.unbindServers, serverInfo) a.unbindServers = append(a.unbindServers, serverInfo)
a.metrics.MarkUnhealthy(serverInfo.Address) a.metrics.MarkUnhealthy(serverInfo.Address)
a.log.Warn(logs.FailedToAddServer, append(fields, zap.Error(err))...) a.log.Warn(logs.FailedToAddServer, append(fields, zap.Error(err), logs.TagField(logs.TagApp))...)
continue continue
} }
a.metrics.MarkHealthy(serverInfo.Address) a.metrics.MarkHealthy(serverInfo.Address)
a.servers = append(a.servers, srv) a.servers = append(a.servers, srv)
a.log.Info(logs.AddServer, fields...) a.log.Info(logs.AddServer, append(fields, logs.TagField(logs.TagApp))...)
} }
if len(a.servers) == 0 { if len(a.servers) == 0 {
a.log.Fatal(logs.NoHealthyServers) a.log.Fatal(logs.NoHealthyServers, logs.TagField(logs.TagApp))
} }
} }
func (a *App) updateServers() error { func (a *App) updateServers() error {
serversInfo := fetchServers(a.cfg, a.log) serversInfo := fetchServers(a.config(), a.log)
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
@ -1049,8 +1116,8 @@ func (a *App) updateServers() error {
if err := ser.UpdateCert(serverInfo.TLS.CertFile, serverInfo.TLS.KeyFile); err != nil { if err := ser.UpdateCert(serverInfo.TLS.CertFile, serverInfo.TLS.KeyFile); err != nil {
return fmt.Errorf("failed to update tls certs: %w", err) return fmt.Errorf("failed to update tls certs: %w", err)
} }
found = true
} }
found = true
} else if unbind := a.updateUnbindServerInfo(serverInfo); unbind { } else if unbind := a.updateUnbindServerInfo(serverInfo); unbind {
found = true found = true
} }
@ -1135,7 +1202,7 @@ func (a *App) initHandler() {
a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid) a.api, err = handler.New(a.log, a.obj, a.settings, a.policyStorage, a.frostfsid)
if err != nil { if err != nil {
a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err)) a.log.Fatal(logs.CouldNotInitializeAPIHandler, zap.Error(err), logs.TagField(logs.TagApp))
} }
} }
@ -1167,16 +1234,18 @@ func (a *App) getServers() []Server {
func (a *App) setRuntimeParameters() { func (a *App) setRuntimeParameters() {
if len(os.Getenv("GOMEMLIMIT")) != 0 { if len(os.Getenv("GOMEMLIMIT")) != 0 {
// default limit < yaml limit < app env limit < GOMEMLIMIT // default limit < yaml limit < app env limit < GOMEMLIMIT
a.log.Warn(logs.RuntimeSoftMemoryDefinedWithGOMEMLIMIT) a.log.Warn(logs.RuntimeSoftMemoryDefinedWithGOMEMLIMIT, logs.TagField(logs.TagApp))
return return
} }
softMemoryLimit := fetchSoftMemoryLimit(a.cfg) softMemoryLimit := fetchSoftMemoryLimit(a.config())
previous := debug.SetMemoryLimit(softMemoryLimit) previous := debug.SetMemoryLimit(softMemoryLimit)
if softMemoryLimit != previous { if softMemoryLimit != previous {
a.log.Info(logs.RuntimeSoftMemoryLimitUpdated, a.log.Info(logs.RuntimeSoftMemoryLimitUpdated,
zap.Int64("new_value", softMemoryLimit), zap.Int64("new_value", softMemoryLimit),
zap.Int64("old_value", previous)) zap.Int64("old_value", previous),
logs.TagField(logs.TagApp),
)
} }
} }
@ -1202,7 +1271,7 @@ func (a *App) tryReconnect(ctx context.Context, sr *http.Server) bool {
a.mu.Lock() a.mu.Lock()
defer a.mu.Unlock() defer a.mu.Unlock()
a.log.Info(logs.ServerReconnecting) a.log.Info(logs.ServerReconnecting, logs.TagField(logs.TagApp))
var failedServers []ServerInfo var failedServers []ServerInfo
for _, serverInfo := range a.unbindServers { for _, serverInfo := range a.unbindServers {
@ -1213,23 +1282,23 @@ func (a *App) tryReconnect(ctx context.Context, sr *http.Server) bool {
srv, err := newServer(ctx, serverInfo) srv, err := newServer(ctx, serverInfo)
if err != nil { if err != nil {
a.log.Warn(logs.ServerReconnectFailed, zap.Error(err)) a.log.Warn(logs.ServerReconnectFailed, zap.Error(err), logs.TagField(logs.TagApp))
failedServers = append(failedServers, serverInfo) failedServers = append(failedServers, serverInfo)
a.metrics.MarkUnhealthy(serverInfo.Address) a.metrics.MarkUnhealthy(serverInfo.Address)
continue continue
} }
go func() { go func() {
a.log.Info(logs.StartingServer, zap.String("address", srv.Address())) a.log.Info(logs.StartingServer, zap.String("address", srv.Address()), logs.TagField(logs.TagApp))
a.metrics.MarkHealthy(serverInfo.Address) a.metrics.MarkHealthy(serverInfo.Address)
if err = sr.Serve(srv.Listener()); err != nil && !errors.Is(err, http.ErrServerClosed) { if err = sr.Serve(srv.Listener()); err != nil && !errors.Is(err, http.ErrServerClosed) {
a.log.Warn(logs.ListenAndServe, zap.Error(err)) a.log.Warn(logs.ListenAndServe, zap.Error(err), logs.TagField(logs.TagApp))
a.metrics.MarkUnhealthy(serverInfo.Address) a.metrics.MarkUnhealthy(serverInfo.Address)
} }
}() }()
a.servers = append(a.servers, srv) a.servers = append(a.servers, srv)
a.log.Info(logs.ServerReconnectedSuccessfully, fields...) a.log.Info(logs.ServerReconnectedSuccessfully, append(fields, logs.TagField(logs.TagApp))...)
} }
a.unbindServers = failedServers a.unbindServers = failedServers
@ -1247,7 +1316,7 @@ func (a *App) fetchContainerInfo(ctx context.Context, cfgKey string) (info *data
} }
func (a *App) resolveContainerID(ctx context.Context, cfgKey string) (cid.ID, error) { func (a *App) resolveContainerID(ctx context.Context, cfgKey string) (cid.ID, error) {
containerString := a.cfg.GetString(cfgKey) containerString := a.config().GetString(cfgKey)
var id cid.ID var id cid.ID
if err := id.DecodeString(containerString); err != nil { if err := id.DecodeString(containerString); err != nil {

View file

@ -10,6 +10,7 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
@ -20,20 +21,13 @@ import (
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version" "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/version"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool" "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/pool"
"git.frostfs.info/TrueCloudLab/zapjournald"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"github.com/spf13/viper" "github.com/spf13/viper"
"github.com/ssgreg/journald"
"go.uber.org/zap" "go.uber.org/zap"
"go.uber.org/zap/zapcore" "go.uber.org/zap/zapcore"
) )
const ( const wildcardPlaceholder = "<wildcard>"
destinationStdout = "stdout"
destinationJournald = "journald"
wildcardPlaceholder = "<wildcard>"
)
const ( const (
defaultRebalanceInterval = 60 * time.Second defaultRebalanceInterval = 60 * time.Second
@ -88,7 +82,8 @@ var (
defaultDefaultNamespaces = []string{"", "root"} defaultDefaultNamespaces = []string{"", "root"}
) )
const ( // Settings. // Settings.
const (
// Logger. // Logger.
cfgLoggerLevel = "logger.level" cfgLoggerLevel = "logger.level"
cfgLoggerDestination = "logger.destination" cfgLoggerDestination = "logger.destination"
@ -98,6 +93,11 @@ const ( // Settings.
cfgLoggerSamplingThereafter = "logger.sampling.thereafter" cfgLoggerSamplingThereafter = "logger.sampling.thereafter"
cfgLoggerSamplingInterval = "logger.sampling.interval" cfgLoggerSamplingInterval = "logger.sampling.interval"
cfgLoggerTags = "logger.tags"
cfgLoggerTagsPrefixTmpl = cfgLoggerTags + ".%d."
cfgLoggerTagsNameTmpl = cfgLoggerTagsPrefixTmpl + "name"
cfgLoggerTagsLevelTmpl = cfgLoggerTagsPrefixTmpl + "level"
// HttpLogging. // HttpLogging.
cfgHTTPLoggingEnabled = "http_logging.enabled" cfgHTTPLoggingEnabled = "http_logging.enabled"
cfgHTTPLoggingMaxBody = "http_logging.max_body" cfgHTTPLoggingMaxBody = "http_logging.max_body"
@ -122,6 +122,7 @@ const ( // Settings.
// Pool config. // Pool config.
cfgConnectTimeout = "connect_timeout" cfgConnectTimeout = "connect_timeout"
cfgStreamTimeout = "stream_timeout" cfgStreamTimeout = "stream_timeout"
cfgTreeStreamTimeout = "tree_stream_timeout"
cfgHealthcheckTimeout = "healthcheck_timeout" cfgHealthcheckTimeout = "healthcheck_timeout"
cfgRebalanceInterval = "rebalance_interval" cfgRebalanceInterval = "rebalance_interval"
cfgPoolErrorThreshold = "pool_error_threshold" cfgPoolErrorThreshold = "pool_error_threshold"
@ -201,6 +202,8 @@ const ( // Settings.
cfgKludgeBypassContentEncodingCheckInChunks = "kludge.bypass_content_encoding_check_in_chunks" cfgKludgeBypassContentEncodingCheckInChunks = "kludge.bypass_content_encoding_check_in_chunks"
cfgKludgeDefaultNamespaces = "kludge.default_namespaces" cfgKludgeDefaultNamespaces = "kludge.default_namespaces"
cfgKludgeProfile = "kludge.profile" cfgKludgeProfile = "kludge.profile"
cfgKludgeListingKeepAliveThrottle = "kludge.listing_keepalive_throttle"
// Web. // Web.
cfgWebReadTimeout = "web.read_timeout" cfgWebReadTimeout = "web.read_timeout"
cfgWebReadHeaderTimeout = "web.read_header_timeout" cfgWebReadHeaderTimeout = "web.read_header_timeout"
@ -301,6 +304,49 @@ var ignore = map[string]struct{}{
cmdVersion: {}, cmdVersion: {},
} }
type appCfg struct {
flags *pflag.FlagSet
mu sync.RWMutex
settings *viper.Viper
}
func (a *appCfg) reload() error {
old := a.config()
v, err := newViper(a.flags)
if err != nil {
return err
}
if old.IsSet(cmdConfig) {
v.Set(cmdConfig, old.Get(cmdConfig))
}
if old.IsSet(cmdConfigDir) {
v.Set(cmdConfigDir, old.Get(cmdConfigDir))
}
if err = readInConfig(v); err != nil {
return err
}
a.setConfig(v)
return nil
}
func (a *appCfg) config() *viper.Viper {
a.mu.RLock()
defer a.mu.RUnlock()
return a.settings
}
func (a *appCfg) setConfig(v *viper.Viper) {
a.mu.Lock()
a.settings = v
a.mu.Unlock()
}
func fetchConnectTimeout(cfg *viper.Viper) time.Duration { func fetchConnectTimeout(cfg *viper.Viper) time.Duration {
connTimeout := cfg.GetDuration(cfgConnectTimeout) connTimeout := cfg.GetDuration(cfgConnectTimeout)
if connTimeout <= 0 { if connTimeout <= 0 {
@ -319,8 +365,8 @@ func fetchReconnectInterval(cfg *viper.Viper) time.Duration {
return reconnect return reconnect
} }
func fetchStreamTimeout(cfg *viper.Viper) time.Duration { func fetchStreamTimeout(cfg *viper.Viper, cfgEntry string) time.Duration {
streamTimeout := cfg.GetDuration(cfgStreamTimeout) streamTimeout := cfg.GetDuration(cfgEntry)
if streamTimeout <= 0 { if streamTimeout <= 0 {
streamTimeout = defaultStreamTimeout streamTimeout = defaultStreamTimeout
} }
@ -425,14 +471,18 @@ func fetchDefaultPolicy(l *zap.Logger, cfg *viper.Viper) netmap.PlacementPolicy
policyStr := cfg.GetString(cfgPolicyDefault) policyStr := cfg.GetString(cfgPolicyDefault)
if err := policy.DecodeString(policyStr); err != nil { if err := policy.DecodeString(policyStr); err != nil {
l.Warn(logs.FailedToParseDefaultLocationConstraint, l.Warn(logs.FailedToParseDefaultLocationConstraint,
zap.String("policy", policyStr), zap.String("default", defaultPlacementPolicy), zap.Error(err)) zap.String("policy", policyStr), zap.String("default", defaultPlacementPolicy),
zap.Error(err), logs.TagField(logs.TagApp))
} else { } else {
return policy return policy
} }
} }
if err := policy.DecodeString(defaultPlacementPolicy); err != nil { if err := policy.DecodeString(defaultPlacementPolicy); err != nil {
l.Fatal(logs.FailedToParseDefaultDefaultLocationConstraint, zap.String("policy", defaultPlacementPolicy)) l.Fatal(logs.FailedToParseDefaultDefaultLocationConstraint,
zap.String("policy", defaultPlacementPolicy),
logs.TagField(logs.TagApp),
)
} }
return policy return policy
@ -445,7 +495,9 @@ func fetchCacheLifetime(v *viper.Viper, l *zap.Logger, cfgEntry string, defaultV
l.Error(logs.InvalidLifetimeUsingDefaultValue, l.Error(logs.InvalidLifetimeUsingDefaultValue,
zap.String("parameter", cfgEntry), zap.String("parameter", cfgEntry),
zap.Duration("value in config", lifetime), zap.Duration("value in config", lifetime),
zap.Duration("default", defaultValue)) zap.Duration("default", defaultValue),
logs.TagField(logs.TagApp),
)
} else { } else {
return lifetime return lifetime
} }
@ -461,7 +513,9 @@ func fetchCacheSize(v *viper.Viper, l *zap.Logger, cfgEntry string, defaultValue
l.Error(logs.InvalidCacheSizeUsingDefaultValue, l.Error(logs.InvalidCacheSizeUsingDefaultValue,
zap.String("parameter", cfgEntry), zap.String("parameter", cfgEntry),
zap.Int("value in config", size), zap.Int("value in config", size),
zap.Int("default", defaultValue)) zap.Int("default", defaultValue),
logs.TagField(logs.TagApp),
)
} else { } else {
return size return size
} }
@ -483,7 +537,8 @@ func fetchRemovingCheckInterval(v *viper.Viper, l *zap.Logger) time.Duration {
l.Error(logs.InvalidAccessBoxCacheRemovingCheckInterval, l.Error(logs.InvalidAccessBoxCacheRemovingCheckInterval,
zap.String("parameter", cfgAccessBoxCacheRemovingCheckInterval), zap.String("parameter", cfgAccessBoxCacheRemovingCheckInterval),
zap.Duration("value in config", duration), zap.Duration("value in config", duration),
zap.Duration("default", defaultAccessBoxCacheRemovingCheckInterval)) zap.Duration("default", defaultAccessBoxCacheRemovingCheckInterval),
logs.TagField(logs.TagApp))
return defaultAccessBoxCacheRemovingCheckInterval return defaultAccessBoxCacheRemovingCheckInterval
} }
@ -497,7 +552,9 @@ func fetchDefaultMaxAge(cfg *viper.Viper, l *zap.Logger) int {
if defaultMaxAge <= 0 && defaultMaxAge != -1 { if defaultMaxAge <= 0 && defaultMaxAge != -1 {
l.Fatal(logs.InvalidDefaultMaxAge, l.Fatal(logs.InvalidDefaultMaxAge,
zap.String("parameter", cfgDefaultMaxAge), zap.String("parameter", cfgDefaultMaxAge),
zap.String("value in config", strconv.Itoa(defaultMaxAge))) zap.String("value in config", strconv.Itoa(defaultMaxAge)),
logs.TagField(logs.TagApp),
)
} }
} }
@ -508,14 +565,19 @@ func fetchRegionMappingPolicies(l *zap.Logger, cfg *viper.Viper) map[string]netm
filepath := cfg.GetString(cfgPolicyRegionMapFile) filepath := cfg.GetString(cfgPolicyRegionMapFile)
regionPolicyMap, err := readRegionMap(filepath) regionPolicyMap, err := readRegionMap(filepath)
if err != nil { if err != nil {
l.Warn(logs.FailedToReadRegionMapFilePolicies, zap.String("file", filepath), zap.Error(err)) l.Warn(logs.FailedToReadRegionMapFilePolicies,
zap.String("file", filepath),
zap.Error(err),
logs.TagField(logs.TagApp))
return make(map[string]netmap.PlacementPolicy) return make(map[string]netmap.PlacementPolicy)
} }
regionMap := make(map[string]netmap.PlacementPolicy, len(regionPolicyMap)) regionMap := make(map[string]netmap.PlacementPolicy, len(regionPolicyMap))
for region, policy := range regionPolicyMap { for region, policy := range regionPolicyMap {
if region == api.DefaultLocationConstraint { if region == api.DefaultLocationConstraint {
l.Warn(logs.DefaultLocationConstraintCantBeOverriden, zap.String("policy", policy)) l.Warn(logs.DefaultLocationConstraintCantBeOverriden,
zap.String("policy", policy),
logs.TagField(logs.TagApp))
continue continue
} }
@ -530,7 +592,10 @@ func fetchRegionMappingPolicies(l *zap.Logger, cfg *viper.Viper) map[string]netm
continue continue
} }
l.Warn(logs.FailedToParseLocationConstraint, zap.String("region", region), zap.String("policy", policy)) l.Warn(logs.FailedToParseLocationConstraint,
zap.String("region", region),
zap.String("policy", policy),
logs.TagField(logs.TagApp))
} }
return regionMap return regionMap
@ -562,7 +627,11 @@ func fetchDefaultCopiesNumbers(l *zap.Logger, v *viper.Viper) []uint32 {
parsedValue, err := strconv.ParseUint(unparsed[i], 10, 32) parsedValue, err := strconv.ParseUint(unparsed[i], 10, 32)
if err != nil { if err != nil {
l.Warn(logs.FailedToParseDefaultCopiesNumbers, l.Warn(logs.FailedToParseDefaultCopiesNumbers,
zap.Strings("copies numbers", unparsed), zap.Uint32s("default", defaultCopiesNumbers), zap.Error(err)) zap.Strings("copies numbers", unparsed),
zap.Uint32s("default", defaultCopiesNumbers),
zap.Error(err),
logs.TagField(logs.TagApp),
)
return defaultCopiesNumbers return defaultCopiesNumbers
} }
result[i] = uint32(parsedValue) result[i] = uint32(parsedValue)
@ -618,15 +687,17 @@ func fetchCopiesNumbers(l *zap.Logger, v *viper.Viper) map[string][]uint32 {
for j := range vector { for j := range vector {
parsedValue, err := strconv.ParseUint(vector[j], 10, 32) parsedValue, err := strconv.ParseUint(vector[j], 10, 32)
if err != nil { if err != nil {
l.Warn(logs.FailedToParseCopiesNumbers, zap.String("location", constraint), l.Warn(logs.FailedToParseCopiesNumbers,
zap.Strings("copies numbers", vector), zap.Error(err)) zap.String("location", constraint),
zap.Strings("copies numbers", vector), zap.Error(err),
logs.TagField(logs.TagApp))
continue continue
} }
vector32[j] = uint32(parsedValue) vector32[j] = uint32(parsedValue)
} }
copiesNums[constraint] = vector32 copiesNums[constraint] = vector32
l.Info(logs.ConstraintAdded, zap.String("location", constraint), zap.Strings("copies numbers", vector)) l.Info(logs.ConstraintAdded, zap.String("location", constraint), zap.Strings("copies numbers", vector), logs.TagField(logs.TagApp))
} }
return copiesNums return copiesNums
} }
@ -635,7 +706,9 @@ func fetchDefaultNamespaces(l *zap.Logger, v *viper.Viper) []string {
defaultNamespaces := v.GetStringSlice(cfgKludgeDefaultNamespaces) defaultNamespaces := v.GetStringSlice(cfgKludgeDefaultNamespaces)
if len(defaultNamespaces) == 0 { if len(defaultNamespaces) == 0 {
defaultNamespaces = defaultDefaultNamespaces defaultNamespaces = defaultDefaultNamespaces
l.Warn(logs.DefaultNamespacesCannotBeEmpty, zap.Strings("namespaces", defaultNamespaces)) l.Warn(logs.DefaultNamespacesCannotBeEmpty,
zap.Strings("namespaces", defaultNamespaces),
logs.TagField(logs.TagApp))
} }
for i := range defaultNamespaces { // to be set namespaces in env variable as `S3_GW_KLUDGE_DEFAULT_NAMESPACES="" 'root'` for i := range defaultNamespaces { // to be set namespaces in env variable as `S3_GW_KLUDGE_DEFAULT_NAMESPACES="" 'root'`
@ -659,7 +732,7 @@ func fetchNamespacesConfig(l *zap.Logger, v *viper.Viper) (NamespacesConfig, []s
nsConfig, err := readNamespacesConfig(v.GetString(cfgNamespacesConfig)) nsConfig, err := readNamespacesConfig(v.GetString(cfgNamespacesConfig))
if err != nil { if err != nil {
l.Warn(logs.FailedToParseNamespacesConfig, zap.Error(err)) l.Warn(logs.FailedToParseNamespacesConfig, zap.Error(err), logs.TagField(logs.TagApp))
} }
defaultNamespacesNames := fetchDefaultNamespaces(l, v) defaultNamespacesNames := fetchDefaultNamespaces(l, v)
@ -673,11 +746,13 @@ func fetchNamespacesConfig(l *zap.Logger, v *viper.Viper) (NamespacesConfig, []s
} }
if len(overrideDefaults) > 0 { if len(overrideDefaults) > 0 {
l.Warn(logs.DefaultNamespaceConfigValuesBeOverwritten) l.Warn(logs.DefaultNamespaceConfigValuesBeOverwritten, logs.TagField(logs.TagApp))
defaultNSValue.LocationConstraints = overrideDefaults[0].LocationConstraints defaultNSValue.LocationConstraints = overrideDefaults[0].LocationConstraints
defaultNSValue.CopiesNumbers = overrideDefaults[0].CopiesNumbers defaultNSValue.CopiesNumbers = overrideDefaults[0].CopiesNumbers
if len(overrideDefaults) > 1 { if len(overrideDefaults) > 1 {
l.Warn(logs.MultipleDefaultOverridesFound, zap.String("name", overrideDefaults[0].Name)) l.Warn(logs.MultipleDefaultOverridesFound,
zap.String("name", overrideDefaults[0].Name),
logs.TagField(logs.TagApp))
} }
} }
@ -720,7 +795,7 @@ func fetchPeers(l *zap.Logger, v *viper.Viper) []pool.NodeParam {
priority := v.GetInt(key + "priority") priority := v.GetInt(key + "priority")
if address == "" { if address == "" {
l.Warn(logs.SkipEmptyAddress) l.Warn(logs.SkipEmptyAddress, logs.TagField(logs.TagApp))
break break
} }
if weight <= 0 { // unspecified or wrong if weight <= 0 { // unspecified or wrong
@ -735,7 +810,9 @@ func fetchPeers(l *zap.Logger, v *viper.Viper) []pool.NodeParam {
l.Info(logs.AddedStoragePeer, l.Info(logs.AddedStoragePeer,
zap.Int("priority", priority), zap.Int("priority", priority),
zap.String("address", address), zap.String("address", address),
zap.Float64("weight", weight)) zap.Float64("weight", weight),
logs.TagField(logs.TagApp),
)
} }
return nodes return nodes
@ -759,7 +836,7 @@ func fetchServers(v *viper.Viper, log *zap.Logger) []ServerInfo {
} }
if _, ok := seen[serverInfo.Address]; ok { if _, ok := seen[serverInfo.Address]; ok {
log.Warn(logs.WarnDuplicateAddress, zap.String("address", serverInfo.Address)) log.Warn(logs.WarnDuplicateAddress, zap.String("address", serverInfo.Address), logs.TagField(logs.TagApp))
continue continue
} }
seen[serverInfo.Address] = struct{}{} seen[serverInfo.Address] = struct{}{}
@ -788,13 +865,13 @@ func fetchVHSNamespaces(v *viper.Viper, log *zap.Logger) map[string]bool {
nsMap := v.GetStringMap(cfgVHSNamespaces) nsMap := v.GetStringMap(cfgVHSNamespaces)
for ns, val := range nsMap { for ns, val := range nsMap {
if _, ok := vhsNamespacesEnabled[ns]; ok { if _, ok := vhsNamespacesEnabled[ns]; ok {
log.Warn(logs.WarnDuplicateNamespaceVHS, zap.String("namespace", ns)) log.Warn(logs.WarnDuplicateNamespaceVHS, zap.String("namespace", ns), logs.TagField(logs.TagApp))
continue continue
} }
enabledFlag, ok := val.(bool) enabledFlag, ok := val.(bool)
if !ok { if !ok {
log.Warn(logs.WarnValueVHSEnabledFlagWrongType, zap.String("namespace", ns)) log.Warn(logs.WarnValueVHSEnabledFlagWrongType, zap.String("namespace", ns), logs.TagField(logs.TagApp))
continue continue
} }
@ -878,7 +955,42 @@ func fetchTombstoneWorkerPoolSize(v *viper.Viper) int {
return tombstoneWorkerPoolSize return tombstoneWorkerPoolSize
} }
func newSettings() *viper.Viper { func fetchLogTagsConfig(v *viper.Viper) (map[string]zapcore.Level, error) {
res := make(map[string]zapcore.Level)
defaultLevel := v.GetString(cfgLoggerLevel)
var defaultLvl zapcore.Level
if err := defaultLvl.Set(defaultLevel); err != nil {
return nil, fmt.Errorf("failed to parse log level, unknown level: '%s'", defaultLevel)
}
for i := 0; ; i++ {
name := v.GetString(fmt.Sprintf(cfgLoggerTagsNameTmpl, i))
if name == "" {
break
}
lvl := defaultLvl
level := v.GetString(fmt.Sprintf(cfgLoggerTagsLevelTmpl, i))
if level != "" {
if err := lvl.Set(level); err != nil {
return nil, fmt.Errorf("failed to parse log tags config, unknown level: '%s'", level)
}
}
res[name] = lvl
}
if len(res) == 0 && !v.IsSet(cfgLoggerTags) {
for _, tag := range defaultTags {
res[tag] = defaultLvl
}
}
return res, nil
}
func newViper(flags *pflag.FlagSet) (*viper.Viper, error) {
v := viper.New() v := viper.New()
v.AutomaticEnv() v.AutomaticEnv()
@ -887,6 +999,20 @@ func newSettings() *viper.Viper {
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
v.AllowEmptyEnv(true) v.AllowEmptyEnv(true)
if err := bindFlags(v, flags); err != nil {
return nil, err
}
setDefaults(v, flags)
if v.IsSet(cfgServer+".0."+cfgTLSKeyFile) && v.IsSet(cfgServer+".0."+cfgTLSCertFile) {
v.Set(cfgServer+".0."+cfgTLSEnabled, true)
}
return v, nil
}
func newSettings() *appCfg {
// flags setup: // flags setup:
flags := pflag.NewFlagSet("commandline", pflag.ExitOnError) flags := pflag.NewFlagSet("commandline", pflag.ExitOnError)
flags.SetOutput(os.Stdout) flags.SetOutput(os.Stdout)
@ -914,15 +1040,71 @@ func newSettings() *viper.Viper {
flags.String(cfgTLSCertFile, "", "TLS certificate file to use") flags.String(cfgTLSCertFile, "", "TLS certificate file to use")
flags.String(cfgTLSKeyFile, "", "TLS key file to use") flags.String(cfgTLSKeyFile, "", "TLS key file to use")
peers := flags.StringArrayP(cfgPeers, "p", nil, "set FrostFS nodes") flags.StringArrayP(cfgPeers, "p", nil, "set FrostFS nodes")
flags.StringP(cfgRPCEndpoint, "r", "", "set RPC endpoint") flags.StringP(cfgRPCEndpoint, "r", "", "set RPC endpoint")
resolveMethods := flags.StringSlice(cfgResolveOrder, []string{resolver.DNSResolver}, "set bucket name resolve order") flags.StringSlice(cfgResolveOrder, []string{resolver.DNSResolver}, "set bucket name resolve order")
domains := flags.StringSliceP(cfgListenDomains, "d", nil, "set domains to be listened") flags.StringSliceP(cfgListenDomains, "d", nil, "set domains to be listened")
// set defaults: if err := flags.Parse(os.Args); err != nil {
panic(err)
}
v, err := newViper(flags)
if err != nil {
panic(fmt.Errorf("bind flags: %w", err))
}
switch {
case help != nil && *help:
fmt.Printf("FrostFS S3 gateway %s\n", version.Version)
flags.PrintDefaults()
fmt.Println()
fmt.Println("Default environments:")
fmt.Println()
keys := v.AllKeys()
sort.Strings(keys)
for i := range keys {
if _, ok := ignore[keys[i]]; ok {
continue
}
defaultValue := v.GetString(keys[i])
if len(defaultValue) == 0 {
continue
}
k := strings.Replace(keys[i], ".", "_", -1)
fmt.Printf("%s_%s = %s\n", envPrefix, strings.ToUpper(k), defaultValue)
}
fmt.Println()
fmt.Println("Peers preset:")
fmt.Println()
fmt.Printf("%s_%s_[N]_ADDRESS = string\n", envPrefix, strings.ToUpper(cfgPeers))
fmt.Printf("%s_%s_[N]_WEIGHT = 0..1 (float)\n", envPrefix, strings.ToUpper(cfgPeers))
os.Exit(0)
case versionFlag != nil && *versionFlag:
fmt.Printf("FrostFS S3 Gateway\nVersion: %s\nGoVersion: %s\n", version.Version, runtime.Version())
os.Exit(0)
}
if err = readInConfig(v); err != nil {
panic(err)
}
return &appCfg{
flags: flags,
settings: v,
}
}
func setDefaults(v *viper.Viper, flags *pflag.FlagSet) {
v.SetDefault(cfgAccessBoxCacheRemovingCheckInterval, defaultAccessBoxCacheRemovingCheckInterval) v.SetDefault(cfgAccessBoxCacheRemovingCheckInterval, defaultAccessBoxCacheRemovingCheckInterval)
// logger: // logger:
@ -986,78 +1168,21 @@ func newSettings() *viper.Viper {
// multinet // multinet
v.SetDefault(cfgMultinetFallbackDelay, defaultMultinetFallbackDelay) v.SetDefault(cfgMultinetFallbackDelay, defaultMultinetFallbackDelay)
// Bind flags if resolveMethods, err := flags.GetStringSlice(cfgResolveOrder); err == nil {
if err := bindFlags(v, flags); err != nil { v.SetDefault(cfgResolveOrder, resolveMethods)
panic(fmt.Errorf("bind flags: %w", err))
} }
if err := flags.Parse(os.Args); err != nil { if peers, err := flags.GetStringArray(cfgPeers); err == nil {
panic(err) for i := range peers {
} v.SetDefault(cfgPeers+"."+strconv.Itoa(i)+".address", peers[i])
if v.IsSet(cfgServer+".0."+cfgTLSKeyFile) && v.IsSet(cfgServer+".0."+cfgTLSCertFile) {
v.Set(cfgServer+".0."+cfgTLSEnabled, true)
}
if resolveMethods != nil {
v.SetDefault(cfgResolveOrder, *resolveMethods)
}
if peers != nil && len(*peers) > 0 {
for i := range *peers {
v.SetDefault(cfgPeers+"."+strconv.Itoa(i)+".address", (*peers)[i])
v.SetDefault(cfgPeers+"."+strconv.Itoa(i)+".weight", 1) v.SetDefault(cfgPeers+"."+strconv.Itoa(i)+".weight", 1)
v.SetDefault(cfgPeers+"."+strconv.Itoa(i)+".priority", 1) v.SetDefault(cfgPeers+"."+strconv.Itoa(i)+".priority", 1)
} }
} }
if domains != nil && len(*domains) > 0 { if domains, err := flags.GetStringSlice(cfgListenDomains); err == nil && len(domains) > 0 {
v.SetDefault(cfgListenDomains, *domains) v.SetDefault(cfgListenDomains, domains)
} }
switch {
case help != nil && *help:
fmt.Printf("FrostFS S3 gateway %s\n", version.Version)
flags.PrintDefaults()
fmt.Println()
fmt.Println("Default environments:")
fmt.Println()
keys := v.AllKeys()
sort.Strings(keys)
for i := range keys {
if _, ok := ignore[keys[i]]; ok {
continue
}
defaultValue := v.GetString(keys[i])
if len(defaultValue) == 0 {
continue
}
k := strings.Replace(keys[i], ".", "_", -1)
fmt.Printf("%s_%s = %s\n", envPrefix, strings.ToUpper(k), defaultValue)
}
fmt.Println()
fmt.Println("Peers preset:")
fmt.Println()
fmt.Printf("%s_%s_[N]_ADDRESS = string\n", envPrefix, strings.ToUpper(cfgPeers))
fmt.Printf("%s_%s_[N]_WEIGHT = 0..1 (float)\n", envPrefix, strings.ToUpper(cfgPeers))
os.Exit(0)
case versionFlag != nil && *versionFlag:
fmt.Printf("FrostFS S3 Gateway\nVersion: %s\nGoVersion: %s\n", version.Version, runtime.Version())
os.Exit(0)
}
if err := readInConfig(v); err != nil {
panic(err)
}
return v
} }
func bindFlags(v *viper.Viper, flags *pflag.FlagSet) error { func bindFlags(v *viper.Viper, flags *pflag.FlagSet) error {
@ -1175,129 +1300,19 @@ type LoggerAppSettings interface {
DroppedLogsInc() DroppedLogsInc()
} }
func pickLogger(v *viper.Viper, settings LoggerAppSettings) *Logger {
lvl, err := getLogLevel(v)
if err != nil {
panic(err)
}
dest := v.GetString(cfgLoggerDestination)
switch dest {
case destinationStdout:
return newStdoutLogger(v, lvl, settings)
case destinationJournald:
return newJournaldLogger(v, lvl, settings)
default:
panic(fmt.Sprintf("wrong destination for logger: %s", dest))
}
}
// newStdoutLogger constructs a Logger instance for the current application.
// Panics on failure.
//
// Logger contains a logger is built from zap's production logging configuration with:
// - parameterized level (debug by default)
// - console encoding
// - ISO8601 time encoding
// - sampling intervals
//
// and atomic log level to dynamically change it.
//
// Logger records a stack trace for all messages at or above fatal level.
//
// See also zapcore.Level, zap.NewProductionConfig, zap.AddStacktrace.
func newStdoutLogger(v *viper.Viper, lvl zapcore.Level, settings LoggerAppSettings) *Logger {
stdout := zapcore.AddSync(os.Stderr)
level := zap.NewAtomicLevelAt(lvl)
consoleOutCore := zapcore.NewCore(newLogEncoder(), stdout, level)
consoleOutCore = applyZapCoreMiddlewares(consoleOutCore, v, settings)
return &Logger{
logger: zap.New(consoleOutCore, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel))),
lvl: level,
}
}
func newJournaldLogger(v *viper.Viper, lvl zapcore.Level, settings LoggerAppSettings) *Logger {
level := zap.NewAtomicLevelAt(lvl)
encoder := zapjournald.NewPartialEncoder(newLogEncoder(), zapjournald.SyslogFields)
core := zapjournald.NewCore(level, encoder, &journald.Journal{}, zapjournald.SyslogFields)
coreWithContext := core.With([]zapcore.Field{
zapjournald.SyslogFacility(zapjournald.LogDaemon),
zapjournald.SyslogIdentifier(),
zapjournald.SyslogPid(),
})
coreWithContext = applyZapCoreMiddlewares(coreWithContext, v, settings)
l := zap.New(coreWithContext, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel)))
return &Logger{
logger: l,
lvl: level,
}
}
func newLogEncoder() zapcore.Encoder {
c := zap.NewProductionEncoderConfig()
c.EncodeTime = zapcore.ISO8601TimeEncoder
return zapcore.NewConsoleEncoder(c)
}
func applyZapCoreMiddlewares(core zapcore.Core, v *viper.Viper, settings LoggerAppSettings) zapcore.Core {
if v.GetBool(cfgLoggerSamplingEnabled) {
core = zapcore.NewSamplerWithOptions(core,
v.GetDuration(cfgLoggerSamplingInterval),
v.GetInt(cfgLoggerSamplingInitial),
v.GetInt(cfgLoggerSamplingThereafter),
zapcore.SamplerHook(func(_ zapcore.Entry, dec zapcore.SamplingDecision) {
if dec&zapcore.LogDropped > 0 {
settings.DroppedLogsInc()
}
}))
}
return core
}
func getLogLevel(v *viper.Viper) (zapcore.Level, error) {
var lvl zapcore.Level
lvlStr := v.GetString(cfgLoggerLevel)
err := lvl.UnmarshalText([]byte(lvlStr))
if err != nil {
return lvl, fmt.Errorf("incorrect logger level configuration %s (%v), "+
"value should be one of %v", lvlStr, err, [...]zapcore.Level{
zapcore.DebugLevel,
zapcore.InfoLevel,
zapcore.WarnLevel,
zapcore.ErrorLevel,
zapcore.DPanicLevel,
zapcore.PanicLevel,
zapcore.FatalLevel,
})
}
return lvl, nil
}
func validateDomains(domains []string, log *zap.Logger) []string { func validateDomains(domains []string, log *zap.Logger) []string {
validDomains := make([]string, 0, len(domains)) validDomains := make([]string, 0, len(domains))
LOOP: LOOP:
for _, domain := range domains { for _, domain := range domains {
if strings.Contains(domain, ":") { if strings.Contains(domain, ":") {
log.Warn(logs.WarnDomainContainsPort, zap.String("domain", domain)) log.Warn(logs.WarnDomainContainsPort, zap.String("domain", domain), logs.TagField(logs.TagApp))
continue continue
} }
domainParts := strings.Split(domain, ".") domainParts := strings.Split(domain, ".")
for _, part := range domainParts { for _, part := range domainParts {
if strings.ContainsAny(part, "<>") && part != wildcardPlaceholder { if strings.ContainsAny(part, "<>") && part != wildcardPlaceholder {
log.Warn(logs.WarnDomainContainsInvalidPlaceholder, zap.String("domain", domain)) log.Warn(logs.WarnDomainContainsInvalidPlaceholder, zap.String("domain", domain), logs.TagField(logs.TagApp))
continue LOOP continue LOOP
} }
} }

View file

@ -0,0 +1,59 @@
package main
import (
"os"
"testing"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/resolver"
"github.com/stretchr/testify/require"
)
func TestConfigReload(t *testing.T) {
f, err := os.CreateTemp("", "conf")
require.NoError(t, err)
defer func() {
require.NoError(t, os.Remove(f.Name()))
}()
confData := `
pprof:
enabled: true
frostfsid:
contract: name.nns
resolve_order:
- nns
`
_, err = f.WriteString(confData)
require.NoError(t, err)
require.NoError(t, f.Close())
cfg := newSettings()
require.NoError(t, cfg.flags.Parse([]string{"--config", f.Name(), "--max_clients_count", "10"}))
require.NoError(t, cfg.reload())
require.True(t, cfg.config().GetBool(cfgPProfEnabled))
require.Equal(t, "name.nns", cfg.config().GetString(cfgFrostfsIDContract))
require.Equal(t, []string{resolver.NNSResolver}, cfg.config().GetStringSlice(cfgResolveOrder))
require.Equal(t, 10, cfg.config().GetInt(cfgMaxClientsCount))
require.NoError(t, os.Truncate(f.Name(), 0))
require.NoError(t, cfg.reload())
require.False(t, cfg.config().GetBool(cfgPProfEnabled))
require.Equal(t, "frostfsid.frostfs", cfg.config().GetString(cfgFrostfsIDContract))
require.Equal(t, []string{resolver.DNSResolver}, cfg.config().GetStringSlice(cfgResolveOrder))
require.Equal(t, 10, cfg.config().GetInt(cfgMaxClientsCount))
}
func TestSetTLSEnabled(t *testing.T) {
cfg := newSettings()
require.NoError(t, cfg.flags.Parse([]string{"--" + cfgTLSCertFile, "tls.crt", "--" + cfgTLSKeyFile, "tls.key"}))
require.NoError(t, cfg.reload())
require.True(t, cfg.config().GetBool(cfgServer+".0."+cfgTLSEnabled))
}

210
cmd/s3-gw/logger.go Normal file
View file

@ -0,0 +1,210 @@
package main
import (
"fmt"
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
"git.frostfs.info/TrueCloudLab/zapjournald"
"github.com/spf13/viper"
"github.com/ssgreg/journald"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
const (
destinationStdout string = "stdout"
destinationJournald string = "journald"
)
var defaultTags = []string{logs.TagApp, logs.TagDatapath, logs.TagExternalStorage, logs.TagExternalStorageTree, logs.TagExternalBlockchain}
type Logger struct {
logger *zap.Logger
lvl zap.AtomicLevel
}
func pickLogger(v *viper.Viper, loggerSettings LoggerAppSettings, tagSettings TagFilterSettings) *Logger {
dest := v.GetString(cfgLoggerDestination)
switch dest {
case destinationStdout:
return newStdoutLogger(v, loggerSettings, tagSettings)
case destinationJournald:
return newJournaldLogger(v, loggerSettings, tagSettings)
default:
panic(fmt.Sprintf("wrong destination for logger: %s", dest))
}
}
// newStdoutLogger constructs a Logger instance for the current application.
// Panics on failure.
//
// Logger contains a logger is built from zap's production logging configuration with:
// - parameterized level (debug by default)
// - console encoding
// - ISO8601 time encoding
// - sampling intervals
//
// and atomic log level to dynamically change it.
//
// Logger records a stack trace for all messages at or above fatal level.
//
// See also zapcore.Level, zap.NewProductionConfig, zap.AddStacktrace.
func newStdoutLogger(v *viper.Viper, loggerSettings LoggerAppSettings, tagSettings TagFilterSettings) *Logger {
c := newZapLogConfig(v)
out, errSink, err := openZapSinks(c)
if err != nil {
panic(fmt.Sprintf("open zap sinks: %v", err.Error()))
}
core := zapcore.NewCore(zapcore.NewConsoleEncoder(c.EncoderConfig), out, c.Level)
core = applyZapCoreMiddlewares(core, v, loggerSettings, tagSettings)
l := zap.New(core, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel)), zap.ErrorOutput(errSink))
return &Logger{logger: l, lvl: c.Level}
}
func newJournaldLogger(v *viper.Viper, logSettings LoggerAppSettings, tagSettings TagFilterSettings) *Logger {
c := newZapLogConfig(v)
// We can use NewJSONEncoder instead if, say, frontend
// would like to access journald logs and parse them easily.
encoder := zapjournald.NewPartialEncoder(zapcore.NewConsoleEncoder(c.EncoderConfig), zapjournald.SyslogFields)
journalCore := zapjournald.NewCore(c.Level, encoder, &journald.Journal{}, zapjournald.SyslogFields)
core := journalCore.With([]zapcore.Field{
zapjournald.SyslogFacility(zapjournald.LogDaemon),
zapjournald.SyslogIdentifier(),
zapjournald.SyslogPid(),
})
core = applyZapCoreMiddlewares(core, v, logSettings, tagSettings)
l := zap.New(core, zap.AddStacktrace(zap.NewAtomicLevelAt(zap.FatalLevel)))
return &Logger{logger: l, lvl: c.Level}
}
func openZapSinks(cfg zap.Config) (zapcore.WriteSyncer, zapcore.WriteSyncer, error) {
sink, closeOut, err := zap.Open(cfg.OutputPaths...)
if err != nil {
return nil, nil, err
}
errSink, _, err := zap.Open(cfg.ErrorOutputPaths...)
if err != nil {
closeOut()
return nil, nil, err
}
return sink, errSink, nil
}
var _ zapcore.Core = (*zapCoreTagFilterWrapper)(nil)
type zapCoreTagFilterWrapper struct {
core zapcore.Core
settings TagFilterSettings
extra []zap.Field
}
type TagFilterSettings interface {
LevelEnabled(tag string, lvl zapcore.Level) bool
}
func (c *zapCoreTagFilterWrapper) Enabled(level zapcore.Level) bool {
return c.core.Enabled(level)
}
func (c *zapCoreTagFilterWrapper) With(fields []zapcore.Field) zapcore.Core {
return &zapCoreTagFilterWrapper{
core: c.core.With(fields),
settings: c.settings,
extra: append(c.extra, fields...),
}
}
func (c *zapCoreTagFilterWrapper) Check(entry zapcore.Entry, checked *zapcore.CheckedEntry) *zapcore.CheckedEntry {
if c.core.Enabled(entry.Level) {
return checked.AddCore(entry, c)
}
return checked
}
func (c *zapCoreTagFilterWrapper) Write(entry zapcore.Entry, fields []zapcore.Field) error {
if c.shouldSkip(entry, fields) || c.shouldSkip(entry, c.extra) {
return nil
}
return c.core.Write(entry, fields)
}
func (c *zapCoreTagFilterWrapper) shouldSkip(entry zapcore.Entry, fields []zap.Field) bool {
for _, field := range fields {
if field.Key == logs.TagFieldName && field.Type == zapcore.StringType {
if !c.settings.LevelEnabled(field.String, entry.Level) {
return true
}
break
}
}
return false
}
func (c *zapCoreTagFilterWrapper) Sync() error {
return c.core.Sync()
}
func applyZapCoreMiddlewares(core zapcore.Core, v *viper.Viper, appSettings LoggerAppSettings, tagSettings TagFilterSettings) zapcore.Core {
core = &zapCoreTagFilterWrapper{
core: core,
settings: tagSettings,
}
if v.GetBool(cfgLoggerSamplingEnabled) {
core = zapcore.NewSamplerWithOptions(core,
v.GetDuration(cfgLoggerSamplingInterval),
v.GetInt(cfgLoggerSamplingInitial),
v.GetInt(cfgLoggerSamplingThereafter),
zapcore.SamplerHook(func(_ zapcore.Entry, dec zapcore.SamplingDecision) {
if dec&zapcore.LogDropped > 0 {
appSettings.DroppedLogsInc()
}
}))
}
return core
}
func newZapLogConfig(v *viper.Viper) zap.Config {
lvl, err := getLogLevel(v)
if err != nil {
panic(err)
}
c := zap.Config{
Level: zap.NewAtomicLevelAt(lvl),
EncoderConfig: zap.NewProductionEncoderConfig(),
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
}
c.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
return c
}
func getLogLevel(v *viper.Viper) (zapcore.Level, error) {
var lvl zapcore.Level
lvlStr := v.GetString(cfgLoggerLevel)
err := lvl.UnmarshalText([]byte(lvlStr))
if err != nil {
return lvl, fmt.Errorf("incorrect logger level configuration %s (%v), "+
"value should be one of %v", lvlStr, err, [...]zapcore.Level{
zapcore.DebugLevel,
zapcore.InfoLevel,
zapcore.WarnLevel,
zapcore.ErrorLevel,
zapcore.DPanicLevel,
zapcore.PanicLevel,
zapcore.FatalLevel,
})
}
return lvl, nil
}

View file

@ -8,9 +8,9 @@ import (
func main() { func main() {
g, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM) g, _ := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
v := newSettings() cfg := newSettings()
a := newApp(g, v) a := newApp(g, cfg)
go a.Serve(g) go a.Serve(g)

View file

@ -19,24 +19,24 @@ type Service struct {
// Start runs http service with the exposed endpoint on the configured port. // Start runs http service with the exposed endpoint on the configured port.
func (ms *Service) Start() { func (ms *Service) Start() {
if ms.enabled { if ms.enabled {
ms.log.Info(logs.ServiceIsRunning, zap.String("endpoint", ms.Addr)) ms.log.Info(logs.ServiceIsRunning, zap.String("endpoint", ms.Addr), logs.TagField(logs.TagApp))
err := ms.ListenAndServe() err := ms.ListenAndServe()
if err != nil && err != http.ErrServerClosed { if err != nil && err != http.ErrServerClosed {
ms.log.Warn(logs.ServiceCouldntStartOnConfiguredPort) ms.log.Warn(logs.ServiceCouldntStartOnConfiguredPort, logs.TagField(logs.TagApp))
} }
} else { } else {
ms.log.Info(logs.ServiceHasntStartedSinceItsDisabled) ms.log.Info(logs.ServiceHasntStartedSinceItsDisabled, logs.TagField(logs.TagApp))
} }
} }
// ShutDown stops the service. // ShutDown stops the service.
func (ms *Service) ShutDown(ctx context.Context) { func (ms *Service) ShutDown(ctx context.Context) {
ms.log.Info(logs.ShuttingDownService, zap.String("endpoint", ms.Addr)) ms.log.Info(logs.ShuttingDownService, zap.String("endpoint", ms.Addr), logs.TagField(logs.TagApp))
err := ms.Shutdown(ctx) err := ms.Shutdown(ctx)
if err != nil { if err != nil {
ms.log.Error(logs.CantGracefullyShutDownService, zap.Error(err)) ms.log.Error(logs.CantGracefullyShutDownService, zap.Error(err), logs.TagField(logs.TagApp))
if err = ms.Close(); err != nil { if err = ms.Close(); err != nil {
ms.log.Panic(logs.CantShutDownService, zap.Error(err)) ms.log.Panic(logs.CantShutDownService, zap.Error(err), logs.TagField(logs.TagApp))
} }
} }
} }

View file

@ -55,6 +55,11 @@ S3_GW_LOGGER_SAMPLING_ENABLED=false
S3_GW_LOGGER_SAMPLING_INITIAL=100 S3_GW_LOGGER_SAMPLING_INITIAL=100
S3_GW_LOGGER_SAMPLING_THEREAFTER=100 S3_GW_LOGGER_SAMPLING_THEREAFTER=100
S3_GW_LOGGER_SAMPLING_INTERVAL=1s S3_GW_LOGGER_SAMPLING_INTERVAL=1s
S3_GW_LOGGER_TAGS_0_NAME=app
S3_GW_LOGGER_TAGS_0_LEVEL=info
S3_GW_LOGGER_TAGS_1_NAME=datapath
S3_GW_LOGGER_TAGS_1_LEVEL=fatal
# HTTP logger # HTTP logger
S3_GW_HTTP_LOGGING_ENABLED=false S3_GW_HTTP_LOGGING_ENABLED=false
@ -80,8 +85,10 @@ S3_GW_PROMETHEUS_ADDRESS=localhost:8086
# Timeout to connect to a node # Timeout to connect to a node
S3_GW_CONNECT_TIMEOUT=10s S3_GW_CONNECT_TIMEOUT=10s
# Timeout for individual operations in streaming RPC. # Timeout for individual operations in object pool streaming RPC.
S3_GW_STREAM_TIMEOUT=10s S3_GW_STREAM_TIMEOUT=10s
# Timeout for individual operations in tree pool streaming RPC.
S3_GW_TREE_STREAM_TIMEOUT=10s
# Timeout to check node health during rebalance. # Timeout to check node health during rebalance.
S3_GW_HEALTHCHECK_TIMEOUT=15s S3_GW_HEALTHCHECK_TIMEOUT=15s
# Interval to check node health # Interval to check node health
@ -186,6 +193,10 @@ S3_GW_KLUDGE_USE_DEFAULT_XMLNS=false
S3_GW_KLUDGE_BYPASS_CONTENT_ENCODING_CHECK_IN_CHUNKS=false S3_GW_KLUDGE_BYPASS_CONTENT_ENCODING_CHECK_IN_CHUNKS=false
# Namespaces that should be handled as default # Namespaces that should be handled as default
S3_GW_KLUDGE_DEFAULT_NAMESPACES="" "root" S3_GW_KLUDGE_DEFAULT_NAMESPACES="" "root"
# During listing the s3 gate may send whitespaces to client to prevent it from cancelling request.
# The gate is going to send whitespace every time it receives chunk of data from FrostFS storage.
# This parameter enables this feature and limits frequency of whitespace transmissions.
S3_GW_KLUDGE_LISTING_KEEPALIVE_THROTTLE=10s
# Kludge profiles # Kludge profiles
S3_GW_KLUDGE_PROFILE_0_USER_AGENT=aws-cli S3_GW_KLUDGE_PROFILE_0_USER_AGENT=aws-cli
S3_GW_KLUDGE_PROFILE_0_USE_DEFAULT_XMLNS=true S3_GW_KLUDGE_PROFILE_0_USE_DEFAULT_XMLNS=true

View file

@ -60,6 +60,12 @@ logger:
initial: 100 initial: 100
thereafter: 100 thereafter: 100
interval: 1s interval: 1s
tags:
- name: "app"
level: "debug"
- name: "datapath"
- name: "external_storage"
- name: "external_storage_tree"
# log http request data (URI, headers, query, etc) # log http request data (URI, headers, query, etc)
http_logging: http_logging:
@ -100,8 +106,10 @@ tracing:
# Timeout to connect to a node # Timeout to connect to a node
connect_timeout: 10s connect_timeout: 10s
# Timeout for individual operations in streaming RPC. # Timeout for individual operations in object pool streaming RPC.
stream_timeout: 10s stream_timeout: 10s
# Timeout for individual operations in tree pool streaming RPC.
tree_stream_timeout: 10s
# Timeout to check node health during rebalance # Timeout to check node health during rebalance
healthcheck_timeout: 15s healthcheck_timeout: 15s
# Interval to check node health # Interval to check node health
@ -226,6 +234,10 @@ kludge:
bypass_content_encoding_check_in_chunks: false bypass_content_encoding_check_in_chunks: false
# Namespaces that should be handled as default # Namespaces that should be handled as default
default_namespaces: [ "", "root" ] default_namespaces: [ "", "root" ]
# During listing the s3 gate may send whitespaces to client to prevent it from cancelling request.
# The gate is going to send whitespace every time it receives chunk of data from FrostFS storage.
# This parameter enables this feature and limits frequency of whitespace transmissions.
listing_keepalive_throttle: 10s
# new profile section override defaults based on user agent # new profile section override defaults based on user agent
profile: profile:
- user_agent: aws-cli - user_agent: aws-cli

View file

@ -202,7 +202,7 @@ func (c *cred) checkIfCredentialsAreRemoved(ctx context.Context, cnrID cid.ID, a
func (c *cred) putBoxToCache(accessKeyID string, val *cache.AccessBoxCacheValue) { func (c *cred) putBoxToCache(accessKeyID string, val *cache.AccessBoxCacheValue) {
if err := c.cache.Put(accessKeyID, val); err != nil { if err := c.cache.Put(accessKeyID, val); err != nil {
c.log.Warn(logs.CouldntPutAccessBoxIntoCache, zap.String("accessKeyID", accessKeyID)) c.log.Warn(logs.CouldntPutAccessBoxIntoCache, zap.String("accessKeyID", accessKeyID), logs.TagField(logs.TagDatapath))
} }
} }
@ -241,7 +241,7 @@ func (c *cred) getAccessBox(ctx context.Context, cnrID cid.ID, accessKeyID strin
func (c *cred) Put(ctx context.Context, prm CredentialsParam) (oid.Address, error) { func (c *cred) Put(ctx context.Context, prm CredentialsParam) (oid.Address, error) {
if prm.AccessKeyID != "" { if prm.AccessKeyID != "" {
c.log.Info(logs.CheckCustomAccessKeyIDUniqueness, zap.String("access_key_id", prm.AccessKeyID)) c.log.Info(logs.CheckCustomAccessKeyIDUniqueness, zap.String("access_key_id", prm.AccessKeyID), logs.TagField(logs.TagApp))
credsPrm := PrmGetCredsObject{ credsPrm := PrmGetCredsObject{
Container: prm.Container, Container: prm.Container,
AccessKeyID: prm.AccessKeyID, AccessKeyID: prm.AccessKeyID,

Some files were not shown because too many files have changed in this diff Show more