Compare commits
375 commits
Author | SHA1 | Date | |
---|---|---|---|
e184b333e4 | |||
853036e44e | |||
ee46382a68 | |||
b207eb48d9 | |||
b7650e01ac | |||
e7f620f137 | |||
ffac62e8b4 | |||
182262ace2 | |||
893b506c83 | |||
beec37797d | |||
5538dce772 | |||
da9703ab63 | |||
a53e50b324 | |||
5e9c562683 | |||
49bf3c1bce | |||
a4d9658fbb | |||
bec63026bd | |||
0064e7ab07 | |||
41e1f1ad7a | |||
4d2e6f8650 | |||
3d3dd00211 | |||
510b0a1005 | |||
da77e426b6 | |||
e7a8d4bdaf | |||
250538a9b4 | |||
619385836d | |||
65fc776dea | |||
5842f5bad5 | |||
8b3252cbd0 | |||
|
d150f8ddcb | ||
bc975989de | |||
0cab76d01e | |||
|
e060308318 | ||
a725c68d06 | |||
fb4921826e | |||
d46f1d3bfa | |||
16eb289929 | |||
0ae7c35352 | |||
95d847d611 | |||
e0ce59fd32 | |||
09412d8f20 | |||
f2274b2786 | |||
f391966326 | |||
d986e74897 | |||
df1af2d2c9 | |||
04b8fc2b5f | |||
59b789f57e | |||
128939c01e | |||
4a4ce00994 | |||
980763c468 | |||
9395b5f39d | |||
11c1a86404 | |||
4515a7ae88 | |||
c5deb2e148 | |||
ea714c2e9e | |||
7bf31bea18 | |||
cc43975536 | |||
c4c757eea6 | |||
389e0de403 | |||
8da71c3ae0 | |||
cc9a68401f | |||
8f7ccb0f62 | |||
2c002b657e | |||
f215d200e8 | |||
51322cccdf | |||
3cd88d6204 | |||
e71ba5e22a | |||
e3141fc8e3 | |||
a12fea8a5b | |||
9875307c9b | |||
b1775f9478 | |||
4fa45bdac2 | |||
368c7d2acd | |||
31076796ce | |||
eff0de43d5 | |||
fb00dff83b | |||
d8f126b339 | |||
7ab902d8d2 | |||
0792fcf456 | |||
c46ffa8146 | |||
3260308cc0 | |||
d6e6a13576 | |||
17d40245de | |||
979d85b046 | |||
539dab8680 | |||
76008d4ba1 | |||
8bc19725ba | |||
9e64304499 | |||
94504e9746 | |||
a8458dbc27 | |||
424038de6c | |||
3cf27d281d | |||
3c7cb82553 | |||
57b7e83380 | |||
6a90f4e624 | |||
cb3753f286 | |||
81209e308c | |||
b78e55e101 | |||
25c24f5ce6 | |||
09c11262c6 | |||
f120715a37 | |||
aaed083d82 | |||
e35b582fe2 | |||
39fc7aa3ee | |||
da41f47826 | |||
9e5fb4be95 | |||
346243b159 | |||
03481274f0 | |||
c2adbd758a | |||
bc17ab5e47 | |||
9fadfbbc2f | |||
827ea1a41e | |||
968f10a72f | |||
582e6ac642 | |||
99f273f9af | |||
cd96adef36 | |||
738ce14f50 | |||
5358e39f71 | |||
34c1426b9f | |||
8ca73e2079 | |||
a87c636b4c | |||
26baf8a94e | |||
f187141ae5 | |||
3cffc782e9 | |||
d0e4d55772 | |||
42e72889a5 | |||
98815d5473 | |||
62615d7ab7 | |||
575ab4d294 | |||
d919e6cce2 | |||
056f168d77 | |||
9bdfe2a016 | |||
d6b506f6d9 | |||
a2e0b92575 | |||
b08f476ea7 | |||
f4275d837a | |||
664f83b2b7 | |||
136b5521fe | |||
a5f670d904 | |||
d76c4fe2a2 | |||
0637133c61 | |||
bf00fa6aa9 | |||
ff690ce996 | |||
534ae7f0f1 | |||
77673797f9 | |||
9e1766ff74 | |||
e73f11c251 | |||
5cb77018f8 | |||
fa68a4ce40 | |||
0644067496 | |||
481520705a | |||
28723f4a68 | |||
20719bd85c | |||
4f27e34974 | |||
3dc989d7fe | |||
69e77aecc9 | |||
c34680d157 | |||
f5326b9f04 | |||
51c5c227c2 | |||
c506620199 | |||
6cb0026007 | |||
971006a28c | |||
527e0dc612 | |||
3213dd7236 | |||
a031777a1b | |||
b2a5da8247 | |||
ec349e4523 | |||
977a20760b | |||
2948d1f942 | |||
c0011ebb8d | |||
456319d2f1 | |||
1d965b23ab | |||
3b83de31d2 | |||
70eedfc077 | |||
f86b82351a | |||
465eaa816a | |||
9241954496 | |||
77f8bdac58 | |||
91541a432d | |||
943b30d9f4 | |||
414f3943e2 | |||
9432782ce6 | |||
2b04fcb5ec | |||
280d11c794 | |||
ed34b2cae4 | |||
76f553d292 | |||
1513a9252b | |||
bb81afc14a | |||
58850f590e | |||
e25dc90c20 | |||
71bae5cd9a | |||
b5fae316cf | |||
9f3ea470e6 | |||
9787b29542 | |||
9152b084ec | |||
21dbe3ea8e | |||
f4d174e740 | |||
8a758293b9 | |||
fb521c7ac6 | |||
87b9e97a80 | |||
d62d8f3874 | |||
6bf6a3b1a3 | |||
2f108c9951 | |||
c43ef040dc | |||
2ab655b909 | |||
1c398551e5 | |||
db05021786 | |||
034396d554 | |||
3c436d8de9 | |||
45f77de8c8 | |||
d903de2457 | |||
e22ff52165 | |||
5315f7b733 | |||
43a687b572 | |||
29a2dae40c | |||
fec3b3f31e | |||
7db89c840b | |||
3ff027587c | |||
9f29fcbd52 | |||
8307c73fef | |||
d8889fca56 | |||
61ff4702a2 | |||
6da1acc554 | |||
3ea3f971e1 | |||
cb83f7646f | |||
9c012d0a66 | |||
bda014b7b4 | |||
37d05dcefd | |||
8407b3ea4c | |||
e537675223 | |||
789464e134 | |||
a138f4954b | |||
8669bf6b50 | |||
6b8095182e | |||
348126b3b8 | |||
fbe7a784e8 | |||
bfcde09f07 | |||
94bd1dfe28 | |||
80c7b73eb9 | |||
62cc5a04a7 | |||
6788306998 | |||
4ee3648183 | |||
ee48d1dc85 | |||
f958eef2b3 | |||
81b44ab3d3 | |||
623001c403 | |||
70043c4800 | |||
8050ca2d51 | |||
c12e264697 | |||
e9f38a49e4 | |||
fabb4134bc | |||
e1ee36b979 | |||
937367caaf | |||
7b86bac6ee | |||
529ec7e0b9 | |||
4741e74210 | |||
f1470bab4a | |||
6e5bcaef97 | |||
1522db05c5 | |||
31da31862a | |||
7de1ffdbe9 | |||
3285a2e105 | |||
1bfea006b0 | |||
56b50f2075 | |||
8f89f275bd | |||
ff15f9f28a | |||
c868af8a62 | |||
bac1b3fb2d | |||
c452d58ce2 | |||
499a202d28 | |||
d9d12debc3 | |||
3d0d2032c6 | |||
1f2cf0ed67 | |||
37be8851b3 | |||
c4c199defe | |||
2981a47e99 | |||
391fc9cbe3 | |||
4eb2c7fb7d | |||
563c1d9bd7 | |||
0f3b4ab0ed | |||
bd8d2d00ba | |||
cc34f659d1 | |||
924e87face | |||
5121c73d3f | |||
c334adeb6d | |||
a74d498df2 | |||
69297a4a38 | |||
71d82d1cc8 | |||
fafe4af529 | |||
88f1acbdfc | |||
4e15452853 | |||
da642a498a | |||
de32dfd7ce | |||
8a30f18ff6 | |||
2d7973b3f1 | |||
6d52f46012 | |||
0ae49eaab0 | |||
093de13f54 | |||
cf4fc3b602 | |||
739a6ec9df | |||
c7ee628ab0 | |||
4ad84b9b94 | |||
3e20f736a6 | |||
b52552e8c2 | |||
6e8960b2ab | |||
29ac91dfd5 | |||
84af85ed67 | |||
4804904d9d | |||
e23cc43824 | |||
12434d5f4d | |||
2e870e99c7 | |||
45e025320f | |||
eae49908da | |||
c32220762f | |||
6f9ee3da76 | |||
08019f1574 | |||
899213b3f3 | |||
3b6d2bc522 | |||
6509639540 | |||
5698d5844e | |||
43cae9ee04 | |||
a17ff66975 | |||
38c5503a02 | |||
8273af8bf8 | |||
6dbb07f0fa | |||
340e6b807b | |||
0850d21ff3 | |||
9272f4e108 | |||
be6a37ada5 | |||
836874a761 | |||
7a285d1464 | |||
f58a0d04ff | |||
94c2674f44 | |||
6b1b43a364 | |||
43abf58068 | |||
ca15acf1d3 | |||
473239bf36 | |||
93cf7c462b | |||
6c5f9b2764 | |||
42862fd69e | |||
c7a65bd075 | |||
28c6bb4cb8 | |||
0db6cd6727 | |||
ff1ec56d24 | |||
9ebfca654b | |||
055cc6a22a | |||
a61ff3b8cb | |||
6304d7bfda | |||
cf7254f8cd | |||
861454e499 | |||
b28ecef43b | |||
406075aebb | |||
fe796ba538 | |||
5ee73fad6a | |||
890a8ed237 | |||
0bed25816c | |||
b169c5e6c3 | |||
122af0b5a7 | |||
cf13aae342 | |||
0938d7ee82 | |||
4f5f5fb5c8 | |||
25bb581fee | |||
8d6aa0d40a | |||
7e91f62c28 | |||
01323ca8e0 | |||
298662df9d | |||
10a03faeb4 | |||
65412ce1d3 | |||
7de73f6b73 | |||
8fc9d93f37 | |||
7301ca52ab | |||
e1ec61ddfc | |||
e3f2d59565 | |||
c4af1dc4ad | |||
b8c93ed391 |
279 changed files with 35315 additions and 11333 deletions
|
@ -1,13 +1,14 @@
|
|||
FROM golang:1.21 as builder
|
||||
FROM golang:1.22 AS builder
|
||||
|
||||
ARG BUILD=now
|
||||
ARG REPO=git.frostfs.info/TrueCloudLab/frostfs-s3-gw
|
||||
ARG VERSION=dev
|
||||
ARG GOFLAGS=""
|
||||
|
||||
WORKDIR /src
|
||||
COPY . /src
|
||||
|
||||
RUN make
|
||||
RUN make GOFLAGS=${GOFLAGS}
|
||||
|
||||
# Executable image
|
||||
FROM alpine AS frostfs-s3-gw
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
.git
|
||||
.cache
|
||||
.github
|
||||
.forgejo
|
||||
|
|
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
|
@ -1,4 +1,8 @@
|
|||
on: [pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
builds:
|
||||
|
@ -6,7 +10,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go_versions: [ '1.20', '1.21' ]
|
||||
go_versions: [ '1.22', '1.23' ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
@ -18,3 +22,6 @@ jobs:
|
|||
|
||||
- name: Build binary
|
||||
run: make
|
||||
|
||||
- name: Check dirty suffix
|
||||
run: if [[ $(make version) == *"dirty"* ]]; then echo "Version has dirty suffix" && exit 1; fi
|
||||
|
|
|
@ -12,9 +12,9 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Run commit format checker
|
||||
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v1
|
||||
uses: https://git.frostfs.info/TrueCloudLab/dco-go@v3
|
||||
with:
|
||||
from: 3fbad97a
|
||||
from: 'origin/${{ github.event.pull_request.base.ref }}'
|
||||
|
|
27
.forgejo/workflows/oci-image.yml
Normal file
27
.forgejo/workflows/oci-image.yml
Normal file
|
@ -0,0 +1,27 @@
|
|||
on:
|
||||
pull_request:
|
||||
push:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
image:
|
||||
name: OCI image
|
||||
runs-on: docker
|
||||
container: git.frostfs.info/truecloudlab/env:oci-image-builder-bookworm
|
||||
steps:
|
||||
- name: Clone git repo
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Build OCI image
|
||||
run: make image
|
||||
|
||||
- name: Push image to OCI registry
|
||||
run: |
|
||||
echo "$REGISTRY_PASSWORD" \
|
||||
| docker login --username truecloudlab --password-stdin git.frostfs.info
|
||||
make image-push
|
||||
if: >-
|
||||
startsWith(github.ref, 'refs/tags/v') &&
|
||||
(github.event_name == 'workflow_dispatch' || github.event_name == 'push')
|
||||
env:
|
||||
REGISTRY_PASSWORD: ${{secrets.FORGEJO_OCI_REGISTRY_PUSH_TOKEN}}
|
|
@ -1,4 +1,8 @@
|
|||
on: [pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
|
@ -10,7 +14,7 @@ jobs:
|
|||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.23'
|
||||
cache: true
|
||||
|
||||
- name: Install linters
|
||||
|
@ -24,7 +28,7 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
go_versions: [ '1.20', '1.21' ]
|
||||
go_versions: [ '1.22', '1.23' ]
|
||||
fail-fast: false
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
on: [pull_request]
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
vulncheck:
|
||||
|
@ -12,7 +16,7 @@ jobs:
|
|||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.23.6'
|
||||
|
||||
- name: Install govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -1 +0,0 @@
|
|||
* @alexvanin @dkirillov
|
|
@ -12,7 +12,8 @@ run:
|
|||
# output configuration options
|
||||
output:
|
||||
# colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number"
|
||||
format: tab
|
||||
formats:
|
||||
- format: tab
|
||||
|
||||
# all available settings of specific linters
|
||||
linters-settings:
|
||||
|
@ -65,3 +66,6 @@ issues:
|
|||
- EXC0003 # test/Test ... consider calling this
|
||||
- EXC0004 # govet
|
||||
- EXC0005 # C-style breaks
|
||||
exclude-dirs:
|
||||
- api/auth/signer/v4asdk2
|
||||
- api/auth/signer/v4sdk2
|
||||
|
|
366
CHANGELOG.md
366
CHANGELOG.md
|
@ -4,6 +4,322 @@ This document outlines major changes between releases.
|
|||
|
||||
## [Unreleased]
|
||||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
- Response codes when checking ACL format (#531)
|
||||
- CORS unmarshal without xmlns (#594)
|
||||
- Response code for invalid Content-Md5 header (#598)
|
||||
|
||||
### Added
|
||||
- Derive encryption keys for accessbox with salt (#529)
|
||||
- Debug log when bucket settings not found (#595)
|
||||
- Context cancellation during tree node streaming (#569)
|
||||
- Add LimitExceeded error (#589)
|
||||
|
||||
### Changed
|
||||
- Docker image repository (#590, #587)
|
||||
|
||||
## [0.32.0] - Khumbu - 2024-12-20
|
||||
|
||||
### Added
|
||||
- Metric of dropped logs by log sampler (#502)
|
||||
- SigV4A signature algorithm (#339)
|
||||
- TLS Termination header for SSE-C (#562)
|
||||
- Kludge profile support (#147)
|
||||
- Netmap support in tree pool (#577)
|
||||
|
||||
### Changed
|
||||
- Improved multipart removal speed (#559)
|
||||
- Updated tree service pool without api-go dependency (#570)
|
||||
|
||||
## [0.31.3] - 2024-12-17
|
||||
|
||||
### Fixed
|
||||
- Return BucketAlreadyExists when global domain taken (#584)
|
||||
- Fix list-buckets vhs routing (#583)
|
||||
- Skip port when matching listen domains (#586)
|
||||
|
||||
## [0.31.2] - 2024-12-13
|
||||
|
||||
### Fixed
|
||||
- Unable to remove EC object (#576)
|
||||
|
||||
## [0.31.1] - 2024-11-28
|
||||
|
||||
### Fixed
|
||||
- Ignore precondition headers with invalid date format (#563)
|
||||
- MD5 calculation of object-part with SSE-C (#543)
|
||||
|
||||
## [0.31.0] - Rongbuk - 2024-11-20
|
||||
|
||||
### Fixed
|
||||
- Docker warnings during image build (#421)
|
||||
- `PartNumberMarker` in ListMultipart response (#451)
|
||||
- PostObject handling (#456)
|
||||
- Tag logging errors (#452)
|
||||
- Removing of duplicated parts in tree service during split brain (#448)
|
||||
- Container resolving (#482)
|
||||
- FrostFS to S3 error transformation (#488)
|
||||
- Default bucket routing (#507)
|
||||
- encoding-type in ListBucketObjectVersions (#404)
|
||||
- SIGHUP support for `tracing.enabled` config parameter (#520)
|
||||
- `trace_id` parameter in logs (#501)
|
||||
- Listing marker processing (#539)
|
||||
- Content-MD5 header check (#540)
|
||||
- Precondition check (#538)
|
||||
- Bucket name check during all S3 operations (#556)
|
||||
|
||||
### Added
|
||||
- Support for separate container for all CORS settings (#422)
|
||||
- `X-Amz-Force-Delete-Bucket` header for forced bucket removal (#31)
|
||||
- `Location` support in CompleteMultipart response (#451)
|
||||
- Tree pool request duration metric (#447)
|
||||
- Expiration lifecycle configuration support (#42, #412, #459, #460, #516, #536)
|
||||
- Add support for virtual hosted style addressing (#446, #449, #493)
|
||||
- Support `frostfs.graceful_close_on_switch_timeout` (#475)
|
||||
- Vulnerability report document (#413)
|
||||
- Support patch object method (#462, #473, #466, #479)
|
||||
- Enhanced logging and request reproducer (#369)
|
||||
- Root CA configuration for tracing (#484)
|
||||
- Log sampling policy configuration (#461)
|
||||
- `sign` command to `frostfs-s3-authmate` (#467)
|
||||
- Support custom aws credentials (#509)
|
||||
- Source IP binding configuration for FrostFS requests (#521)
|
||||
- Tracing attributes (#549)
|
||||
|
||||
### Changed
|
||||
- Split `FrostFS` interface into separate read methods (#427)
|
||||
- golangci-lint v1.60 support (#474)
|
||||
- Updated Go version to 1.22 (#470)
|
||||
- Container removal after failed bucket creation (#434)
|
||||
- Explicit check for `.` symbol in bucket name (#506)
|
||||
- Transaction waiter in contract clients (#522)
|
||||
- Avoid maintenance mode storage node during object operations (#524)
|
||||
- Content-Type does not include in Presigned URL of s3-authmate (#505)
|
||||
- Check owner ID before deleting bucket (#528)
|
||||
- S3-Authmate now uses APE instead basic-ACL (#553)
|
||||
|
||||
### Removed
|
||||
- Reduce using mutex when update app settings (#329)
|
||||
|
||||
## [0.30.9] - 2024-12-13
|
||||
|
||||
### Fixed
|
||||
- Unable to remove EC object (#576)
|
||||
|
||||
## [0.30.8] - 2024-10-18
|
||||
|
||||
### Fixed
|
||||
- Error handling for correct connection switch in SDK Pool (#517)
|
||||
|
||||
## [0.30.7] - 2024-10-03
|
||||
|
||||
### Fixed
|
||||
- Correct aws-chunk encoding size handling (#511)
|
||||
|
||||
|
||||
## [0.30.6] - 2024-09-17
|
||||
|
||||
### Fixed
|
||||
- Object size of objects upload with aws-chunked encoding (#450)
|
||||
- Object size of objects upload with negative Content-Length (#486)
|
||||
|
||||
## [0.30.5] - 2024-09-16
|
||||
|
||||
### Fixed
|
||||
- Panic catchers for fuzzing tests (#492)
|
||||
|
||||
## [0.30.4] - 2024-09-03
|
||||
|
||||
### Added
|
||||
- Fuzzing tests (#480)
|
||||
|
||||
## [0.30.3] - 2024-08-27
|
||||
|
||||
### Fixed
|
||||
- Empty listing when multipart upload contains more than 1000 parts (#471)
|
||||
|
||||
## [0.30.2] - 2024-08-20
|
||||
|
||||
### Fixed
|
||||
- Error counting in pool component before connection switch (#468)
|
||||
|
||||
### Added
|
||||
- Log of endpoint address during tree pool errors (#468)
|
||||
|
||||
## [0.30.1] - 2024-07-25
|
||||
|
||||
### Fixed
|
||||
- Redundant system node removal in tree service (#437)
|
||||
|
||||
### Added
|
||||
- Log details on SDK Pool health status change (#439)
|
||||
|
||||
## [0.30.0] - Kangshung -2024-07-19
|
||||
|
||||
### Fixed
|
||||
- Fix HTTP/2 requests (#341)
|
||||
- Fix Decoder.CharsetReader is nil (#379)
|
||||
- Fix flaky ACL encode test (#340)
|
||||
- Docs grammar (#432)
|
||||
|
||||
### Added
|
||||
- Add new `reconnect_interval` config param for server rebinding (#291)
|
||||
- Support `GetBucketPolicyStatus` (#301)
|
||||
- Support request IP filter with policy (#371, #377)
|
||||
- Support tag checks in policies (#357, #365, #392, #403, #411)
|
||||
- Support IAM-MFA checks (#367)
|
||||
- More docs (#334, #353)
|
||||
- Add `register-user` command to `authmate` (#414)
|
||||
- `User` field in request log (#396)
|
||||
- Erasure coding support in placement policy (#400)
|
||||
- Improved test coverage (#402)
|
||||
|
||||
### Changed
|
||||
- Update dependencies noted by govulncheck (#368)
|
||||
- Improve test coverage (#380, #387)
|
||||
- Support updated naming in native policy JSON (#385)
|
||||
- Improve determining AccessBox latest version (#335)
|
||||
- Don't set full_control policy for bucket owner (#407)
|
||||
|
||||
### Removed
|
||||
- Remove control api (#406)
|
||||
- Remove notifications (#401)
|
||||
- Remove `layer.Client` interface (#410)
|
||||
- Remove extended ACL related code (#372)
|
||||
|
||||
## [0.29.3] - 2024-07-19
|
||||
|
||||
### Fixed
|
||||
- Support tree split environment when multiple nodes
|
||||
may be part of the same sub path (#430)
|
||||
- Collision of multipart name and system data in the tree (#430)
|
||||
- Workaround for removal of multiple null versions in unversioned bucket (#430)
|
||||
|
||||
## [0.29.2] - 2024-07-03
|
||||
|
||||
### Fixed
|
||||
- Parsing of put-bucket-setting retry configuration (#398)
|
||||
|
||||
## [0.29.1] - 2024-06-20
|
||||
|
||||
### Fixed
|
||||
- OPTIONS request processing for object operations (#399)
|
||||
|
||||
### Added
|
||||
- Retries of put-bucket-setting operation during container creation (#398)
|
||||
|
||||
## [0.29.0] - Zemu - 2024-05-27
|
||||
|
||||
### Fixed
|
||||
- Fix marshaling errors in `DeleteObjects` method (#222)
|
||||
- Fix status code in GET/HEAD delete marker (#226)
|
||||
- Fix `NextVersionIDMarker` in `list-object-versions` (#248)
|
||||
- Fix possibility of panic during SIGHUP (#288)
|
||||
- Fix flaky `TestErrorTimeoutChecking` (`make test` sometimes failed) (#290)
|
||||
- Fix log-level change on SIGHUP (#313)
|
||||
- Fix anonymous put request (#311)
|
||||
- Fix routine leak from nns resolver (#324)
|
||||
- Fix svace errors (#325, #328)
|
||||
|
||||
### Added
|
||||
- Add new `frostfs.buffer_max_size_for_put` config param and sync TZ hash for PUT operations (#197)
|
||||
- Add `X-Amz-Version-Id` header after complete multipart upload (#227)
|
||||
- Add handling of `X-Amz-Copy-Source-Server-Side-Encryption-Customer-*` headers during copy (#217)
|
||||
- Add new `logger.destination` config param (#236)
|
||||
- Add `X-Amz-Content-Sha256` header validation (#218)
|
||||
- Support frostfsid contract. See `frostfsid` config section (#260)
|
||||
- Support per namespace placement policies configuration (see `namespaces.config` config param) (#266)
|
||||
- Support control api to manage policies. See `control` config section (#258)
|
||||
- Add `namespace` label to billing metrics (#271)
|
||||
- Support policy-engine (#257, #259, #282, #283, #302, #307, #345, #351, #358, #360, #362, #383, #354)
|
||||
- Support `proxy` contract (#287)
|
||||
- Authmate: support custom attributes (#292)
|
||||
- Add FrostfsID cache (#269)
|
||||
|
||||
### Changed
|
||||
- Generalise config param `use_default_xmlns_for_complete_multipart` to `use_default_xmlns` so that use default xmlns for all requests (#221)
|
||||
- Set server IdleTimeout and ReadHeaderTimeout to `30s` and allow to configure them (#220)
|
||||
- Return `ETag` value in quotes (#219)
|
||||
- Use tombstone when delete multipart upload (#275)
|
||||
- Support new parameter `cache.accessbox.removing_check_interval` (#305)
|
||||
- Use APE rules instead of eACL in container creation (#306)
|
||||
- Rework bucket policy with policy-engine (#261)
|
||||
- Improved object listing speed (#165, #347)
|
||||
- Logging improvement (#300, #318)
|
||||
|
||||
### Removed
|
||||
- Drop sending whitespace characters during complete multipart upload and related config param `kludge.complete_multipart_keepalive` (#227)
|
||||
- Unused legacy minio related code (#299)
|
||||
- Redundant output with journald logging (#298)
|
||||
|
||||
## [0.28.2] - 2024-05-27
|
||||
|
||||
### Fixed
|
||||
- `anon` user in billing metrics (#321)
|
||||
- Parts are not removed when multipart object removed (#370)
|
||||
|
||||
### Added
|
||||
- Put request in duration metrics (#280)
|
||||
|
||||
## [0.28.1] - 2024-01-24
|
||||
|
||||
### Added
|
||||
- MD5 hash as ETag and response header (#205)
|
||||
- Tree pool traversal limit (#262)
|
||||
|
||||
### Updating from 0.28.0
|
||||
|
||||
See new `features.md5.enabled` and `frostfs.tree_pool_max_attempts` config
|
||||
parameters.
|
||||
|
||||
## [0.28.0] - Academy of Sciences - 2023-12-07
|
||||
|
||||
### Fixed
|
||||
- Handle negative `Content-Length` on put (#125)
|
||||
- Use `DisableURIPathEscaping` to presign urls (#125)
|
||||
|
@ -15,11 +331,15 @@ This document outlines major changes between releases.
|
|||
- Fix parsing signed headers in presigned urls (#182)
|
||||
- Fix url escaping (#188)
|
||||
- Use correct keys in `list-multipart-uploads` response (#185)
|
||||
- Fix parsing `key-marker` for object list versions (#237)
|
||||
- `GetSubTree` failures (#179)
|
||||
- Unexpected EOF during multipart download (#210)
|
||||
- Produce clean version in debian build (#245)
|
||||
|
||||
### Added
|
||||
- Add `trace_id` value into log record when tracing is enabled (#142)
|
||||
- Add basic error types and exit codes to `frostfs-s3-authmate` (#152)
|
||||
- Add a metric with addresses of nodes of the same and highest priority that are currently healthy (#51)
|
||||
- Add a metric with addresses of nodes of the same and highest priority that are currently healthy (#186)
|
||||
- Support dump metrics descriptions (#80)
|
||||
- Add `copies_numbers` section to `placement_policy` in config file and support vectors of copies numbers (#70, #101)
|
||||
- Support impersonate bearer token (#81, #105)
|
||||
|
@ -30,6 +350,9 @@ This document outlines major changes between releases.
|
|||
- Implement chunk uploading (#106)
|
||||
- Add new `kludge.bypass_content_encoding_check_in_chunks` config param (#146)
|
||||
- Add new `frostfs.client_cut` config param (#192)
|
||||
- Add selection of the node of the latest version of the object (#231)
|
||||
- Soft memory limit with `runtime.soft_memory_limit` (#196)
|
||||
- `server_health` metric for every S3 endpoint status (#199)
|
||||
|
||||
### Changed
|
||||
- Update prometheus to v1.15.0 (#94)
|
||||
|
@ -40,10 +363,16 @@ This document outlines major changes between releases.
|
|||
- Use request scope logger (#111)
|
||||
- Add `s3-authmate update-secret` command (#131)
|
||||
- Use default registerer for app metrics (#155)
|
||||
- Use chi router instead of archived gorlilla/mux (#149)
|
||||
- Use chi router instead of archived gorlilla/mux (#149, #174, #188)
|
||||
- Complete multipart upload doesn't unnecessary copy now. Thus, the total time of multipart upload was reduced by 2 times (#63)
|
||||
- Use gate key to form object owner (#175)
|
||||
- Apply placement policies and copies if there is at least one valid value (#168)
|
||||
- `statistic_tx_bytes_total` and `statistic_rx_bytes_total` metric to `statistic_bytes_total` metric with `direction` label (#153)
|
||||
- Refactor of context-stored data receivers (#137)
|
||||
- Refactor fetch/parse config parameters functions (#117)
|
||||
- Move all log messages to constants (#96)
|
||||
- Allow zero value of `part-number-marker` (#207)
|
||||
- Clean tag node in the tree service instead of removal (#233)
|
||||
|
||||
### Removed
|
||||
- Drop `tree.service` param (now endpoints from `peers` section are used) (#133)
|
||||
|
@ -92,4 +421,35 @@ This project is a fork of [NeoFS S3 Gateway](https://github.com/nspcc-dev/neofs-
|
|||
To see CHANGELOG for older versions, refer to https://github.com/nspcc-dev/neofs-s3-gw/blob/master/CHANGELOG.md.
|
||||
|
||||
[0.27.0]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/b2148cc3...v0.27.0
|
||||
[Unreleased]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.27.0...master
|
||||
[0.28.0]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.27.0...v0.28.0
|
||||
[0.28.1]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.28.0...v0.28.1
|
||||
[0.28.2]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.28.1...v0.28.2
|
||||
[0.29.0]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.28.2...v0.29.0
|
||||
[0.29.1]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.29.0...v0.29.1
|
||||
[0.29.2]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.29.1...v0.29.2
|
||||
[0.29.3]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.29.2...v0.29.3
|
||||
[0.30.0]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.29.3...v0.30.0
|
||||
[0.30.1]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.0...v0.30.1
|
||||
[0.30.2]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.1...v0.30.2
|
||||
[0.30.3]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.2...v0.30.3
|
||||
[0.30.4]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.3...v0.30.4
|
||||
[0.30.5]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.4...v0.30.5
|
||||
[0.30.6]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.5...v0.30.6
|
||||
[0.30.7]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.6...v0.30.7
|
||||
[0.30.8]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.7...v0.30.8
|
||||
[0.30.9]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.8...v0.30.9
|
||||
[0.31.0]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.30.9...v0.31.0
|
||||
[0.31.1]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.31.0...v0.31.1
|
||||
[0.31.2]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.31.1...v0.31.2
|
||||
[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.1]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.0...v0.32.1
|
||||
[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
|
||||
[Unreleased]: https://git.frostfs.info/TrueCloudLab/frostfs-s3-gw/compare/v0.32.9...master
|
3
CODEOWNERS
Normal file
3
CODEOWNERS
Normal file
|
@ -0,0 +1,3 @@
|
|||
.* @TrueCloudLab/storage-services-developers @TrueCloudLab/storage-services-committers
|
||||
.forgejo/.* @potyarkin
|
||||
Makefile @potyarkin
|
56
Makefile
56
Makefile
|
@ -3,9 +3,9 @@
|
|||
# Common variables
|
||||
REPO ?= $(shell go list -m)
|
||||
VERSION ?= $(shell git describe --tags --dirty --match "v*" --always --abbrev=8 2>/dev/null || cat VERSION 2>/dev/null || echo "develop")
|
||||
GO_VERSION ?= 1.20
|
||||
LINT_VERSION ?= 1.54.0
|
||||
TRUECLOUDLAB_LINT_VERSION ?= 0.0.2
|
||||
GO_VERSION ?= 1.22
|
||||
LINT_VERSION ?= 1.60.1
|
||||
TRUECLOUDLAB_LINT_VERSION ?= 0.0.6
|
||||
BINDIR = bin
|
||||
|
||||
METRICS_DUMP_OUT ?= ./metrics-dump.json
|
||||
|
@ -14,15 +14,23 @@ METRICS_DUMP_OUT ?= ./metrics-dump.json
|
|||
CMDS = $(addprefix frostfs-, $(notdir $(wildcard cmd/*)))
|
||||
BINS = $(addprefix $(BINDIR)/, $(CMDS))
|
||||
|
||||
GOFLAGS ?=
|
||||
|
||||
# Variables for docker
|
||||
REPO_BASENAME = $(shell basename `go list -m`)
|
||||
HUB_IMAGE ?= "truecloudlab/$(REPO_BASENAME)"
|
||||
HUB_IMAGE ?= "git.frostfs.info/truecloudlab/$(REPO_BASENAME)"
|
||||
HUB_TAG ?= "$(shell echo ${VERSION} | sed 's/^v//')"
|
||||
|
||||
OUTPUT_LINT_DIR ?= $(shell pwd)/bin
|
||||
LINT_DIR = $(OUTPUT_LINT_DIR)/golangci-lint-$(LINT_VERSION)-v$(TRUECLOUDLAB_LINT_VERSION)
|
||||
TMP_DIR := .cache
|
||||
|
||||
# Variables for fuzzing
|
||||
FUZZ_NGFUZZ_DIR ?= ""
|
||||
FUZZ_TIMEOUT ?= 30
|
||||
FUZZ_FUNCTIONS ?= "all"
|
||||
FUZZ_AUX ?= ""
|
||||
|
||||
.PHONY: all $(BINS) $(BINDIR) dep docker/ test cover format image image-push dirty-image lint docker/lint pre-commit unpre-commit version clean protoc
|
||||
|
||||
# .deb package versioning
|
||||
|
@ -38,6 +46,7 @@ all: $(BINS)
|
|||
$(BINS): $(BINDIR) dep
|
||||
@echo "⇒ Build $@"
|
||||
CGO_ENABLED=0 \
|
||||
GOFLAGS=$(GOFLAGS) \
|
||||
go build -v -trimpath \
|
||||
-ldflags "-X $(REPO)/internal/version.Version=$(VERSION)" \
|
||||
-o $@ ./cmd/$(subst frostfs-,,$(notdir $@))
|
||||
|
@ -64,7 +73,7 @@ docker/%:
|
|||
-w /src \
|
||||
-u `stat -c "%u:%g" .` \
|
||||
--env HOME=/src \
|
||||
golang:$(GO_VERSION) make $*,\
|
||||
golang:$(GO_VERSION) make GOFLAGS=$(GOFLAGS) $*,\
|
||||
@echo "supported docker targets: all $(BINS) lint")
|
||||
|
||||
# Run tests
|
||||
|
@ -76,6 +85,34 @@ cover:
|
|||
@go test -v -race ./... -coverprofile=coverage.txt -covermode=atomic
|
||||
@go tool cover -html=coverage.txt -o coverage.html
|
||||
|
||||
# Run fuzzing
|
||||
CLANG := $(shell which clang-17 2>/dev/null)
|
||||
.PHONY: check-clang all
|
||||
check-clang:
|
||||
ifeq ($(CLANG),)
|
||||
@echo "clang-17 is not installed. Please install it before proceeding - https://apt.llvm.org/llvm.sh "
|
||||
@exit 1
|
||||
endif
|
||||
|
||||
.PHONY: check-ngfuzz all
|
||||
check-ngfuzz:
|
||||
@if [ -z "$(FUZZ_NGFUZZ_DIR)" ]; then \
|
||||
echo "Please set a variable FUZZ_NGFUZZ_DIR to specify path to the ngfuzz"; \
|
||||
exit 1; \
|
||||
fi
|
||||
|
||||
.PHONY: install-fuzzing-deps
|
||||
install-fuzzing-deps: check-clang check-ngfuzz
|
||||
|
||||
.PHONY: fuzz
|
||||
fuzz: install-fuzzing-deps
|
||||
@START_PATH=$$(pwd); \
|
||||
ROOT_PATH=$$(realpath --relative-to=$(FUZZ_NGFUZZ_DIR) $$START_PATH) ; \
|
||||
cd $(FUZZ_NGFUZZ_DIR) && \
|
||||
./ngfuzz -clean && \
|
||||
./ngfuzz -fuzz $(FUZZ_FUNCTIONS) -rootdir $$ROOT_PATH -timeout $(FUZZ_TIMEOUT) $(FUZZ_AUX) && \
|
||||
./ngfuzz -report
|
||||
|
||||
# Reformat code
|
||||
format:
|
||||
@echo "⇒ Processing gofmt check"
|
||||
|
@ -87,6 +124,7 @@ image:
|
|||
@docker build \
|
||||
--build-arg REPO=$(REPO) \
|
||||
--build-arg VERSION=$(VERSION) \
|
||||
--build-arg GOFLAGS=$(GOFLAGS) \
|
||||
--rm \
|
||||
-f .docker/Dockerfile \
|
||||
-t $(HUB_IMAGE):$(HUB_TAG) .
|
||||
|
@ -151,10 +189,16 @@ clean:
|
|||
|
||||
# Generate code from .proto files
|
||||
protoc:
|
||||
# Install specific version for protobuf lib
|
||||
@GOBIN=$(abspath $(BINDIR)) go install -mod=mod -v git.frostfs.info/TrueCloudLab/frostfs-api-go/v2/util/protogen
|
||||
@for f in `find . -type f -name '*.proto' -not -path './vendor/*'`; do \
|
||||
echo "⇒ Processing $$f "; \
|
||||
protoc \
|
||||
--go_out=paths=source_relative:. $$f; \
|
||||
--go_out=paths=source_relative:. \
|
||||
--plugin=protoc-gen-go-frostfs=$(BINDIR)/protogen \
|
||||
--go-frostfs_out=. --go-frostfs_opt=paths=source_relative \
|
||||
--go-grpc_opt=require_unimplemented_servers=false \
|
||||
--go-grpc_out=. --go-grpc_opt=paths=source_relative $$f; \
|
||||
done
|
||||
rm -rf vendor
|
||||
|
||||
|
|
20
README.md
20
README.md
|
@ -1,5 +1,5 @@
|
|||
<p align="center">
|
||||
<img src="./.github/logo.svg" width="500px" alt="FrostFS logo">
|
||||
<img src="./.forgejo/logo.svg" width="500px" alt="FrostFS logo">
|
||||
</p>
|
||||
<p align="center">
|
||||
<a href="https://frostfs.info">FrostFS</a> is a decentralized distributed object storage integrated with the <a href="https://neo.org">NEO Blockchain</a>.
|
||||
|
@ -93,6 +93,24 @@ HTTP/1.1 200 OK
|
|||
|
||||
Also, you can configure domains using `.env` variables or `yaml` file.
|
||||
|
||||
## Fuzzing
|
||||
To run fuzzing tests use the following command:
|
||||
|
||||
```shell
|
||||
$ make fuzz
|
||||
```
|
||||
|
||||
This command will install dependencies for the fuzzing process and run existing fuzzing tests.
|
||||
|
||||
You can also use the following arguments:
|
||||
|
||||
```
|
||||
FUZZ_TIMEOUT - time to run each fuzzing test (default 30)
|
||||
FUZZ_FUNCTIONS - fuzzing tests that will be started (default "all")
|
||||
FUZZ_AUX - additional parameters for the fuzzer (for example, "-debug")
|
||||
FUZZ_NGFUZZ_DIR - path to ngfuzz tool
|
||||
````
|
||||
|
||||
## Documentation
|
||||
|
||||
- [Configuration](./docs/configuration.md)
|
||||
|
|
26
SECURITY.md
Normal file
26
SECURITY.md
Normal file
|
@ -0,0 +1,26 @@
|
|||
# Security Policy
|
||||
|
||||
|
||||
## How To Report a Vulnerability
|
||||
|
||||
If you think you have found a vulnerability in this repository, please report it to us through coordinated disclosure.
|
||||
|
||||
**Please do not report security vulnerabilities through public issues, discussions, or change requests.**
|
||||
|
||||
Instead, you can report it using one of the following ways:
|
||||
|
||||
* Contact the [TrueCloudLab Security Team](mailto:security@frostfs.info) via email
|
||||
|
||||
Please include as much of the information listed below as you can to help us better understand and resolve the issue:
|
||||
|
||||
* The type of issue (e.g., buffer overflow, or cross-site scripting)
|
||||
* Affected version(s)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Any log files that are related to this issue (if possible)
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
|
||||
This information will help us triage your report more quickly.
|
2
VERSION
2
VERSION
|
@ -1 +1 @@
|
|||
v0.27.0
|
||||
v0.32.9
|
||||
|
|
|
@ -2,139 +2,166 @@ package auth
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
|
||||
apiErrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2"
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4"
|
||||
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
)
|
||||
|
||||
// authorizationFieldRegexp -- is regexp for credentials with Base58 encoded cid and oid and '0' (zero) as delimiter.
|
||||
var 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>.+)`)
|
||||
var (
|
||||
// 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>.+)`)
|
||||
|
||||
// postPolicyCredentialRegexp -- is regexp for credentials when uploading file using POST with policy.
|
||||
var postPolicyCredentialRegexp = regexp.MustCompile(`(?P<access_key_id>[^/]+)/(?P<date>[^/]+)/(?P<region>[^/]*)/(?P<service>[^/]+)/aws4_request`)
|
||||
// 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>.+)`)
|
||||
|
||||
// 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`)
|
||||
)
|
||||
|
||||
type (
|
||||
// Center is a user authentication interface.
|
||||
Center interface {
|
||||
Authenticate(request *http.Request) (*Box, error)
|
||||
}
|
||||
|
||||
// Box contains access box and additional info.
|
||||
Box struct {
|
||||
AccessBox *accessbox.Box
|
||||
ClientTime time.Time
|
||||
AuthHeaders *AuthHeader
|
||||
}
|
||||
|
||||
center struct {
|
||||
Center struct {
|
||||
reg *RegexpSubmatcher
|
||||
regV4a *RegexpSubmatcher
|
||||
postReg *RegexpSubmatcher
|
||||
cli tokens.Credentials
|
||||
allowedAccessKeyIDPrefixes []string // empty slice means all access key ids are allowed
|
||||
settings CenterSettings
|
||||
}
|
||||
|
||||
prs int
|
||||
CenterSettings interface {
|
||||
AccessBoxContainer() (cid.ID, bool)
|
||||
}
|
||||
|
||||
//nolint:revive
|
||||
AuthHeader struct {
|
||||
AccessKeyID string
|
||||
Service string
|
||||
Region string
|
||||
SignatureV4 string
|
||||
Signature string
|
||||
SignedFields []string
|
||||
Date string
|
||||
IsPresigned bool
|
||||
Expiration time.Duration
|
||||
Preamble string
|
||||
PayloadHash string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
accessKeyPartsNum = 2
|
||||
authHeaderPartsNum = 6
|
||||
maxFormSizeMemory = 50 * 1048576 // 50 MB
|
||||
authHeaderPartsNum = 6
|
||||
authHeaderV4aPartsNum = 5
|
||||
maxFormSizeMemory = 50 * 1048576 // 50 MB
|
||||
|
||||
AmzAlgorithm = "X-Amz-Algorithm"
|
||||
AmzCredential = "X-Amz-Credential"
|
||||
AmzSignature = "X-Amz-Signature"
|
||||
AmzSignedHeaders = "X-Amz-SignedHeaders"
|
||||
AmzRegionSet = "X-Amz-Region-Set"
|
||||
AmzExpires = "X-Amz-Expires"
|
||||
AmzDate = "X-Amz-Date"
|
||||
AmzContentSHA256 = "X-Amz-Content-Sha256"
|
||||
AuthorizationHdr = "Authorization"
|
||||
ContentTypeHdr = "Content-Type"
|
||||
|
||||
UnsignedPayload = "UNSIGNED-PAYLOAD"
|
||||
StreamingUnsignedPayloadTrailer = "STREAMING-UNSIGNED-PAYLOAD-TRAILER"
|
||||
StreamingContentSHA256 = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"
|
||||
StreamingContentSHA256Trailer = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD-TRAILER"
|
||||
StreamingContentECDSASHA256 = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD"
|
||||
StreamingContentECDSASHA256Trailer = "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD-TRAILER"
|
||||
)
|
||||
|
||||
// ErrNoAuthorizationHeader is returned for unauthenticated requests.
|
||||
var ErrNoAuthorizationHeader = errors.New("no authorization header")
|
||||
|
||||
func (p prs) Read(_ []byte) (n int, err error) {
|
||||
panic("implement me")
|
||||
var ContentSHA256HeaderStandardValue = map[string]struct{}{
|
||||
UnsignedPayload: {},
|
||||
StreamingUnsignedPayloadTrailer: {},
|
||||
StreamingContentSHA256: {},
|
||||
StreamingContentSHA256Trailer: {},
|
||||
StreamingContentECDSASHA256: {},
|
||||
StreamingContentECDSASHA256Trailer: {},
|
||||
}
|
||||
|
||||
func (p prs) Seek(_ int64, _ int) (int64, error) {
|
||||
panic("implement me")
|
||||
}
|
||||
|
||||
var _ io.ReadSeeker = prs(0)
|
||||
|
||||
// New creates an instance of AuthCenter.
|
||||
func New(frostFS tokens.FrostFS, key *keys.PrivateKey, prefixes []string, config *cache.Config) Center {
|
||||
return ¢er{
|
||||
cli: tokens.New(frostFS, key, config),
|
||||
reg: NewRegexpMatcher(authorizationFieldRegexp),
|
||||
func New(creds tokens.Credentials, prefixes []string, settings CenterSettings) *Center {
|
||||
return &Center{
|
||||
cli: creds,
|
||||
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
|
||||
regV4a: NewRegexpMatcher(authorizationFieldV4aRegexp),
|
||||
postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
|
||||
allowedAccessKeyIDPrefixes: prefixes,
|
||||
settings: settings,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *center) parseAuthHeader(header string) (*AuthHeader, error) {
|
||||
submatches := c.reg.GetSubmatches(header)
|
||||
if len(submatches) != authHeaderPartsNum {
|
||||
return nil, apiErrors.GetAPIError(apiErrors.ErrAuthorizationHeaderMalformed)
|
||||
}
|
||||
const (
|
||||
signaturePreambleSigV4 = "AWS4-HMAC-SHA256"
|
||||
signaturePreambleSigV4A = "AWS4-ECDSA-P256-SHA256"
|
||||
)
|
||||
|
||||
accessKey := strings.Split(submatches["access_key_id"], "0")
|
||||
if len(accessKey) != accessKeyPartsNum {
|
||||
return nil, apiErrors.GetAPIError(apiErrors.ErrInvalidAccessKeyID)
|
||||
}
|
||||
func (c *Center) parseAuthHeader(authHeader string, headers http.Header) (*AuthHeader, error) {
|
||||
preamble, _, _ := strings.Cut(authHeader, " ")
|
||||
|
||||
signedFields := strings.Split(submatches["signed_header_fields"], ";")
|
||||
var (
|
||||
submatches map[string]string
|
||||
region string
|
||||
)
|
||||
|
||||
switch preamble {
|
||||
case signaturePreambleSigV4:
|
||||
submatches = c.reg.GetSubmatches(authHeader)
|
||||
if len(submatches) != authHeaderPartsNum {
|
||||
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader)
|
||||
}
|
||||
region = submatches["region"]
|
||||
case signaturePreambleSigV4A:
|
||||
submatches = c.regV4a.GetSubmatches(authHeader)
|
||||
if len(submatches) != authHeaderV4aPartsNum {
|
||||
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader)
|
||||
}
|
||||
region = headers.Get(AmzRegionSet)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), authHeader)
|
||||
}
|
||||
|
||||
return &AuthHeader{
|
||||
AccessKeyID: submatches["access_key_id"],
|
||||
Service: submatches["service"],
|
||||
Region: submatches["region"],
|
||||
SignatureV4: submatches["v4_signature"],
|
||||
SignedFields: signedFields,
|
||||
Region: region,
|
||||
Signature: submatches["v4_signature"],
|
||||
SignedFields: strings.Split(submatches["signed_header_fields"], ";"),
|
||||
Date: submatches["date"],
|
||||
Preamble: preamble,
|
||||
PayloadHash: headers.Get(AmzContentSHA256),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a *AuthHeader) getAddress() (oid.Address, error) {
|
||||
var addr oid.Address
|
||||
if err := addr.DecodeString(strings.ReplaceAll(a.AccessKeyID, "0", "/")); err != nil {
|
||||
return addr, apiErrors.GetAPIError(apiErrors.ErrInvalidAccessKeyID)
|
||||
}
|
||||
return addr, nil
|
||||
func IsStandardContentSHA256(key string) bool {
|
||||
_, ok := ContentSHA256HeaderStandardValue[key]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (c *center) Authenticate(r *http.Request) (*Box, error) {
|
||||
func (c *Center) Authenticate(r *http.Request) (*middleware.Box, error) {
|
||||
var (
|
||||
err error
|
||||
authHdr *AuthHeader
|
||||
|
@ -143,7 +170,7 @@ func (c *center) Authenticate(r *http.Request) (*Box, error) {
|
|||
)
|
||||
|
||||
queryValues := r.URL.Query()
|
||||
if queryValues.Get(AmzAlgorithm) == "AWS4-HMAC-SHA256" {
|
||||
if queryValues.Get(AmzAlgorithm) == signaturePreambleSigV4 {
|
||||
creds := strings.Split(queryValues.Get(AmzCredential), "/")
|
||||
if len(creds) != 5 || creds[4] != "aws4_request" {
|
||||
return nil, fmt.Errorf("bad X-Amz-Credential")
|
||||
|
@ -152,14 +179,37 @@ func (c *center) Authenticate(r *http.Request) (*Box, error) {
|
|||
AccessKeyID: creds[0],
|
||||
Service: creds[3],
|
||||
Region: creds[2],
|
||||
SignatureV4: queryValues.Get(AmzSignature),
|
||||
Signature: queryValues.Get(AmzSignature),
|
||||
SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"),
|
||||
Date: creds[1],
|
||||
IsPresigned: true,
|
||||
Preamble: signaturePreambleSigV4,
|
||||
PayloadHash: r.Header.Get(AmzContentSHA256),
|
||||
}
|
||||
authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("couldn't parse X-Amz-Expires: %w", err)
|
||||
return nil, fmt.Errorf("%w: couldn't parse X-Amz-Expires %v", apierr.GetAPIError(apierr.ErrMalformedExpires), err)
|
||||
}
|
||||
signatureDateTimeStr = queryValues.Get(AmzDate)
|
||||
} else if queryValues.Get(AmzAlgorithm) == signaturePreambleSigV4A {
|
||||
creds := strings.Split(queryValues.Get(AmzCredential), "/")
|
||||
if len(creds) != 4 || creds[3] != "aws4_request" {
|
||||
return nil, fmt.Errorf("bad X-Amz-Credential")
|
||||
}
|
||||
authHdr = &AuthHeader{
|
||||
AccessKeyID: creds[0],
|
||||
Service: creds[2],
|
||||
Region: queryValues.Get(AmzRegionSet),
|
||||
Signature: queryValues.Get(AmzSignature),
|
||||
SignedFields: strings.Split(queryValues.Get(AmzSignedHeaders), ";"),
|
||||
Date: creds[1],
|
||||
IsPresigned: true,
|
||||
Preamble: signaturePreambleSigV4A,
|
||||
PayloadHash: r.Header.Get(AmzContentSHA256),
|
||||
}
|
||||
authHdr.Expiration, err = time.ParseDuration(queryValues.Get(AmzExpires) + "s")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: couldn't parse X-Amz-Expires %v", apierr.GetAPIError(apierr.ErrMalformedExpires), err)
|
||||
}
|
||||
signatureDateTimeStr = queryValues.Get(AmzDate)
|
||||
} else {
|
||||
|
@ -168,9 +218,9 @@ func (c *center) Authenticate(r *http.Request) (*Box, error) {
|
|||
if strings.HasPrefix(r.Header.Get(ContentTypeHdr), "multipart/form-data") {
|
||||
return c.checkFormData(r)
|
||||
}
|
||||
return nil, ErrNoAuthorizationHeader
|
||||
return nil, fmt.Errorf("%w: %v", middleware.ErrNoAuthorizationHeader, authHeaderField)
|
||||
}
|
||||
authHdr, err = c.parseAuthHeader(authHeaderField[0])
|
||||
authHdr, err = c.parseAuthHeader(authHeaderField[0], r.Header)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -183,28 +233,37 @@ func (c *center) Authenticate(r *http.Request) (*Box, error) {
|
|||
return nil, fmt.Errorf("failed to parse x-amz-date header field: %w", err)
|
||||
}
|
||||
|
||||
if err := c.checkAccessKeyID(authHdr.AccessKeyID); err != nil {
|
||||
if err = c.checkAccessKeyID(authHdr.AccessKeyID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
addr, err := authHdr.getAddress()
|
||||
cnrID, err := c.getAccessBoxContainer(authHdr.AccessKeyID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
box, err := c.cli.GetBox(r.Context(), addr)
|
||||
box, attrs, err := c.cli.GetBox(r.Context(), cnrID, authHdr.AccessKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get box: %w", err)
|
||||
return nil, fmt.Errorf("get box by access key '%s': %w", authHdr.AccessKeyID, err)
|
||||
}
|
||||
|
||||
if err = checkFormatHashContentSHA256(r.Header.Get(AmzContentSHA256)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clonedRequest := cloneRequest(r, authHdr)
|
||||
if err = c.checkSign(authHdr, box, clonedRequest, signatureDateTime); err != nil {
|
||||
if err = c.checkSign(r.Context(), authHdr, box, clonedRequest, signatureDateTime); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &Box{
|
||||
AccessBox: box,
|
||||
AuthHeaders: authHdr,
|
||||
result := &middleware.Box{
|
||||
AccessBox: box,
|
||||
AuthHeaders: &middleware.AuthHeader{
|
||||
AccessKeyID: authHdr.AccessKeyID,
|
||||
Region: authHdr.Region,
|
||||
SignatureV4: authHdr.Signature,
|
||||
},
|
||||
Attributes: attrs,
|
||||
}
|
||||
if needClientTime {
|
||||
result.ClientTime = signatureDateTime
|
||||
|
@ -213,7 +272,36 @@ func (c *center) Authenticate(r *http.Request) (*Box, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
func (c center) checkAccessKeyID(accessKeyID string) error {
|
||||
func (c *Center) getAccessBoxContainer(accessKeyID string) (cid.ID, error) {
|
||||
var addr oid.Address
|
||||
if err := addr.DecodeString(strings.ReplaceAll(accessKeyID, "0", "/")); err == nil {
|
||||
return addr.Container(), nil
|
||||
}
|
||||
|
||||
cnrID, ok := c.settings.AccessBoxContainer()
|
||||
if ok {
|
||||
return cnrID, nil
|
||||
}
|
||||
|
||||
return cid.ID{}, fmt.Errorf("%w: unknown container for creds '%s'", apierr.GetAPIError(apierr.ErrInvalidAccessKeyID), accessKeyID)
|
||||
}
|
||||
|
||||
func checkFormatHashContentSHA256(hash string) error {
|
||||
if !IsStandardContentSHA256(hash) {
|
||||
hashBinary, err := hex.DecodeString(hash)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: decode hash: %s: %s", apierr.GetAPIError(apierr.ErrContentSHA256Mismatch),
|
||||
hash, err.Error())
|
||||
}
|
||||
if len(hashBinary) != sha256.Size && len(hash) != 0 {
|
||||
return fmt.Errorf("%w: invalid hash size %d", apierr.GetAPIError(apierr.ErrContentSHA256Mismatch), len(hashBinary))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Center) checkAccessKeyID(accessKeyID string) error {
|
||||
if len(c.allowedAccessKeyIDPrefixes) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
@ -224,12 +312,12 @@ func (c center) checkAccessKeyID(accessKeyID string) error {
|
|||
}
|
||||
}
|
||||
|
||||
return apiErrors.GetAPIError(apiErrors.ErrAccessDenied)
|
||||
return fmt.Errorf("%w: accesskeyID prefix isn't allowed", apierr.GetAPIError(apierr.ErrAccessDenied))
|
||||
}
|
||||
|
||||
func (c *center) checkFormData(r *http.Request) (*Box, error) {
|
||||
func (c *Center) checkFormData(r *http.Request) (*middleware.Box, error) {
|
||||
if err := r.ParseMultipartForm(maxFormSizeMemory); err != nil {
|
||||
return nil, apiErrors.GetAPIError(apiErrors.ErrInvalidArgument)
|
||||
return nil, fmt.Errorf("%w: parse multipart form with max size %d", apierr.GetAPIError(apierr.ErrInvalidArgument), maxFormSizeMemory)
|
||||
}
|
||||
|
||||
if err := prepareForm(r.MultipartForm); err != nil {
|
||||
|
@ -238,12 +326,13 @@ func (c *center) checkFormData(r *http.Request) (*Box, error) {
|
|||
|
||||
policy := MultipartFormValue(r, "policy")
|
||||
if policy == "" {
|
||||
return nil, ErrNoAuthorizationHeader
|
||||
return nil, fmt.Errorf("%w: missing policy", middleware.ErrNoAuthorizationHeader)
|
||||
}
|
||||
|
||||
submatches := c.postReg.GetSubmatches(MultipartFormValue(r, "x-amz-credential"))
|
||||
creds := MultipartFormValue(r, "x-amz-credential")
|
||||
submatches := c.postReg.GetSubmatches(creds)
|
||||
if len(submatches) != 4 {
|
||||
return nil, apiErrors.GetAPIError(apiErrors.ErrAuthorizationHeaderMalformed)
|
||||
return nil, fmt.Errorf("%w: %s", apierr.GetAPIError(apierr.ErrAuthorizationHeaderMalformed), creds)
|
||||
}
|
||||
|
||||
signatureDateTime, err := time.Parse("20060102T150405Z", MultipartFormValue(r, "x-amz-date"))
|
||||
|
@ -251,25 +340,37 @@ func (c *center) checkFormData(r *http.Request) (*Box, error) {
|
|||
return nil, fmt.Errorf("failed to parse x-amz-date field: %w", err)
|
||||
}
|
||||
|
||||
var addr oid.Address
|
||||
if err = addr.DecodeString(strings.ReplaceAll(submatches["access_key_id"], "0", "/")); err != nil {
|
||||
return nil, apiErrors.GetAPIError(apiErrors.ErrInvalidAccessKeyID)
|
||||
}
|
||||
accessKeyID := submatches["access_key_id"]
|
||||
|
||||
box, err := c.cli.GetBox(r.Context(), addr)
|
||||
cnrID, err := c.getAccessBoxContainer(accessKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get box: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
secret := box.Gate.AccessKey
|
||||
box, attrs, err := c.cli.GetBox(r.Context(), cnrID, accessKeyID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("get box by accessKeyID '%s': %w", accessKeyID, err)
|
||||
}
|
||||
|
||||
secret := box.Gate.SecretKey
|
||||
service, region := submatches["service"], submatches["region"]
|
||||
|
||||
signature := signStr(secret, service, region, signatureDateTime, policy)
|
||||
if signature != MultipartFormValue(r, "x-amz-signature") {
|
||||
return nil, apiErrors.GetAPIError(apiErrors.ErrSignatureDoesNotMatch)
|
||||
signature := SignStr(secret, service, region, signatureDateTime, policy)
|
||||
reqSignature := MultipartFormValue(r, "x-amz-signature")
|
||||
if signature != reqSignature {
|
||||
return nil, fmt.Errorf("%w: %s != %s", apierr.GetAPIError(apierr.ErrSignatureDoesNotMatch),
|
||||
reqSignature, signature)
|
||||
}
|
||||
|
||||
return &Box{AccessBox: box}, nil
|
||||
return &middleware.Box{
|
||||
AccessBox: box,
|
||||
AuthHeaders: &middleware.AuthHeader{
|
||||
AccessKeyID: accessKeyID,
|
||||
Region: region,
|
||||
SignatureV4: signature,
|
||||
},
|
||||
Attributes: attrs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func cloneRequest(r *http.Request, authHeader *AuthHeader) *http.Request {
|
||||
|
@ -293,44 +394,121 @@ func cloneRequest(r *http.Request, authHeader *AuthHeader) *http.Request {
|
|||
return otherRequest
|
||||
}
|
||||
|
||||
func (c *center) checkSign(authHeader *AuthHeader, box *accessbox.Box, request *http.Request, signatureDateTime time.Time) error {
|
||||
awsCreds := credentials.NewStaticCredentials(authHeader.AccessKeyID, box.Gate.AccessKey, "")
|
||||
signer := v4.NewSigner(awsCreds)
|
||||
signer.DisableURIPathEscaping = true
|
||||
|
||||
func (c *Center) checkSign(ctx context.Context, authHeader *AuthHeader, box *accessbox.Box, request *http.Request, signatureDateTime time.Time) error {
|
||||
var signature string
|
||||
if authHeader.IsPresigned {
|
||||
now := time.Now()
|
||||
if signatureDateTime.Add(authHeader.Expiration).Before(now) {
|
||||
return apiErrors.GetAPIError(apiErrors.ErrExpiredPresignRequest)
|
||||
}
|
||||
if now.Before(signatureDateTime) {
|
||||
return apiErrors.GetAPIError(apiErrors.ErrBadRequest)
|
||||
}
|
||||
if _, err := signer.Presign(request, nil, authHeader.Service, authHeader.Region, authHeader.Expiration, signatureDateTime); err != nil {
|
||||
return fmt.Errorf("failed to pre-sign temporary HTTP request: %w", err)
|
||||
}
|
||||
signature = request.URL.Query().Get(AmzSignature)
|
||||
} else {
|
||||
if _, err := signer.Sign(request, nil, authHeader.Service, authHeader.Region, signatureDateTime); err != nil {
|
||||
return fmt.Errorf("failed to sign temporary HTTP request: %w", err)
|
||||
}
|
||||
signature = c.reg.GetSubmatches(request.Header.Get(AuthorizationHdr))["v4_signature"]
|
||||
|
||||
if !slices.Contains(authHeader.SignedFields, "x-amz-content-sha256") && authHeader.PayloadHash == "" {
|
||||
authHeader.PayloadHash = UnsignedPayload
|
||||
}
|
||||
|
||||
if authHeader.SignatureV4 != signature {
|
||||
return apiErrors.GetAPIError(apiErrors.ErrSignatureDoesNotMatch)
|
||||
switch authHeader.Preamble {
|
||||
case signaturePreambleSigV4:
|
||||
creds := aws.Credentials{
|
||||
AccessKeyID: authHeader.AccessKeyID,
|
||||
SecretAccessKey: box.Gate.SecretKey,
|
||||
}
|
||||
signer := v4.NewSigner(func(options *v4.SignerOptions) {
|
||||
options.DisableURIPathEscaping = true
|
||||
})
|
||||
|
||||
if authHeader.IsPresigned {
|
||||
if err := checkPresignedDate(authHeader, signatureDateTime); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
signedURI, _, err := signer.PresignHTTP(ctx, creds, request, authHeader.PayloadHash, authHeader.Service, authHeader.Region, signatureDateTime)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to pre-sign temporary HTTP request: %w", err)
|
||||
}
|
||||
|
||||
u, err := url.ParseRequestURI(signedURI)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
signature = u.Query().Get(AmzSignature)
|
||||
} else {
|
||||
if err := signer.SignHTTP(ctx, creds, request, authHeader.PayloadHash, authHeader.Service, authHeader.Region, signatureDateTime); err != nil {
|
||||
return fmt.Errorf("failed to sign temporary HTTP request: %w", err)
|
||||
}
|
||||
signature = c.reg.GetSubmatches(request.Header.Get(AuthorizationHdr))["v4_signature"]
|
||||
}
|
||||
if authHeader.Signature != signature {
|
||||
return fmt.Errorf("%w: %s != %s: headers %v", apierr.GetAPIError(apierr.ErrSignatureDoesNotMatch),
|
||||
authHeader.Signature, signature, authHeader.SignedFields)
|
||||
}
|
||||
|
||||
case signaturePreambleSigV4A:
|
||||
signer := v4a.NewSigner(func(options *v4a.SignerOptions) {
|
||||
options.DisableURIPathEscaping = true
|
||||
})
|
||||
|
||||
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||
SymmetricProvider: credentials.NewStaticCredentialsProvider(authHeader.AccessKeyID, box.Gate.SecretKey, ""),
|
||||
}
|
||||
|
||||
creds, err := credAdapter.RetrievePrivateKey(request.Context())
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to derive assymetric key from credentials: %w", err)
|
||||
}
|
||||
|
||||
if !authHeader.IsPresigned {
|
||||
return signer.VerifySignature(creds, request, authHeader.PayloadHash, authHeader.Service,
|
||||
strings.Split(authHeader.Region, ","), signatureDateTime, authHeader.Signature)
|
||||
}
|
||||
|
||||
if err = checkPresignedDate(authHeader, signatureDateTime); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return signer.VerifyPresigned(creds, request, authHeader.PayloadHash, authHeader.Service,
|
||||
strings.Split(authHeader.Region, ","), signatureDateTime, authHeader.Signature)
|
||||
default:
|
||||
return fmt.Errorf("invalid preamble: %s", authHeader.Preamble)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func signStr(secret, service, region string, t time.Time, strToSign string) string {
|
||||
func checkPresignedDate(authHeader *AuthHeader, signatureDateTime time.Time) error {
|
||||
now := time.Now()
|
||||
if signatureDateTime.Add(authHeader.Expiration).Before(now) {
|
||||
return fmt.Errorf("%w: expired: now %s, signature %s", apierr.GetAPIError(apierr.ErrExpiredPresignRequest),
|
||||
now.Format(time.RFC3339), signatureDateTime.Format(time.RFC3339))
|
||||
}
|
||||
if now.Before(signatureDateTime) {
|
||||
return fmt.Errorf("%w: signature time from the future: now %s, signature %s", apierr.GetAPIError(apierr.ErrBadRequest),
|
||||
now.Format(time.RFC3339), signatureDateTime.Format(time.RFC3339))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SignStr(secret, service, region string, t time.Time, strToSign string) string {
|
||||
creds := deriveKey(secret, service, region, t)
|
||||
signature := hmacSHA256(creds, []byte(strToSign))
|
||||
return hex.EncodeToString(signature)
|
||||
}
|
||||
|
||||
func SignStrV4A(ctx context.Context, cred aws.Credentials, strToSign string) (string, error) {
|
||||
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||
SymmetricProvider: credentials.NewStaticCredentialsProvider(cred.AccessKeyID, cred.SecretAccessKey, ""),
|
||||
}
|
||||
|
||||
creds, err := credAdapter.RetrievePrivateKey(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte(strToSign))
|
||||
|
||||
sig, err := creds.PrivateKey.Sign(rand.Reader, hash.Sum(nil), crypto.SHA256)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return hex.EncodeToString(sig), nil
|
||||
}
|
||||
|
||||
func deriveKey(secret, service, region string, t time.Time) []byte {
|
||||
hmacDate := hmacSHA256([]byte("AWS4"+secret), []byte(t.UTC().Format("20060102")))
|
||||
hmacRegion := hmacSHA256(hmacDate, []byte(region))
|
||||
|
|
93
api/auth/center_fuzz_test.go
Normal file
93
api/auth/center_fuzz_test.go
Normal file
|
@ -0,0 +1,93 @@
|
|||
//go:build gofuzz
|
||||
// +build gofuzz
|
||||
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
utils "github.com/trailofbits/go-fuzz-utils"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
fuzzSuccessExitCode = 0
|
||||
fuzzFailExitCode = -1
|
||||
)
|
||||
|
||||
func InitFuzzAuthenticate() {
|
||||
}
|
||||
|
||||
func DoFuzzAuthenticate(input []byte) int {
|
||||
// FUZZER INIT
|
||||
if len(input) < 100 {
|
||||
return fuzzFailExitCode
|
||||
}
|
||||
|
||||
tp, err := utils.NewTypeProvider(input)
|
||||
if err != nil {
|
||||
return fuzzFailExitCode
|
||||
}
|
||||
|
||||
var accessKeyAddr oid.Address
|
||||
err = tp.Fill(accessKeyAddr)
|
||||
if err != nil {
|
||||
return fuzzFailExitCode
|
||||
}
|
||||
|
||||
accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0")
|
||||
secretKey, err := tp.GetString()
|
||||
if err != nil {
|
||||
return fuzzFailExitCode
|
||||
}
|
||||
awsCreds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey}
|
||||
|
||||
reqData := RequestData{
|
||||
Method: "GET",
|
||||
Endpoint: "http://localhost:8084",
|
||||
Bucket: "my-bucket",
|
||||
Object: "@obj/name",
|
||||
}
|
||||
presignData := PresignData{
|
||||
Service: "s3",
|
||||
Region: "spb",
|
||||
Lifetime: 10 * time.Minute,
|
||||
SignTime: time.Now().UTC(),
|
||||
}
|
||||
|
||||
req, err := PresignRequest(context.Background(), awsCreds, reqData, presignData, zap.NewNop())
|
||||
if req == nil {
|
||||
return fuzzFailExitCode
|
||||
}
|
||||
|
||||
expBox := &accessbox.Box{
|
||||
Gate: &accessbox.GateData{
|
||||
SecretKey: secretKey,
|
||||
},
|
||||
}
|
||||
|
||||
mock := newTokensFrostfsMock()
|
||||
mock.addBox(accessKeyAddr, expBox)
|
||||
|
||||
c := &Center{
|
||||
cli: mock,
|
||||
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
|
||||
postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
|
||||
}
|
||||
|
||||
_, _ = c.Authenticate(req)
|
||||
|
||||
return fuzzSuccessExitCode
|
||||
}
|
||||
|
||||
func FuzzAuthenticate(f *testing.F) {
|
||||
f.Fuzz(func(t *testing.T, data []byte) {
|
||||
DoFuzzAuthenticate(data)
|
||||
})
|
||||
}
|
|
@ -1,19 +1,57 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2"
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/cache"
|
||||
"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/tokens"
|
||||
frosterr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/bearer"
|
||||
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/aws/aws-sdk-go-v2/aws"
|
||||
"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/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
type centerSettingsMock struct {
|
||||
accessBoxContainer *cid.ID
|
||||
}
|
||||
|
||||
func (c *centerSettingsMock) AccessBoxContainer() (cid.ID, bool) {
|
||||
if c.accessBoxContainer == nil {
|
||||
return cid.ID{}, false
|
||||
}
|
||||
return *c.accessBoxContainer, true
|
||||
}
|
||||
|
||||
func TestAuthHeaderParse(t *testing.T) {
|
||||
defaultHeader := "AWS4-HMAC-SHA256 Credential=oid0cid/20210809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=2811ccb9e242f41426738fb1f"
|
||||
|
||||
center := ¢er{
|
||||
reg: NewRegexpMatcher(authorizationFieldRegexp),
|
||||
center := &Center{
|
||||
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
|
||||
settings: ¢erSettingsMock{},
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
|
@ -28,9 +66,10 @@ func TestAuthHeaderParse(t *testing.T) {
|
|||
AccessKeyID: "oid0cid",
|
||||
Service: "s3",
|
||||
Region: "us-east-1",
|
||||
SignatureV4: "2811ccb9e242f41426738fb1f",
|
||||
Signature: "2811ccb9e242f41426738fb1f",
|
||||
SignedFields: []string{"host", "x-amz-content-sha256", "x-amz-date"},
|
||||
Date: "20210809",
|
||||
Preamble: signaturePreambleSigV4,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -38,55 +77,13 @@ func TestAuthHeaderParse(t *testing.T) {
|
|||
err: errors.GetAPIError(errors.ErrAuthorizationHeaderMalformed),
|
||||
expected: nil,
|
||||
},
|
||||
{
|
||||
header: strings.ReplaceAll(defaultHeader, "oid0cid", "oidcid"),
|
||||
err: errors.GetAPIError(errors.ErrInvalidAccessKeyID),
|
||||
expected: nil,
|
||||
},
|
||||
} {
|
||||
authHeader, err := center.parseAuthHeader(tc.header)
|
||||
require.Equal(t, tc.err, err, tc.header)
|
||||
authHeader, err := center.parseAuthHeader(tc.header, nil)
|
||||
require.ErrorIs(t, err, tc.err, tc.header)
|
||||
require.Equal(t, tc.expected, authHeader, tc.header)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthHeaderGetAddress(t *testing.T) {
|
||||
defaulErr := errors.GetAPIError(errors.ErrInvalidAccessKeyID)
|
||||
|
||||
for _, tc := range []struct {
|
||||
authHeader *AuthHeader
|
||||
err error
|
||||
}{
|
||||
{
|
||||
authHeader: &AuthHeader{
|
||||
AccessKeyID: "vWqF8cMDRbJcvnPLALoQGnABPPhw8NyYMcGsfDPfZJM0HrgjonN8CgFvCZ3kh9BUXw4W2tJ5E7EAGhueSF122HB",
|
||||
},
|
||||
err: nil,
|
||||
},
|
||||
{
|
||||
authHeader: &AuthHeader{
|
||||
AccessKeyID: "vWqF8cMDRbJcvnPLALoQGnABPPhw8NyYMcGsfDPfZJMHrgjonN8CgFvCZ3kh9BUXw4W2tJ5E7EAGhueSF122HB",
|
||||
},
|
||||
err: defaulErr,
|
||||
},
|
||||
{
|
||||
authHeader: &AuthHeader{
|
||||
AccessKeyID: "oid0cid",
|
||||
},
|
||||
err: defaulErr,
|
||||
},
|
||||
{
|
||||
authHeader: &AuthHeader{
|
||||
AccessKeyID: "oidcid",
|
||||
},
|
||||
err: defaulErr,
|
||||
},
|
||||
} {
|
||||
_, err := tc.authHeader.getAddress()
|
||||
require.Equal(t, tc.err, err, tc.authHeader.AccessKeyID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignature(t *testing.T) {
|
||||
secret := "66be461c3cd429941c55daf42fad2b8153e5a2016ba89c9494d97677cc9d3872"
|
||||
strToSign := "eyAiZXhwaXJhdGlvbiI6ICIyMDE1LTEyLTMwVDEyOjAwOjAwLjAwMFoiLAogICJjb25kaXRpb25zIjogWwogICAgeyJidWNrZXQiOiAiYWNsIn0sCiAgICBbInN0YXJ0cy13aXRoIiwgIiRrZXkiLCAidXNlci91c2VyMS8iXSwKICAgIHsic3VjY2Vzc19hY3Rpb25fcmVkaXJlY3QiOiAiaHR0cDovL2xvY2FsaG9zdDo4MDg0L2FjbCJ9LAogICAgWyJzdGFydHMtd2l0aCIsICIkQ29udGVudC1UeXBlIiwgImltYWdlLyJdLAogICAgeyJ4LWFtei1tZXRhLXV1aWQiOiAiMTQzNjUxMjM2NTEyNzQifSwKICAgIFsic3RhcnRzLXdpdGgiLCAiJHgtYW16LW1ldGEtdGFnIiwgIiJdLAoKICAgIHsiWC1BbXotQ3JlZGVudGlhbCI6ICI4Vmk0MVBIbjVGMXNzY2J4OUhqMXdmMUU2aERUYURpNndxOGhxTU05NllKdTA1QzVDeUVkVlFoV1E2aVZGekFpTkxXaTlFc3BiUTE5ZDRuR3pTYnZVZm10TS8yMDE1MTIyOS91cy1lYXN0LTEvczMvYXdzNF9yZXF1ZXN0In0sCiAgICB7IngtYW16LWFsZ29yaXRobSI6ICJBV1M0LUhNQUMtU0hBMjU2In0sCiAgICB7IlgtQW16LURhdGUiOiAiMjAxNTEyMjlUMDAwMDAwWiIgfSwKICAgIHsieC1pZ25vcmUtdG1wIjogInNvbWV0aGluZyIgfQogIF0KfQ=="
|
||||
|
@ -96,6 +93,707 @@ func TestSignature(t *testing.T) {
|
|||
panic(err)
|
||||
}
|
||||
|
||||
signature := signStr(secret, "s3", "us-east-1", signTime, strToSign)
|
||||
signature := SignStr(secret, "s3", "us-east-1", signTime, strToSign)
|
||||
require.Equal(t, "dfbe886241d9e369cf4b329ca0f15eb27306c97aa1022cc0bb5a914c4ef87634", signature)
|
||||
}
|
||||
|
||||
func TestSignatureV4A(t *testing.T) {
|
||||
accessKeyID := "2XEbqH4M3ym7a3E3esxfZ2gRLnMwDXrCN4y1SkQg5fHa09sThVmVL3EE6xeKsyMzaqu5jPi41YCaVbnwbwCTF3bx1"
|
||||
secretKey := "00637f53f842573aaa06c2164c598973cd986880987111416cf71f1619def537"
|
||||
|
||||
signer := v4a.NewSigner(func(options *v4a.SignerOptions) {
|
||||
options.DisableURIPathEscaping = true
|
||||
options.Logger = zaptest.NewLogger(t)
|
||||
options.LogSigning = true
|
||||
})
|
||||
|
||||
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||
SymmetricProvider: credentials.NewStaticCredentialsProvider(accessKeyID, secretKey, ""),
|
||||
}
|
||||
|
||||
bodyStr := `
|
||||
1b;chunk-signature=3045022100b63692a1b20759bdabd342011823427a8952df75c93174d98ad043abca8052e002201695228a91ba986171b8d0ad20856d3d94ca3614d0a90a50a531ba8e52447b9b**
|
||||
Testing with the {sdk-java}
|
||||
0;chunk-signature=30440220455885a2d4e9f705256ca6b0a5a22f7f784780ccbd1c0a371e5db3059c91745b022073259dd44746cbd63261d628a04d25be5a32a974c077c5c2d83c8157fb323b9f****
|
||||
|
||||
`
|
||||
body := bytes.NewBufferString(bodyStr)
|
||||
|
||||
req, err := http.NewRequest("PUT", "http://localhost:8084/test/tmp", body)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Amz-Sdk-Invocation-Id", "ca3a3cde-7d26-fce6-ed9c-82f7a0573824")
|
||||
req.Header.Set("Amz-Sdk-Request", "attempt=2; max=2")
|
||||
req.Header.Set("Authorization", "AWS4-ECDSA-P256-SHA256 Credential=2XEbqH4M3ym7a3E3esxfZ2gRLnMwDXrCN4y1SkQg5fHa09sThVmVL3EE6xeKsyMzaqu5jPi41YCaVbnwbwCTF3bx1/20240904/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=30440220574244c5ff5deba388c4e3b0541a42113179b6839b3e6b4212d255a118fa9089022056f7b9b72c93f67dbcd25fe9ca67950b5913fc00bb7a62bc276c21e828c0b6c7")
|
||||
req.Header.Set("Content-Length", "360")
|
||||
req.Header.Set("Content-Type", "text/plain; charset=UTF-8")
|
||||
req.Header.Set("X-Amz-Content-Sha256", "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD")
|
||||
req.Header.Set("X-Amz-Date", "20240904T133253Z")
|
||||
req.Header.Set("X-Amz-Decoded-Content-Length", "27")
|
||||
req.Header.Set("X-Amz-Region-Set", "us-east-1")
|
||||
|
||||
service := "s3"
|
||||
regionSet := []string{"us-east-1"}
|
||||
signature := "30440220574244c5ff5deba388c4e3b0541a42113179b6839b3e6b4212d255a118fa9089022056f7b9b72c93f67dbcd25fe9ca67950b5913fc00bb7a62bc276c21e828c0b6c7"
|
||||
signingTime, err := time.Parse("20060102T150405Z", "20240904T133253Z")
|
||||
require.NoError(t, err)
|
||||
creds, err := credAdapter.RetrievePrivateKey(req.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = signer.VerifySignature(creds, req, "STREAMING-AWS4-ECDSA-P256-SHA256-PAYLOAD", service, regionSet, signingTime, signature)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestSignatureV4(t *testing.T) {
|
||||
signer := v4.NewSigner(func(options *v4.SignerOptions) {
|
||||
options.DisableURIPathEscaping = true
|
||||
options.Logger = zaptest.NewLogger(t)
|
||||
options.LogSigning = true
|
||||
})
|
||||
|
||||
creds := aws.Credentials{
|
||||
AccessKeyID: "9CBEGH8T9XfLin2pg7LG8ZxBH1PnZc1yoioViKngrUnu0CbC2mcjpcw9t4Y7AS6zsF5cJGkDhXAx5hxFDKwfZzgj7",
|
||||
SecretAccessKey: "8742218da7f905de24f633f44efe02f82c6d2a317ed6f99592627215d17816e3",
|
||||
}
|
||||
|
||||
bodyStr := `tmp2
|
||||
`
|
||||
body := bytes.NewBufferString(bodyStr)
|
||||
|
||||
req, err := http.NewRequest("PUT", "http://localhost:8084/main/tmp2", body)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential=9CBEGH8T9XfLin2pg7LG8ZxBH1PnZc1yoioViKngrUnu0CbC2mcjpcw9t4Y7AS6zsF5cJGkDhXAx5hxFDKwfZzgj7/20241210/ru/s3/aws4_request, SignedHeaders=content-md5;host;x-amz-content-sha256;x-amz-date, Signature=945664a5bccfd37a1167ca5e718e2b883f68a7ccf7f1044768e7fe58b737b7ed")
|
||||
req.Header.Set("Content-Length", "5")
|
||||
req.Header.Set("User-Agent", "aws-cli/2.13.2 Python/3.11.4 Linux/6.4.5-x64v1-xanmod1 exe/x86_64.debian.11 prompt/off command/s3api.put-object")
|
||||
req.Header.Set("Content-MD5", "DstU4KxdzBj5jTGltfyqgA==")
|
||||
req.Header.Set("Expect", "101-continue")
|
||||
req.Header.Set("X-Amz-Content-Sha256", "1f9b7417ee5445c41dbe904c3651eb0ba1c12fecff16c1bccd8df3db6e390b5f")
|
||||
req.Header.Set("X-Amz-Date", "20241210T114611Z")
|
||||
|
||||
service := "s3"
|
||||
region := "ru"
|
||||
signature := "945664a5bccfd37a1167ca5e718e2b883f68a7ccf7f1044768e7fe58b737b7ed"
|
||||
signingTime, err := time.Parse("20060102T150405Z", "20241210T114611Z")
|
||||
require.NoError(t, err)
|
||||
cloned := cloneRequest(req, &AuthHeader{SignedFields: []string{"content-md5", "host", "x-amz-content-sha256", "x-amz-date"}})
|
||||
|
||||
err = signer.SignHTTP(cloned.Context(), creds, cloned, "1f9b7417ee5445c41dbe904c3651eb0ba1c12fecff16c1bccd8df3db6e390b5f", service, region, signingTime)
|
||||
require.NoError(t, err)
|
||||
signatureComputed := NewRegexpMatcher(AuthorizationFieldRegexp).GetSubmatches(cloned.Header.Get(AuthorizationHdr))["v4_signature"]
|
||||
require.Equal(t, signature, signatureComputed, "signature mismatched")
|
||||
}
|
||||
|
||||
func TestCheckFormatContentSHA256(t *testing.T) {
|
||||
defaultErr := errors.GetAPIError(errors.ErrContentSHA256Mismatch)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
hash string
|
||||
error error
|
||||
}{
|
||||
{
|
||||
name: "invalid hash format: length and character",
|
||||
hash: "invalid-hash",
|
||||
error: defaultErr,
|
||||
},
|
||||
{
|
||||
name: "invalid hash format: length (63 characters)",
|
||||
hash: "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f7",
|
||||
error: defaultErr,
|
||||
},
|
||||
{
|
||||
name: "invalid hash format: character",
|
||||
hash: "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f7s",
|
||||
error: defaultErr,
|
||||
},
|
||||
{
|
||||
name: "invalid hash format: hash size",
|
||||
hash: "5aadb45520dcd8726b2822a7a78bb53d794f557199d5d4abdedd2c55a4bd6ca73607605c558de3db80c8e86c3196484566163ed1327e82e8b6757d1932113cb8",
|
||||
error: defaultErr,
|
||||
},
|
||||
{
|
||||
name: "unsigned payload",
|
||||
hash: "UNSIGNED-PAYLOAD",
|
||||
error: nil,
|
||||
},
|
||||
{
|
||||
name: "no hash",
|
||||
hash: "",
|
||||
error: nil,
|
||||
},
|
||||
{
|
||||
name: "correct hash format",
|
||||
hash: "ed7002b439e9ac845f22357d822bac1444730fbdb6016d3ec9432297b9ec9f73",
|
||||
error: nil,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := checkFormatHashContentSHA256(tc.hash)
|
||||
require.ErrorIs(t, err, tc.error)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type frostFSMock struct {
|
||||
objects map[string]*object.Object
|
||||
}
|
||||
|
||||
func newFrostFSMock() *frostFSMock {
|
||||
return &frostFSMock{
|
||||
objects: map[string]*object.Object{},
|
||||
}
|
||||
}
|
||||
|
||||
func (f *frostFSMock) GetCredsObject(_ context.Context, prm tokens.PrmGetCredsObject) (*object.Object, error) {
|
||||
obj, ok := f.objects[prm.AccessKeyID]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
func (f *frostFSMock) CreateObject(context.Context, tokens.PrmObjectCreate) (oid.ID, error) {
|
||||
return oid.ID{}, fmt.Errorf("the mock method is not implemented")
|
||||
}
|
||||
|
||||
func TestAuthenticate(t *testing.T) {
|
||||
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, secret, err := accessbox.PackTokens(gateData, []byte("secret"), false)
|
||||
require.NoError(t, err)
|
||||
data, err := accessBox.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
var obj object.Object
|
||||
obj.SetPayload(data)
|
||||
addr := oidtest.Address()
|
||||
obj.SetContainerID(addr.Container())
|
||||
obj.SetID(addr.Object())
|
||||
|
||||
accessKeyID := getAccessKeyID(addr)
|
||||
|
||||
frostfs := newFrostFSMock()
|
||||
frostfs.objects[accessKeyID] = &obj
|
||||
|
||||
awsCreds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secret.SecretKey}
|
||||
defaultSigner := v4.NewSigner()
|
||||
|
||||
service, region := "s3", "default"
|
||||
invalidValue := "invalid-value"
|
||||
|
||||
bigConfig := tokens.Config{
|
||||
FrostFS: frostfs,
|
||||
Key: key,
|
||||
CacheConfig: cfg,
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
region string
|
||||
prefixes []string
|
||||
request *http.Request
|
||||
err bool
|
||||
errCode errors.ErrorCode
|
||||
}{
|
||||
{
|
||||
name: "valid sign",
|
||||
prefixes: []string{addr.Container().String()},
|
||||
request: func() *http.Request {
|
||||
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())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
region: region,
|
||||
},
|
||||
{
|
||||
name: "no authorization header",
|
||||
request: func() *http.Request {
|
||||
return httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "invalid authorization header",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
r.Header.Set(AuthorizationHdr, invalidValue)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrAuthorizationHeaderMalformed,
|
||||
},
|
||||
{
|
||||
name: "invalid access key id format",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
cred := aws.Credentials{AccessKeyID: addr.Object().String(), SecretAccessKey: secret.SecretKey}
|
||||
err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrInvalidAccessKeyID,
|
||||
},
|
||||
{
|
||||
name: "not allowed access key id",
|
||||
prefixes: []string{addr.Object().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "invalid access key id value",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
cred := aws.Credentials{AccessKeyID: accessKeyID[:len(accessKeyID)-4], SecretAccessKey: secret.SecretKey}
|
||||
err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrInvalidAccessKeyID,
|
||||
},
|
||||
{
|
||||
name: "unknown access key id",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
cred := aws.Credentials{AccessKeyID: addr.Object().String() + "0" + addr.Container().String(), SecretAccessKey: secret.SecretKey}
|
||||
err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "invalid signature",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
cred := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: "secret"}
|
||||
err = v4.NewSigner().SignHTTP(ctx, cred, r, "", service, region, time.Now())
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrSignatureDoesNotMatch,
|
||||
},
|
||||
{
|
||||
name: "invalid signature - AmzDate",
|
||||
prefixes: []string{addr.Container().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now())
|
||||
r.Header.Set(AmzDate, invalidValue)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "invalid AmzContentSHA256",
|
||||
prefixes: []string{addr.Container().String()},
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
err = defaultSigner.SignHTTP(ctx, awsCreds, r, "", service, region, time.Now())
|
||||
r.Header.Set(AmzContentSHA256, invalidValue)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "valid presign",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
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())
|
||||
require.NoError(t, err)
|
||||
r.URL, err = url.ParseRequestURI(signedURI)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
region: region,
|
||||
},
|
||||
{
|
||||
name: "presign, bad X-Amz-Credential",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
query := url.Values{
|
||||
AmzAlgorithm: []string{"AWS4-HMAC-SHA256"},
|
||||
AmzCredential: []string{invalidValue},
|
||||
}
|
||||
r.URL.RawQuery = query.Encode()
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "presign, bad X-Amz-Expires",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
r.Header.Set(AmzExpires, invalidValue)
|
||||
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
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrMalformedExpires,
|
||||
},
|
||||
{
|
||||
name: "presign, expired",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
r.Header.Set(AmzExpires, "60")
|
||||
signedURI, _, err := defaultSigner.PresignHTTP(ctx, awsCreds, r, UnsignedPayload, service, region, time.Now().Add(-time.Minute))
|
||||
require.NoError(t, err)
|
||||
r.URL, err = url.ParseRequestURI(signedURI)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrExpiredPresignRequest,
|
||||
},
|
||||
{
|
||||
name: "presign, signature from future",
|
||||
request: func() *http.Request {
|
||||
r := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
r.Header.Set(AmzExpires, "60")
|
||||
signedURI, _, err := defaultSigner.PresignHTTP(ctx, awsCreds, r, UnsignedPayload, service, region, time.Now().Add(time.Minute))
|
||||
require.NoError(t, err)
|
||||
r.URL, err = url.ParseRequestURI(signedURI)
|
||||
require.NoError(t, err)
|
||||
return r
|
||||
}(),
|
||||
err: true,
|
||||
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) {
|
||||
creds := tokens.New(bigConfig)
|
||||
cntr := New(creds, tc.prefixes, ¢erSettingsMock{})
|
||||
box, err := cntr.Authenticate(tc.request)
|
||||
|
||||
if tc.err {
|
||||
require.Error(t, err)
|
||||
if tc.errCode > 0 {
|
||||
err = frosterr.UnwrapErr(err)
|
||||
require.Equal(t, errors.GetAPIError(tc.errCode), err)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, accessKeyID, box.AuthHeaders.AccessKeyID)
|
||||
require.Equal(t, tc.region, box.AuthHeaders.Region)
|
||||
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) {
|
||||
const (
|
||||
policyBase64 = "eyJleHBpcmF0aW9uIjogIjIwMjUtMTItMDFUMTI6MDA6MDAuMDAwWiIsImNvbmRpdGlvbnMiOiBbCiBbInN0YXJ0cy13aXRoIiwgIiR4LWFtei1jcmVkZW50aWFsIiwgIiJdLAogWyJzdGFydHMtd2l0aCIsICIkeC1hbXotZGF0ZSIsICIiXQpdfQ=="
|
||||
invalidValue = "invalid-value"
|
||||
defaultFieldName = "file"
|
||||
service = "s3"
|
||||
region = "default"
|
||||
)
|
||||
|
||||
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, secret, err := accessbox.PackTokens(gateData, []byte("secret"), false)
|
||||
require.NoError(t, err)
|
||||
data, err := accessBox.Marshal()
|
||||
require.NoError(t, err)
|
||||
|
||||
var obj object.Object
|
||||
obj.SetPayload(data)
|
||||
addr := oidtest.Address()
|
||||
obj.SetContainerID(addr.Container())
|
||||
obj.SetID(addr.Object())
|
||||
|
||||
accessKeyID := getAccessKeyID(addr)
|
||||
|
||||
frostfs := newFrostFSMock()
|
||||
frostfs.objects[accessKeyID] = &obj
|
||||
|
||||
invalidAccessKeyID := oidtest.Address().String() + "0" + oidtest.Address().Object().String()
|
||||
|
||||
timeToSign := time.Now()
|
||||
timeToSignStr := timeToSign.Format("20060102T150405Z")
|
||||
|
||||
bigConfig := tokens.Config{
|
||||
FrostFS: frostfs,
|
||||
Key: key,
|
||||
CacheConfig: cfg,
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
prefixes []string
|
||||
request *http.Request
|
||||
err bool
|
||||
errCode errors.ErrorCode
|
||||
}{
|
||||
{
|
||||
name: "HTTP POST valid",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "HTTP POST valid with custom field name",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, "files")
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "HTTP POST valid with field name with a capital letter",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, "File")
|
||||
}(),
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid multipart form",
|
||||
request: func() *http.Request {
|
||||
req := httptest.NewRequest(http.MethodPost, "/", nil)
|
||||
req.Header.Set(ContentTypeHdr, "multipart/form-data")
|
||||
|
||||
return req
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrInvalidArgument,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid signature date time",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, invalidValue, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid creds",
|
||||
request: func() *http.Request {
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, invalidValue, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrAuthorizationHeaderMalformed,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST missing policy",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, "", creds, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid accessKeyId",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(invalidValue, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid accessKeyId - a non-existent box",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(invalidAccessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, policyBase64)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
},
|
||||
{
|
||||
name: "HTTP POST invalid signature",
|
||||
request: func() *http.Request {
|
||||
creds := getCredsStr(accessKeyID, timeToSignStr, region, service)
|
||||
sign := SignStr(secret.SecretKey, service, region, timeToSign, invalidValue)
|
||||
|
||||
return getRequestWithMultipartForm(t, policyBase64, creds, timeToSignStr, sign, defaultFieldName)
|
||||
}(),
|
||||
err: true,
|
||||
errCode: errors.ErrSignatureDoesNotMatch,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
creds := tokens.New(bigConfig)
|
||||
cntr := New(creds, tc.prefixes, ¢erSettingsMock{})
|
||||
box, err := cntr.Authenticate(tc.request)
|
||||
|
||||
if tc.err {
|
||||
require.Error(t, err)
|
||||
if tc.errCode > 0 {
|
||||
err = frosterr.UnwrapErr(err)
|
||||
require.Equal(t, errors.GetAPIError(tc.errCode), err)
|
||||
}
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, secret.SecretKey, box.AccessBox.Gate.SecretKey)
|
||||
require.Equal(t, accessKeyID, box.AuthHeaders.AccessKeyID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func getCredsStr(accessKeyID, timeToSign, region, service string) string {
|
||||
return accessKeyID + "/" + timeToSign + "/" + region + "/" + service + "/aws4_request"
|
||||
}
|
||||
|
||||
func getRequestWithMultipartForm(t *testing.T, policy, creds, date, sign, fieldName string) *http.Request {
|
||||
body := &bytes.Buffer{}
|
||||
writer := multipart.NewWriter(body)
|
||||
defer writer.Close()
|
||||
|
||||
err := writer.WriteField("policy", policy)
|
||||
require.NoError(t, err)
|
||||
err = writer.WriteField(AmzCredential, creds)
|
||||
require.NoError(t, err)
|
||||
err = writer.WriteField(AmzDate, date)
|
||||
require.NoError(t, err)
|
||||
err = writer.WriteField(AmzSignature, sign)
|
||||
require.NoError(t, err)
|
||||
_, err = writer.CreateFormFile(fieldName, "test.txt")
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/", body)
|
||||
req.Header.Set(ContentTypeHdr, writer.FormDataContentType())
|
||||
|
||||
return req
|
||||
}
|
||||
|
||||
func getAccessKeyID(addr oid.Address) string {
|
||||
return strings.ReplaceAll(addr.EncodeToString(), "/", "0")
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/private/protocol/rest"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/smithy/encoding/httpbinding"
|
||||
v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2"
|
||||
v4 "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/v4"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type RequestData struct {
|
||||
|
@ -23,25 +29,85 @@ type PresignData struct {
|
|||
Region string
|
||||
Lifetime time.Duration
|
||||
SignTime time.Time
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
// PresignRequest forms pre-signed request to access objects without aws credentials.
|
||||
func PresignRequest(creds *credentials.Credentials, reqData RequestData, presignData PresignData) (*http.Request, error) {
|
||||
urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, rest.EscapePath(reqData.Bucket, false), rest.EscapePath(reqData.Object, false))
|
||||
func PresignRequest(ctx context.Context, creds aws.Credentials, reqData RequestData, presignData PresignData, log *zap.Logger) (*http.Request, error) {
|
||||
urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, httpbinding.EscapePath(reqData.Bucket, false), httpbinding.EscapePath(reqData.Object, false))
|
||||
req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new request: %w", err)
|
||||
}
|
||||
|
||||
for k, v := range presignData.Headers {
|
||||
req.Header.Set(k, v) // maybe we should filter system header (or keep responsibility on caller)
|
||||
}
|
||||
req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z"))
|
||||
req.Header.Set(ContentTypeHdr, "text/plain")
|
||||
req.Header.Set(AmzExpires, strconv.FormatFloat(presignData.Lifetime.Round(time.Second).Seconds(), 'f', 0, 64))
|
||||
|
||||
signer := v4.NewSigner(creds)
|
||||
signer.DisableURIPathEscaping = true
|
||||
signer := v4.NewSigner(func(options *v4.SignerOptions) {
|
||||
options.DisableURIPathEscaping = true
|
||||
options.LogSigning = true
|
||||
options.Logger = log
|
||||
})
|
||||
|
||||
if _, err = signer.Presign(req, nil, presignData.Service, presignData.Region, presignData.Lifetime, presignData.SignTime); err != nil {
|
||||
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 {
|
||||
return nil, fmt.Errorf("presign: %w", err)
|
||||
}
|
||||
|
||||
if req.URL, err = url.ParseRequestURI(signedURI); err != nil {
|
||||
return nil, fmt.Errorf("parse signed URI: %w", err)
|
||||
}
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
// PresignRequestV4a forms pre-signed request to access objects without aws credentials.
|
||||
func PresignRequestV4a(cred aws.Credentials, reqData RequestData, presignData PresignData, log *zap.Logger) (*http.Request, error) {
|
||||
urlStr := fmt.Sprintf("%s/%s/%s", reqData.Endpoint, httpbinding.EscapePath(reqData.Bucket, false), httpbinding.EscapePath(reqData.Object, false))
|
||||
req, err := http.NewRequest(strings.ToUpper(reqData.Method), urlStr, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create new request: %w", err)
|
||||
}
|
||||
|
||||
for k, v := range presignData.Headers {
|
||||
req.Header.Set(k, v) // maybe we should filter system header (or keep responsibility on caller)
|
||||
}
|
||||
|
||||
req.Header.Set(AmzDate, presignData.SignTime.Format("20060102T150405Z"))
|
||||
req.Header.Set(AmzExpires, strconv.FormatFloat(presignData.Lifetime.Round(time.Second).Seconds(), 'f', 0, 64))
|
||||
|
||||
signer := v4a.NewSigner(func(options *v4a.SignerOptions) {
|
||||
options.DisableURIPathEscaping = true
|
||||
options.LogSigning = true
|
||||
options.Logger = log
|
||||
})
|
||||
|
||||
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||
SymmetricProvider: credentials.NewStaticCredentialsProvider(cred.AccessKeyID, cred.SecretAccessKey, ""),
|
||||
}
|
||||
|
||||
creds, err := credAdapter.RetrievePrivateKey(req.Context())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive assymetric key from credentials: %w", err)
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf("presign: %w", err)
|
||||
}
|
||||
|
||||
return http.NewRequest(reqData.Method, presignedURL, nil)
|
||||
}
|
||||
|
|
|
@ -2,19 +2,23 @@ package auth
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v4a "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/tokens"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
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"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
credentialsv2 "github.com/aws/aws-sdk-go-v2/credentials"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
var _ tokens.Credentials = (*credentialsMock)(nil)
|
||||
|
@ -30,34 +34,36 @@ func newTokensFrostfsMock() *credentialsMock {
|
|||
}
|
||||
|
||||
func (m credentialsMock) addBox(addr oid.Address, box *accessbox.Box) {
|
||||
m.boxes[addr.String()] = box
|
||||
m.boxes[getAccessKeyID(addr)] = box
|
||||
}
|
||||
|
||||
func (m credentialsMock) GetBox(_ context.Context, addr oid.Address) (*accessbox.Box, error) {
|
||||
box, ok := m.boxes[addr.String()]
|
||||
func (m credentialsMock) GetBox(_ context.Context, _ cid.ID, accessKeyID string) (*accessbox.Box, []object.Attribute, error) {
|
||||
box, ok := m.boxes[accessKeyID]
|
||||
if !ok {
|
||||
return nil, &apistatus.ObjectNotFound{}
|
||||
return nil, nil, &apistatus.ObjectNotFound{}
|
||||
}
|
||||
|
||||
return box, nil
|
||||
return box, nil, nil
|
||||
}
|
||||
|
||||
func (m credentialsMock) Put(context.Context, cid.ID, user.ID, *accessbox.AccessBox, uint64, ...*keys.PublicKey) (oid.Address, error) {
|
||||
func (m credentialsMock) Put(context.Context, tokens.CredentialsParam) (oid.Address, error) {
|
||||
return oid.Address{}, nil
|
||||
}
|
||||
|
||||
func (m credentialsMock) Update(context.Context, oid.Address, user.ID, *accessbox.AccessBox, uint64, ...*keys.PublicKey) (oid.Address, error) {
|
||||
func (m credentialsMock) Update(context.Context, tokens.CredentialsParam) (oid.Address, error) {
|
||||
return oid.Address{}, nil
|
||||
}
|
||||
|
||||
func TestCheckSign(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
|
||||
var accessKeyAddr oid.Address
|
||||
err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto")
|
||||
require.NoError(t, err)
|
||||
|
||||
accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0")
|
||||
secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb"
|
||||
awsCreds := credentials.NewStaticCredentials(accessKeyID, secretKey, "")
|
||||
awsCreds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey}
|
||||
|
||||
reqData := RequestData{
|
||||
Method: "GET",
|
||||
|
@ -70,26 +76,126 @@ func TestCheckSign(t *testing.T) {
|
|||
Region: "spb",
|
||||
Lifetime: 10 * time.Minute,
|
||||
SignTime: time.Now().UTC(),
|
||||
Headers: map[string]string{
|
||||
ContentTypeHdr: "text/plain",
|
||||
},
|
||||
}
|
||||
|
||||
req, err := PresignRequest(awsCreds, reqData, presignData)
|
||||
req, err := PresignRequest(ctx, awsCreds, reqData, presignData, zaptest.NewLogger(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
expBox := &accessbox.Box{
|
||||
Gate: &accessbox.GateData{
|
||||
AccessKey: secretKey,
|
||||
SecretKey: secretKey,
|
||||
},
|
||||
}
|
||||
|
||||
mock := newTokensFrostfsMock()
|
||||
mock.addBox(accessKeyAddr, expBox)
|
||||
|
||||
c := ¢er{
|
||||
c := &Center{
|
||||
cli: mock,
|
||||
reg: NewRegexpMatcher(AuthorizationFieldRegexp),
|
||||
postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
|
||||
settings: ¢erSettingsMock{},
|
||||
}
|
||||
box, err := c.Authenticate(req)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, expBox, box.AccessBox)
|
||||
}
|
||||
|
||||
func TestCheckSignV4a(t *testing.T) {
|
||||
var accessKeyAddr oid.Address
|
||||
err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto")
|
||||
require.NoError(t, err)
|
||||
|
||||
accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0")
|
||||
secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb"
|
||||
awsCreds := aws.Credentials{AccessKeyID: accessKeyID, SecretAccessKey: secretKey}
|
||||
|
||||
reqData := RequestData{
|
||||
Method: "GET",
|
||||
Endpoint: "http://localhost:8084",
|
||||
Bucket: "my-bucket",
|
||||
Object: "@obj/name",
|
||||
}
|
||||
presignData := PresignData{
|
||||
Service: "s3",
|
||||
Region: "spb",
|
||||
Lifetime: 10 * time.Minute,
|
||||
SignTime: time.Now().UTC(),
|
||||
Headers: map[string]string{
|
||||
ContentTypeHdr: "text/plain",
|
||||
},
|
||||
}
|
||||
|
||||
req, err := PresignRequestV4a(awsCreds, reqData, presignData, zaptest.NewLogger(t))
|
||||
require.NoError(t, err)
|
||||
|
||||
req.Header.Set(ContentTypeHdr, "text/plain")
|
||||
|
||||
expBox := &accessbox.Box{
|
||||
Gate: &accessbox.GateData{
|
||||
SecretKey: secretKey,
|
||||
},
|
||||
}
|
||||
|
||||
mock := newTokensFrostfsMock()
|
||||
mock.addBox(accessKeyAddr, expBox)
|
||||
|
||||
c := &Center{
|
||||
cli: mock,
|
||||
reg: NewRegexpMatcher(authorizationFieldRegexp),
|
||||
regV4a: NewRegexpMatcher(authorizationFieldV4aRegexp),
|
||||
postReg: NewRegexpMatcher(postPolicyCredentialRegexp),
|
||||
}
|
||||
box, err := c.Authenticate(req)
|
||||
require.NoError(t, err)
|
||||
require.EqualValues(t, expBox, box.AccessBox)
|
||||
}
|
||||
|
||||
func TestPresignRequestV4a(t *testing.T) {
|
||||
var accessKeyAddr oid.Address
|
||||
err := accessKeyAddr.DecodeString("8N7CYBY74kxZXoyvA5UNdmovaXqFpwNfvEPsqaN81es2/3tDwq5tR8fByrJcyJwyiuYX7Dae8tyDT7pd8oaL1MBto")
|
||||
require.NoError(t, err)
|
||||
|
||||
accessKeyID := strings.ReplaceAll(accessKeyAddr.String(), "/", "0")
|
||||
secretKey := "713d0a0b9efc7d22923e17b0402a6a89b4273bc711c8bacb2da1b643d0006aeb"
|
||||
|
||||
signer := v4a.NewSigner(func(options *v4a.SignerOptions) {
|
||||
options.DisableURIPathEscaping = true
|
||||
options.LogSigning = true
|
||||
options.Logger = zaptest.NewLogger(t)
|
||||
})
|
||||
|
||||
credAdapter := v4a.SymmetricCredentialAdaptor{
|
||||
SymmetricProvider: credentialsv2.NewStaticCredentialsProvider(accessKeyID, secretKey, ""),
|
||||
}
|
||||
|
||||
creds, err := credAdapter.RetrievePrivateKey(context.TODO())
|
||||
require.NoError(t, err)
|
||||
|
||||
signingTime := time.Now()
|
||||
service := "s3"
|
||||
regionSet := []string{"spb"}
|
||||
|
||||
req, err := http.NewRequest("GET", "http://localhost:8084/bucket/object", nil)
|
||||
require.NoError(t, err)
|
||||
req.Header.Set(AmzExpires, "600")
|
||||
|
||||
presignedURL, hdr, err := signer.PresignHTTP(req.Context(), creds, req, "", service, regionSet, signingTime)
|
||||
require.NoError(t, err)
|
||||
|
||||
fmt.Println(presignedURL)
|
||||
fmt.Println(hdr)
|
||||
|
||||
signature := req.URL.Query().Get(AmzSignature)
|
||||
|
||||
r, err := http.NewRequest("GET", presignedURL, nil)
|
||||
require.NoError(t, err)
|
||||
query := r.URL.Query()
|
||||
query.Del(AmzSignature)
|
||||
r.URL.RawQuery = query.Encode()
|
||||
|
||||
err = signer.VerifyPresigned(creds, r, "", service, regionSet, signingTime, signature)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
|
37
api/auth/signer/smithy/encoding/httpbinding/path_replace.go
Normal file
37
api/auth/signer/smithy/encoding/httpbinding/path_replace.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
// This file is part of https://github.com/aws/smithy-go/blob/f0c6adfdec6e40bb8bb2920a40d016943b4ad762/encoding/httpbinding/path_replace.go
|
||||
|
||||
package httpbinding
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// EscapePath escapes part of a URL path in Amazon style.
|
||||
func EscapePath(path string, encodeSep bool) string {
|
||||
var buf bytes.Buffer
|
||||
for i := 0; i < len(path); i++ {
|
||||
c := path[i]
|
||||
if noEscape[c] || (c == '/' && !encodeSep) {
|
||||
buf.WriteByte(c)
|
||||
} else {
|
||||
fmt.Fprintf(&buf, "%%%02X", c)
|
||||
}
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
var noEscape [256]bool
|
||||
|
||||
func init() {
|
||||
for i := 0; i < len(noEscape); i++ {
|
||||
// AWS expects every character except these to be escaped
|
||||
noEscape[i] = (i >= 'A' && i <= 'Z') ||
|
||||
(i >= 'a' && i <= 'z') ||
|
||||
(i >= '0' && i <= '9') ||
|
||||
i == '-' ||
|
||||
i == '.' ||
|
||||
i == '_' ||
|
||||
i == '~'
|
||||
}
|
||||
}
|
|
@ -1,87 +0,0 @@
|
|||
package v4
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// validator houses a set of rule needed for validation of a
|
||||
// string value.
|
||||
type rules []rule
|
||||
|
||||
// rule interface allows for more flexible rules and just simply
|
||||
// checks whether or not a value adheres to that rule.
|
||||
type rule interface {
|
||||
IsValid(value string) bool
|
||||
}
|
||||
|
||||
// IsValid will iterate through all rules and see if any rules
|
||||
// apply to the value and supports nested rules.
|
||||
func (r rules) IsValid(value string) bool {
|
||||
for _, rule := range r {
|
||||
if rule.IsValid(value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// mapRule generic rule for maps.
|
||||
type mapRule map[string]struct{}
|
||||
|
||||
// IsValid for the map rule satisfies whether it exists in the map.
|
||||
func (m mapRule) IsValid(value string) bool {
|
||||
_, ok := m[value]
|
||||
return ok
|
||||
}
|
||||
|
||||
// whitelist is a generic rule for whitelisting.
|
||||
type whitelist struct {
|
||||
rule
|
||||
}
|
||||
|
||||
// IsValid for whitelist checks if the value is within the whitelist.
|
||||
func (w whitelist) IsValid(value string) bool {
|
||||
return w.rule.IsValid(value)
|
||||
}
|
||||
|
||||
// blacklist is a generic rule for blacklisting.
|
||||
type blacklist struct {
|
||||
rule
|
||||
}
|
||||
|
||||
// IsValid for whitelist checks if the value is within the whitelist.
|
||||
func (b blacklist) IsValid(value string) bool {
|
||||
return !b.rule.IsValid(value)
|
||||
}
|
||||
|
||||
type patterns []string
|
||||
|
||||
// IsValid for patterns checks each pattern and returns if a match has
|
||||
// been found.
|
||||
func (p patterns) IsValid(value string) bool {
|
||||
for _, pattern := range p {
|
||||
if HasPrefixFold(value, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HasPrefixFold tests whether the string s begins with prefix, interpreted as UTF-8 strings,
|
||||
// under Unicode case-folding.
|
||||
func HasPrefixFold(s, prefix string) bool {
|
||||
return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix)
|
||||
}
|
||||
|
||||
// inclusiveRules rules allow for rules to depend on one another.
|
||||
type inclusiveRules []rule
|
||||
|
||||
// IsValid will return true if all rules are true.
|
||||
func (r inclusiveRules) IsValid(value string) bool {
|
||||
for _, rule := range r {
|
||||
if !rule.IsValid(value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
package v4
|
||||
|
||||
// WithUnsignedPayload will enable and set the UnsignedPayload field to
|
||||
// true of the signer.
|
||||
func WithUnsignedPayload(v4 *Signer) {
|
||||
v4.UnsignedPayload = true
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
//go:build go1.7
|
||||
// +build go1.7
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
)
|
||||
|
||||
func requestContext(r *http.Request) aws.Context {
|
||||
return r.Context()
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
package v4
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
)
|
||||
|
||||
type credentialValueProvider interface {
|
||||
Get() (credentials.Value, error)
|
||||
}
|
||||
|
||||
// StreamSigner implements signing of event stream encoded payloads.
|
||||
type StreamSigner struct {
|
||||
region string
|
||||
service string
|
||||
|
||||
credentials credentialValueProvider
|
||||
|
||||
prevSig []byte
|
||||
}
|
||||
|
||||
// NewStreamSigner creates a SigV4 signer used to sign Event Stream encoded messages.
|
||||
func NewStreamSigner(region, service string, seedSignature []byte, credentials *credentials.Credentials) *StreamSigner {
|
||||
return &StreamSigner{
|
||||
region: region,
|
||||
service: service,
|
||||
credentials: credentials,
|
||||
prevSig: seedSignature,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSignature takes an event stream encoded headers and payload and returns a signature.
|
||||
func (s *StreamSigner) GetSignature(headers, payload []byte, date time.Time) ([]byte, error) {
|
||||
credValue, err := s.credentials.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sigKey := deriveSigningKey(s.region, s.service, credValue.SecretAccessKey, date)
|
||||
|
||||
keyPath := buildSigningScope(s.region, s.service, date)
|
||||
|
||||
stringToSign := buildEventStreamStringToSign(headers, payload, s.prevSig, keyPath, date)
|
||||
|
||||
signature := hmacSHA256(sigKey, []byte(stringToSign))
|
||||
s.prevSig = signature
|
||||
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
func buildEventStreamStringToSign(headers, payload, prevSig []byte, scope string, date time.Time) string {
|
||||
return strings.Join([]string{
|
||||
"AWS4-HMAC-SHA256-PAYLOAD",
|
||||
formatTime(date),
|
||||
scope,
|
||||
hex.EncodeToString(prevSig),
|
||||
hex.EncodeToString(hashSHA256(headers)),
|
||||
hex.EncodeToString(hashSHA256(payload)),
|
||||
}, "\n")
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
//go:build go1.5
|
||||
// +build go1.5
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func getURIPath(u *url.URL) string {
|
||||
var uri string
|
||||
|
||||
if len(u.Opaque) > 0 {
|
||||
uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/")
|
||||
} else {
|
||||
uri = u.EscapedPath()
|
||||
}
|
||||
|
||||
if len(uri) == 0 {
|
||||
uri = "/"
|
||||
}
|
||||
|
||||
return uri
|
||||
}
|
|
@ -1,858 +0,0 @@
|
|||
// Package v4 implements signing for AWS V4 signer
|
||||
//
|
||||
// Provides request signing for request that need to be signed with
|
||||
// AWS V4 Signatures.
|
||||
//
|
||||
// # Standalone Signer
|
||||
//
|
||||
// Generally using the signer outside of the SDK should not require any additional
|
||||
// logic when using Go v1.5 or higher. The signer does this by taking advantage
|
||||
// of the URL.EscapedPath method. If your request URI requires additional escaping
|
||||
// you many need to use the URL.Opaque to define what the raw URI should be sent
|
||||
// to the service as.
|
||||
//
|
||||
// The signer will first check the URL.Opaque field, and use its value if set.
|
||||
// The signer does require the URL.Opaque field to be set in the form of:
|
||||
//
|
||||
// "//<hostname>/<path>"
|
||||
//
|
||||
// // e.g.
|
||||
// "//example.com/some/path"
|
||||
//
|
||||
// The leading "//" and hostname are required or the URL.Opaque escaping will
|
||||
// not work correctly.
|
||||
//
|
||||
// If URL.Opaque is not set the signer will fallback to the URL.EscapedPath()
|
||||
// method and using the returned value. If you're using Go v1.4 you must set
|
||||
// URL.Opaque if the URI path needs escaping. If URL.Opaque is not set with
|
||||
// Go v1.5 the signer will fallback to URL.Path.
|
||||
//
|
||||
// AWS v4 signature validation requires that the canonical string's URI path
|
||||
// element must be the URI escaped form of the HTTP request's path.
|
||||
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||
//
|
||||
// The Go HTTP client will perform escaping automatically on the request. Some
|
||||
// of these escaping may cause signature validation errors because the HTTP
|
||||
// request differs from the URI path or query that the signature was generated.
|
||||
// https://golang.org/pkg/net/url/#URL.EscapedPath
|
||||
//
|
||||
// Because of this, it is recommended that when using the signer outside of the
|
||||
// SDK that explicitly escaping the request prior to being signed is preferable,
|
||||
// and will help prevent signature validation errors. This can be done by setting
|
||||
// the URL.Opaque or URL.RawPath. The SDK will use URL.Opaque first and then
|
||||
// call URL.EscapedPath() if Opaque is not set.
|
||||
//
|
||||
// If signing a request intended for HTTP2 server, and you're using Go 1.6.2
|
||||
// through 1.7.4 you should use the URL.RawPath as the pre-escaped form of the
|
||||
// request URL. https://github.com/golang/go/issues/16847 points to a bug in
|
||||
// Go pre 1.8 that fails to make HTTP2 requests using absolute URL in the HTTP
|
||||
// message. URL.Opaque generally will force Go to make requests with absolute URL.
|
||||
// URL.RawPath does not do this, but RawPath must be a valid escaping of Path
|
||||
// or url.EscapedPath will ignore the RawPath escaping.
|
||||
//
|
||||
// Test `TestStandaloneSign` provides a complete example of using the signer
|
||||
// outside of the SDK and pre-escaping the URI path.
|
||||
package v4
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/request"
|
||||
"github.com/aws/aws-sdk-go/private/protocol/rest"
|
||||
)
|
||||
|
||||
const (
|
||||
authorizationHeader = "Authorization"
|
||||
authHeaderSignatureElem = "Signature="
|
||||
signatureQueryKey = "X-Amz-Signature"
|
||||
|
||||
authHeaderPrefix = "AWS4-HMAC-SHA256"
|
||||
timeFormat = "20060102T150405Z"
|
||||
shortTimeFormat = "20060102"
|
||||
awsV4Request = "aws4_request"
|
||||
|
||||
// emptyStringSHA256 is a SHA256 of an empty string.
|
||||
emptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
|
||||
)
|
||||
|
||||
var ignoredPresignHeaders = rules{
|
||||
blacklist{
|
||||
mapRule{
|
||||
authorizationHeader: struct{}{},
|
||||
"User-Agent": struct{}{},
|
||||
"X-Amzn-Trace-Id": struct{}{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// drop User-Agent header to be compatible with aws sdk java v1.
|
||||
var ignoredHeaders = rules{
|
||||
blacklist{
|
||||
mapRule{
|
||||
authorizationHeader: struct{}{},
|
||||
"X-Amzn-Trace-Id": struct{}{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// requiredSignedHeaders is a whitelist for build canonical headers.
|
||||
var requiredSignedHeaders = rules{
|
||||
whitelist{
|
||||
mapRule{
|
||||
"Cache-Control": struct{}{},
|
||||
"Content-Disposition": struct{}{},
|
||||
"Content-Encoding": struct{}{},
|
||||
"Content-Language": struct{}{},
|
||||
"Content-Md5": struct{}{},
|
||||
"Content-Type": struct{}{},
|
||||
"Expires": struct{}{},
|
||||
"If-Match": struct{}{},
|
||||
"If-Modified-Since": struct{}{},
|
||||
"If-None-Match": struct{}{},
|
||||
"If-Unmodified-Since": struct{}{},
|
||||
"Range": struct{}{},
|
||||
"X-Amz-Acl": struct{}{},
|
||||
"X-Amz-Copy-Source": struct{}{},
|
||||
"X-Amz-Copy-Source-If-Match": struct{}{},
|
||||
"X-Amz-Copy-Source-If-Modified-Since": struct{}{},
|
||||
"X-Amz-Copy-Source-If-None-Match": struct{}{},
|
||||
"X-Amz-Copy-Source-If-Unmodified-Since": struct{}{},
|
||||
"X-Amz-Copy-Source-Range": struct{}{},
|
||||
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||||
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{},
|
||||
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||||
"X-Amz-Grant-Full-control": struct{}{},
|
||||
"X-Amz-Grant-Read": struct{}{},
|
||||
"X-Amz-Grant-Read-Acp": struct{}{},
|
||||
"X-Amz-Grant-Write": struct{}{},
|
||||
"X-Amz-Grant-Write-Acp": struct{}{},
|
||||
"X-Amz-Metadata-Directive": struct{}{},
|
||||
"X-Amz-Mfa": struct{}{},
|
||||
"X-Amz-Request-Payer": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Customer-Key": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||||
"X-Amz-Storage-Class": struct{}{},
|
||||
"X-Amz-Tagging": struct{}{},
|
||||
"X-Amz-Website-Redirect-Location": struct{}{},
|
||||
"X-Amz-Content-Sha256": struct{}{},
|
||||
},
|
||||
},
|
||||
patterns{"X-Amz-Meta-"},
|
||||
}
|
||||
|
||||
// allowedHoisting is a whitelist for build query headers. The boolean value
|
||||
// represents whether or not it is a pattern.
|
||||
var allowedQueryHoisting = inclusiveRules{
|
||||
blacklist{requiredSignedHeaders},
|
||||
patterns{"X-Amz-"},
|
||||
}
|
||||
|
||||
// Signer applies AWS v4 signing to given request. Use this to sign requests
|
||||
// that need to be signed with AWS V4 Signatures.
|
||||
type Signer struct {
|
||||
// The authentication credentials the request will be signed against.
|
||||
// This value must be set to sign requests.
|
||||
Credentials *credentials.Credentials
|
||||
|
||||
// Sets the log level the signer should use when reporting information to
|
||||
// the logger. If the logger is nil nothing will be logged. See
|
||||
// aws.LogLevelType for more information on available logging levels
|
||||
//
|
||||
// By default nothing will be logged.
|
||||
Debug aws.LogLevelType
|
||||
|
||||
// The logger loging information will be written to. If there the logger
|
||||
// is nil, nothing will be logged.
|
||||
Logger aws.Logger
|
||||
|
||||
// Disables the Signer's moving HTTP header key/value pairs from the HTTP
|
||||
// request header to the request's query string. This is most commonly used
|
||||
// with pre-signed requests preventing headers from being added to the
|
||||
// request's query string.
|
||||
DisableHeaderHoisting bool
|
||||
|
||||
// Disables the automatic escaping of the URI path of the request for the
|
||||
// siganture's canonical string's path. For services that do not need additional
|
||||
// escaping then use this to disable the signer escaping the path.
|
||||
//
|
||||
// S3 is an example of a service that does not need additional escaping.
|
||||
//
|
||||
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||
DisableURIPathEscaping bool
|
||||
|
||||
// Disables the automatical setting of the HTTP request's Body field with the
|
||||
// io.ReadSeeker passed in to the signer. This is useful if you're using a
|
||||
// custom wrapper around the body for the io.ReadSeeker and want to preserve
|
||||
// the Body value on the Request.Body.
|
||||
//
|
||||
// This does run the risk of signing a request with a body that will not be
|
||||
// sent in the request. Need to ensure that the underlying data of the Body
|
||||
// values are the same.
|
||||
DisableRequestBodyOverwrite bool
|
||||
|
||||
// currentTimeFn returns the time value which represents the current time.
|
||||
// This value should only be used for testing. If it is nil the default
|
||||
// time.Now will be used.
|
||||
currentTimeFn func() time.Time
|
||||
|
||||
// UnsignedPayload will prevent signing of the payload. This will only
|
||||
// work for services that have support for this.
|
||||
UnsignedPayload bool
|
||||
}
|
||||
|
||||
// NewSigner returns a Signer pointer configured with the credentials and optional
|
||||
// option values provided. If not options are provided the Signer will use its
|
||||
// default configuration.
|
||||
func NewSigner(credentials *credentials.Credentials, options ...func(*Signer)) *Signer {
|
||||
v4 := &Signer{
|
||||
Credentials: credentials,
|
||||
}
|
||||
|
||||
for _, option := range options {
|
||||
option(v4)
|
||||
}
|
||||
|
||||
return v4
|
||||
}
|
||||
|
||||
type signingCtx struct {
|
||||
ServiceName string
|
||||
Region string
|
||||
Request *http.Request
|
||||
Body io.ReadSeeker
|
||||
Query url.Values
|
||||
Time time.Time
|
||||
ExpireTime time.Duration
|
||||
SignedHeaderVals http.Header
|
||||
|
||||
DisableURIPathEscaping bool
|
||||
|
||||
credValues credentials.Value
|
||||
isPresign bool
|
||||
unsignedPayload bool
|
||||
|
||||
bodyDigest string
|
||||
signedHeaders string
|
||||
canonicalHeaders string
|
||||
canonicalString string
|
||||
credentialString string
|
||||
stringToSign string
|
||||
signature string
|
||||
}
|
||||
|
||||
// Sign signs AWS v4 requests with the provided body, service name, region the
|
||||
// request is made to, and time the request is signed at. The signTime allows
|
||||
// you to specify that a request is signed for the future, and cannot be
|
||||
// used until then.
|
||||
//
|
||||
// Returns a list of HTTP headers that were included in the signature or an
|
||||
// error if signing the request failed. Generally for signed requests this value
|
||||
// is not needed as the full request context will be captured by the http.Request
|
||||
// value. It is included for reference though.
|
||||
//
|
||||
// Sign will set the request's Body to be the `body` parameter passed in. If
|
||||
// the body is not already an io.ReadCloser, it will be wrapped within one. If
|
||||
// a `nil` body parameter passed to Sign, the request's Body field will be
|
||||
// also set to nil. Its important to note that this functionality will not
|
||||
// change the request's ContentLength of the request.
|
||||
//
|
||||
// Sign differs from Presign in that it will sign the request using HTTP
|
||||
// header values. This type of signing is intended for http.Request values that
|
||||
// will not be shared, or are shared in a way the header values on the request
|
||||
// will not be lost.
|
||||
//
|
||||
// The requests body is an io.ReadSeeker so the SHA256 of the body can be
|
||||
// generated. To bypass the signer computing the hash you can set the
|
||||
// "X-Amz-Content-Sha256" header with a precomputed value. The signer will
|
||||
// only compute the hash if the request header value is empty.
|
||||
func (v4 Signer) Sign(r *http.Request, body io.ReadSeeker, service, region string, signTime time.Time) (http.Header, error) {
|
||||
return v4.signWithBody(r, body, service, region, 0, false, signTime)
|
||||
}
|
||||
|
||||
// Presign signs AWS v4 requests with the provided body, service name, region
|
||||
// the request is made to, and time the request is signed at. The signTime
|
||||
// allows you to specify that a request is signed for the future, and cannot
|
||||
// be used until then.
|
||||
//
|
||||
// Returns a list of HTTP headers that were included in the signature or an
|
||||
// error if signing the request failed. For presigned requests these headers
|
||||
// and their values must be included on the HTTP request when it is made. This
|
||||
// is helpful to know what header values need to be shared with the party the
|
||||
// presigned request will be distributed to.
|
||||
//
|
||||
// Presign differs from Sign in that it will sign the request using query string
|
||||
// instead of header values. This allows you to share the Presigned Request's
|
||||
// URL with third parties, or distribute it throughout your system with minimal
|
||||
// dependencies.
|
||||
//
|
||||
// Presign also takes an exp value which is the duration the
|
||||
// signed request will be valid after the signing time. This is allows you to
|
||||
// set when the request will expire.
|
||||
//
|
||||
// The requests body is an io.ReadSeeker so the SHA256 of the body can be
|
||||
// generated. To bypass the signer computing the hash you can set the
|
||||
// "X-Amz-Content-Sha256" header with a precomputed value. The signer will
|
||||
// only compute the hash if the request header value is empty.
|
||||
//
|
||||
// Presigning a S3 request will not compute the body's SHA256 hash by default.
|
||||
// This is done due to the general use case for S3 presigned URLs is to share
|
||||
// PUT/GET capabilities. If you would like to include the body's SHA256 in the
|
||||
// presigned request's signature you can set the "X-Amz-Content-Sha256"
|
||||
// HTTP header and that will be included in the request's signature.
|
||||
func (v4 Signer) Presign(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, signTime time.Time) (http.Header, error) {
|
||||
return v4.signWithBody(r, body, service, region, exp, true, signTime)
|
||||
}
|
||||
|
||||
func (v4 Signer) signWithBody(r *http.Request, body io.ReadSeeker, service, region string, exp time.Duration, isPresign bool, signTime time.Time) (http.Header, error) {
|
||||
currentTimeFn := v4.currentTimeFn
|
||||
if currentTimeFn == nil {
|
||||
currentTimeFn = time.Now
|
||||
}
|
||||
|
||||
ctx := &signingCtx{
|
||||
Request: r,
|
||||
Body: body,
|
||||
Query: r.URL.Query(),
|
||||
Time: signTime,
|
||||
ExpireTime: exp,
|
||||
isPresign: isPresign,
|
||||
ServiceName: service,
|
||||
Region: region,
|
||||
DisableURIPathEscaping: v4.DisableURIPathEscaping,
|
||||
unsignedPayload: v4.UnsignedPayload,
|
||||
}
|
||||
|
||||
for key := range ctx.Query {
|
||||
sort.Strings(ctx.Query[key])
|
||||
}
|
||||
|
||||
if ctx.isRequestSigned() {
|
||||
ctx.Time = currentTimeFn()
|
||||
ctx.handlePresignRemoval()
|
||||
}
|
||||
|
||||
var err error
|
||||
ctx.credValues, err = v4.Credentials.GetWithContext(requestContext(r))
|
||||
if err != nil {
|
||||
return http.Header{}, err
|
||||
}
|
||||
|
||||
ctx.sanitizeHostForHeader()
|
||||
ctx.assignAmzQueryValues()
|
||||
if err := ctx.build(v4.DisableHeaderHoisting); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If the request is not presigned the body should be attached to it. This
|
||||
// prevents the confusion of wanting to send a signed request without
|
||||
// the body the request was signed for attached.
|
||||
if !(v4.DisableRequestBodyOverwrite || ctx.isPresign) {
|
||||
var reader io.ReadCloser
|
||||
if body != nil {
|
||||
var ok bool
|
||||
if reader, ok = body.(io.ReadCloser); !ok {
|
||||
reader = io.NopCloser(body)
|
||||
}
|
||||
}
|
||||
r.Body = reader
|
||||
}
|
||||
|
||||
if v4.Debug.Matches(aws.LogDebugWithSigning) {
|
||||
v4.logSigningInfo(ctx)
|
||||
}
|
||||
|
||||
return ctx.SignedHeaderVals, nil
|
||||
}
|
||||
|
||||
func (ctx *signingCtx) sanitizeHostForHeader() {
|
||||
request.SanitizeHostForHeader(ctx.Request)
|
||||
}
|
||||
|
||||
func (ctx *signingCtx) handlePresignRemoval() {
|
||||
if !ctx.isPresign {
|
||||
return
|
||||
}
|
||||
|
||||
// The credentials have expired for this request. The current signing
|
||||
// is invalid, and needs to be request because the request will fail.
|
||||
ctx.removePresign()
|
||||
|
||||
// Update the request's query string to ensure the values stays in
|
||||
// sync in the case retrieving the new credentials fails.
|
||||
ctx.Request.URL.RawQuery = ctx.Query.Encode()
|
||||
}
|
||||
|
||||
func (ctx *signingCtx) assignAmzQueryValues() {
|
||||
if ctx.isPresign {
|
||||
ctx.Query.Set("X-Amz-Algorithm", authHeaderPrefix)
|
||||
if ctx.credValues.SessionToken != "" {
|
||||
ctx.Query.Set("X-Amz-Security-Token", ctx.credValues.SessionToken)
|
||||
} else {
|
||||
ctx.Query.Del("X-Amz-Security-Token")
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if ctx.credValues.SessionToken != "" {
|
||||
ctx.Request.Header.Set("X-Amz-Security-Token", ctx.credValues.SessionToken)
|
||||
}
|
||||
}
|
||||
|
||||
// SignRequestHandler is a named request handler the SDK will use to sign
|
||||
// service client request with using the V4 signature.
|
||||
var SignRequestHandler = request.NamedHandler{
|
||||
Name: "v4.SignRequestHandler", Fn: SignSDKRequest,
|
||||
}
|
||||
|
||||
// SignSDKRequest signs an AWS request with the V4 signature. This
|
||||
// request handler should only be used with the SDK's built in service client's
|
||||
// API operation requests.
|
||||
//
|
||||
// This function should not be used on its on its own, but in conjunction with
|
||||
// an AWS service client's API operation call. To sign a standalone request
|
||||
// not created by a service client's API operation method use the "Sign" or
|
||||
// "Presign" functions of the "Signer" type.
|
||||
//
|
||||
// If the credentials of the request's config are set to
|
||||
// credentials.AnonymousCredentials the request will not be signed.
|
||||
func SignSDKRequest(req *request.Request) {
|
||||
SignSDKRequestWithCurrentTime(req, time.Now)
|
||||
}
|
||||
|
||||
// BuildNamedHandler will build a generic handler for signing.
|
||||
func BuildNamedHandler(name string, opts ...func(*Signer)) request.NamedHandler {
|
||||
return request.NamedHandler{
|
||||
Name: name,
|
||||
Fn: func(req *request.Request) {
|
||||
SignSDKRequestWithCurrentTime(req, time.Now, opts...)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SignSDKRequestWithCurrentTime will sign the SDK's request using the time
|
||||
// function passed in. Behaves the same as SignSDKRequest with the exception
|
||||
// the request is signed with the value returned by the current time function.
|
||||
func SignSDKRequestWithCurrentTime(req *request.Request, curTimeFn func() time.Time, opts ...func(*Signer)) {
|
||||
// If the request does not need to be signed ignore the signing of the
|
||||
// request if the AnonymousCredentials object is used.
|
||||
if req.Config.Credentials == credentials.AnonymousCredentials {
|
||||
return
|
||||
}
|
||||
|
||||
region := req.ClientInfo.SigningRegion
|
||||
if region == "" {
|
||||
region = aws.StringValue(req.Config.Region)
|
||||
}
|
||||
|
||||
name := req.ClientInfo.SigningName
|
||||
if name == "" {
|
||||
name = req.ClientInfo.ServiceName
|
||||
}
|
||||
|
||||
v4 := NewSigner(req.Config.Credentials, func(v4 *Signer) {
|
||||
v4.Debug = req.Config.LogLevel.Value()
|
||||
v4.Logger = req.Config.Logger
|
||||
v4.DisableHeaderHoisting = req.NotHoist
|
||||
v4.currentTimeFn = curTimeFn
|
||||
if name == "s3" {
|
||||
// S3 service should not have any escaping applied
|
||||
v4.DisableURIPathEscaping = true
|
||||
}
|
||||
// Prevents setting the HTTPRequest's Body. Since the Body could be
|
||||
// wrapped in a custom io.Closer that we do not want to be stompped
|
||||
// on top of by the signer.
|
||||
v4.DisableRequestBodyOverwrite = true
|
||||
})
|
||||
|
||||
for _, opt := range opts {
|
||||
opt(v4)
|
||||
}
|
||||
|
||||
curTime := curTimeFn()
|
||||
signedHeaders, err := v4.signWithBody(req.HTTPRequest, req.GetBody(),
|
||||
name, region, req.ExpireTime, req.ExpireTime > 0, curTime,
|
||||
)
|
||||
if err != nil {
|
||||
req.Error = err
|
||||
req.SignedHeaderVals = nil
|
||||
return
|
||||
}
|
||||
|
||||
req.SignedHeaderVals = signedHeaders
|
||||
req.LastSignedAt = curTime
|
||||
}
|
||||
|
||||
const logSignInfoMsg = `DEBUG: Request Signature:
|
||||
---[ CANONICAL STRING ]-----------------------------
|
||||
%s
|
||||
---[ STRING TO SIGN ]--------------------------------
|
||||
%s%s
|
||||
-----------------------------------------------------`
|
||||
const logSignedURLMsg = `
|
||||
---[ SIGNED URL ]------------------------------------
|
||||
%s`
|
||||
|
||||
func (v4 *Signer) logSigningInfo(ctx *signingCtx) {
|
||||
signedURLMsg := ""
|
||||
if ctx.isPresign {
|
||||
signedURLMsg = fmt.Sprintf(logSignedURLMsg, ctx.Request.URL.String())
|
||||
}
|
||||
msg := fmt.Sprintf(logSignInfoMsg, ctx.canonicalString, ctx.stringToSign, signedURLMsg)
|
||||
v4.Logger.Log(msg)
|
||||
}
|
||||
|
||||
func (ctx *signingCtx) build(disableHeaderHoisting bool) error {
|
||||
ctx.buildTime() // no depends
|
||||
ctx.buildCredentialString() // no depends
|
||||
|
||||
if err := ctx.buildBodyDigest(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
unsignedHeaders := ctx.Request.Header
|
||||
if ctx.isPresign {
|
||||
if !disableHeaderHoisting {
|
||||
var urlValues url.Values
|
||||
urlValues, unsignedHeaders = buildQuery(allowedQueryHoisting, unsignedHeaders) // no depends
|
||||
for k := range urlValues {
|
||||
ctx.Query[k] = urlValues[k]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.isPresign {
|
||||
ctx.buildCanonicalHeaders(ignoredPresignHeaders, unsignedHeaders)
|
||||
} else {
|
||||
ctx.buildCanonicalHeaders(ignoredHeaders, unsignedHeaders)
|
||||
}
|
||||
ctx.buildCanonicalString() // depends on canon headers / signed headers
|
||||
ctx.buildStringToSign() // depends on canon string
|
||||
ctx.buildSignature() // depends on string to sign
|
||||
|
||||
if ctx.isPresign {
|
||||
ctx.Request.URL.RawQuery += "&" + signatureQueryKey + "=" + ctx.signature
|
||||
} else {
|
||||
parts := []string{
|
||||
authHeaderPrefix + " Credential=" + ctx.credValues.AccessKeyID + "/" + ctx.credentialString,
|
||||
"SignedHeaders=" + ctx.signedHeaders,
|
||||
authHeaderSignatureElem + ctx.signature,
|
||||
}
|
||||
ctx.Request.Header.Set(authorizationHeader, strings.Join(parts, ", "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetSignedRequestSignature attempts to extract the signature of the request.
|
||||
// Returning an error if the request is unsigned, or unable to extract the
|
||||
// signature.
|
||||
func GetSignedRequestSignature(r *http.Request) ([]byte, error) {
|
||||
if auth := r.Header.Get(authorizationHeader); len(auth) != 0 {
|
||||
ps := strings.Split(auth, ", ")
|
||||
for _, p := range ps {
|
||||
if idx := strings.Index(p, authHeaderSignatureElem); idx >= 0 {
|
||||
sig := p[len(authHeaderSignatureElem):]
|
||||
if len(sig) == 0 {
|
||||
return nil, fmt.Errorf("invalid request signature authorization header")
|
||||
}
|
||||
return hex.DecodeString(sig)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if sig := r.URL.Query().Get("X-Amz-Signature"); len(sig) != 0 {
|
||||
return hex.DecodeString(sig)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("request not signed")
|
||||
}
|
||||
|
||||
func (ctx *signingCtx) buildTime() {
|
||||
if ctx.isPresign {
|
||||
duration := int64(ctx.ExpireTime / time.Second)
|
||||
ctx.Query.Set("X-Amz-Date", formatTime(ctx.Time))
|
||||
ctx.Query.Set("X-Amz-Expires", strconv.FormatInt(duration, 10))
|
||||
} else {
|
||||
ctx.Request.Header.Set("X-Amz-Date", formatTime(ctx.Time))
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *signingCtx) buildCredentialString() {
|
||||
ctx.credentialString = buildSigningScope(ctx.Region, ctx.ServiceName, ctx.Time)
|
||||
|
||||
if ctx.isPresign {
|
||||
ctx.Query.Set("X-Amz-Credential", ctx.credValues.AccessKeyID+"/"+ctx.credentialString)
|
||||
}
|
||||
}
|
||||
|
||||
func buildQuery(r rule, header http.Header) (url.Values, http.Header) {
|
||||
query := url.Values{}
|
||||
unsignedHeaders := http.Header{}
|
||||
for k, h := range header {
|
||||
if r.IsValid(k) {
|
||||
query[k] = h
|
||||
} else {
|
||||
unsignedHeaders[k] = h
|
||||
}
|
||||
}
|
||||
|
||||
return query, unsignedHeaders
|
||||
}
|
||||
func (ctx *signingCtx) buildCanonicalHeaders(r rule, header http.Header) {
|
||||
var headers []string
|
||||
headers = append(headers, "host")
|
||||
for k, v := range header {
|
||||
if !r.IsValid(k) {
|
||||
continue // ignored header
|
||||
}
|
||||
if ctx.SignedHeaderVals == nil {
|
||||
ctx.SignedHeaderVals = make(http.Header)
|
||||
}
|
||||
|
||||
lowerCaseKey := strings.ToLower(k)
|
||||
if _, ok := ctx.SignedHeaderVals[lowerCaseKey]; ok {
|
||||
// include additional values
|
||||
ctx.SignedHeaderVals[lowerCaseKey] = append(ctx.SignedHeaderVals[lowerCaseKey], v...)
|
||||
continue
|
||||
}
|
||||
|
||||
headers = append(headers, lowerCaseKey)
|
||||
ctx.SignedHeaderVals[lowerCaseKey] = v
|
||||
}
|
||||
sort.Strings(headers)
|
||||
|
||||
ctx.signedHeaders = strings.Join(headers, ";")
|
||||
|
||||
if ctx.isPresign {
|
||||
ctx.Query.Set("X-Amz-SignedHeaders", ctx.signedHeaders)
|
||||
}
|
||||
|
||||
headerValues := make([]string, len(headers))
|
||||
for i, k := range headers {
|
||||
if k == "host" {
|
||||
if ctx.Request.Host != "" {
|
||||
headerValues[i] = "host:" + ctx.Request.Host
|
||||
} else {
|
||||
headerValues[i] = "host:" + ctx.Request.URL.Host
|
||||
}
|
||||
} else {
|
||||
headerValues[i] = k + ":" +
|
||||
strings.Join(ctx.SignedHeaderVals[k], ",")
|
||||
}
|
||||
}
|
||||
stripExcessSpaces(headerValues)
|
||||
ctx.canonicalHeaders = strings.Join(headerValues, "\n")
|
||||
}
|
||||
|
||||
func (ctx *signingCtx) buildCanonicalString() {
|
||||
ctx.Request.URL.RawQuery = strings.Replace(ctx.Query.Encode(), "+", "%20", -1)
|
||||
|
||||
uri := getURIPath(ctx.Request.URL)
|
||||
|
||||
if !ctx.DisableURIPathEscaping {
|
||||
uri = rest.EscapePath(uri, false)
|
||||
}
|
||||
|
||||
ctx.canonicalString = strings.Join([]string{
|
||||
ctx.Request.Method,
|
||||
uri,
|
||||
ctx.Request.URL.RawQuery,
|
||||
ctx.canonicalHeaders + "\n",
|
||||
ctx.signedHeaders,
|
||||
ctx.bodyDigest,
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func (ctx *signingCtx) buildStringToSign() {
|
||||
ctx.stringToSign = strings.Join([]string{
|
||||
authHeaderPrefix,
|
||||
formatTime(ctx.Time),
|
||||
ctx.credentialString,
|
||||
hex.EncodeToString(hashSHA256([]byte(ctx.canonicalString))),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func (ctx *signingCtx) buildSignature() {
|
||||
creds := deriveSigningKey(ctx.Region, ctx.ServiceName, ctx.credValues.SecretAccessKey, ctx.Time)
|
||||
signature := hmacSHA256(creds, []byte(ctx.stringToSign))
|
||||
ctx.signature = hex.EncodeToString(signature)
|
||||
}
|
||||
|
||||
func (ctx *signingCtx) buildBodyDigest() error {
|
||||
hash := ctx.Request.Header.Get("X-Amz-Content-Sha256")
|
||||
if hash == "" {
|
||||
includeSHA256Header := ctx.unsignedPayload ||
|
||||
ctx.ServiceName == "s3" ||
|
||||
ctx.ServiceName == "glacier"
|
||||
|
||||
s3Presign := ctx.isPresign && ctx.ServiceName == "s3"
|
||||
|
||||
if ctx.unsignedPayload || s3Presign {
|
||||
hash = "UNSIGNED-PAYLOAD"
|
||||
includeSHA256Header = !s3Presign
|
||||
} else if ctx.Body == nil {
|
||||
hash = emptyStringSHA256
|
||||
} else {
|
||||
if !aws.IsReaderSeekable(ctx.Body) {
|
||||
return fmt.Errorf("cannot use unseekable request body %T, for signed request with body", ctx.Body)
|
||||
}
|
||||
hashBytes, err := makeSha256Reader(ctx.Body)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash = hex.EncodeToString(hashBytes)
|
||||
}
|
||||
|
||||
if includeSHA256Header {
|
||||
ctx.Request.Header.Set("X-Amz-Content-Sha256", hash)
|
||||
}
|
||||
}
|
||||
ctx.bodyDigest = hash
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isRequestSigned returns if the request is currently signed or presigned.
|
||||
func (ctx *signingCtx) isRequestSigned() bool {
|
||||
if ctx.isPresign && ctx.Query.Get("X-Amz-Signature") != "" {
|
||||
return true
|
||||
}
|
||||
if ctx.Request.Header.Get("Authorization") != "" {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// unsign removes signing flags for both signed and presigned requests.
|
||||
func (ctx *signingCtx) removePresign() {
|
||||
ctx.Query.Del("X-Amz-Algorithm")
|
||||
ctx.Query.Del("X-Amz-Signature")
|
||||
ctx.Query.Del("X-Amz-Security-Token")
|
||||
ctx.Query.Del("X-Amz-Date")
|
||||
ctx.Query.Del("X-Amz-Expires")
|
||||
ctx.Query.Del("X-Amz-Credential")
|
||||
ctx.Query.Del("X-Amz-SignedHeaders")
|
||||
}
|
||||
|
||||
func hmacSHA256(key []byte, data []byte) []byte {
|
||||
hash := hmac.New(sha256.New, key)
|
||||
hash.Write(data)
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
|
||||
func hashSHA256(data []byte) []byte {
|
||||
hash := sha256.New()
|
||||
hash.Write(data)
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
|
||||
func makeSha256Reader(reader io.ReadSeeker) (hashBytes []byte, err error) {
|
||||
hash := sha256.New()
|
||||
start, err := reader.Seek(0, io.SeekCurrent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer func() {
|
||||
// ensure error is return if unable to seek back to start of payload.
|
||||
_, err = reader.Seek(start, io.SeekStart)
|
||||
}()
|
||||
|
||||
// Use CopyN to avoid allocating the 32KB buffer in io.Copy for bodies
|
||||
// smaller than 32KB. Fall back to io.Copy if we fail to determine the size.
|
||||
size, err := aws.SeekerLen(reader)
|
||||
if err != nil {
|
||||
_, _ = io.Copy(hash, reader)
|
||||
} else {
|
||||
_, _ = io.CopyN(hash, reader, size)
|
||||
}
|
||||
|
||||
return hash.Sum(nil), nil
|
||||
}
|
||||
|
||||
const doubleSpace = " "
|
||||
|
||||
// stripExcessSpaces will rewrite the passed in slice's string values to not
|
||||
// contain multiple side-by-side spaces.
|
||||
//
|
||||
//nolint:revive
|
||||
func stripExcessSpaces(vals []string) {
|
||||
var j, k, l, m, spaces int
|
||||
for i, str := range vals {
|
||||
// Trim trailing spaces
|
||||
for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- {
|
||||
}
|
||||
|
||||
// Trim leading spaces
|
||||
for k = 0; k < j && str[k] == ' '; k++ {
|
||||
}
|
||||
str = str[k : j+1]
|
||||
|
||||
// Strip multiple spaces.
|
||||
j = strings.Index(str, doubleSpace)
|
||||
if j < 0 {
|
||||
vals[i] = str
|
||||
continue
|
||||
}
|
||||
|
||||
buf := []byte(str)
|
||||
for k, m, l = j, j, len(buf); k < l; k++ {
|
||||
if buf[k] == ' ' {
|
||||
if spaces == 0 {
|
||||
// First space.
|
||||
buf[m] = buf[k]
|
||||
m++
|
||||
}
|
||||
spaces++
|
||||
} else {
|
||||
// End of multiple spaces.
|
||||
spaces = 0
|
||||
buf[m] = buf[k]
|
||||
m++
|
||||
}
|
||||
}
|
||||
|
||||
vals[i] = string(buf[:m])
|
||||
}
|
||||
}
|
||||
|
||||
func buildSigningScope(region, service string, dt time.Time) string {
|
||||
return strings.Join([]string{
|
||||
formatShortTime(dt),
|
||||
region,
|
||||
service,
|
||||
awsV4Request,
|
||||
}, "/")
|
||||
}
|
||||
|
||||
func deriveSigningKey(region, service, secretKey string, dt time.Time) []byte {
|
||||
hmacDate := hmacSHA256([]byte("AWS4"+secretKey), []byte(formatShortTime(dt)))
|
||||
hmacRegion := hmacSHA256(hmacDate, []byte(region))
|
||||
hmacService := hmacSHA256(hmacRegion, []byte(service))
|
||||
signingKey := hmacSHA256(hmacService, []byte(awsV4Request))
|
||||
return signingKey
|
||||
}
|
||||
|
||||
func formatShortTime(dt time.Time) string {
|
||||
return dt.UTC().Format(shortTimeFormat)
|
||||
}
|
||||
|
||||
func formatTime(dt time.Time) string {
|
||||
return dt.UTC().Format(timeFormat)
|
||||
}
|
144
api/auth/signer/v4asdk2/credentials.go
Normal file
144
api/auth/signer/v4asdk2/credentials.go
Normal file
|
@ -0,0 +1,144 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/credentials.go
|
||||
// with changes:
|
||||
// * use `time.Now()` instead of `sdk.NowTime()`
|
||||
|
||||
package v4a
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
)
|
||||
|
||||
// Credentials is Context, ECDSA, and Optional Session Token that can be used
|
||||
// to sign requests using SigV4a
|
||||
type Credentials struct {
|
||||
Context string
|
||||
PrivateKey *ecdsa.PrivateKey
|
||||
SessionToken string
|
||||
|
||||
// Time the credentials will expire.
|
||||
CanExpire bool
|
||||
Expires time.Time
|
||||
}
|
||||
|
||||
// Expired returns if the credentials have expired.
|
||||
func (v Credentials) Expired() bool {
|
||||
if v.CanExpire {
|
||||
return !v.Expires.After(time.Now())
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// HasKeys returns if the credentials keys are set.
|
||||
func (v Credentials) HasKeys() bool {
|
||||
return len(v.Context) > 0 && v.PrivateKey != nil
|
||||
}
|
||||
|
||||
// SymmetricCredentialAdaptor wraps a SigV4 AccessKey/SecretKey provider and adapts the credentials
|
||||
// to a ECDSA PrivateKey for signing with SiV4a
|
||||
type SymmetricCredentialAdaptor struct {
|
||||
SymmetricProvider aws.CredentialsProvider
|
||||
|
||||
asymmetric atomic.Value
|
||||
m sync.Mutex
|
||||
}
|
||||
|
||||
// Retrieve retrieves symmetric credentials from the underlying provider.
|
||||
func (s *SymmetricCredentialAdaptor) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||
symCreds, err := s.retrieveFromSymmetricProvider(ctx)
|
||||
if err != nil {
|
||||
return aws.Credentials{}, err
|
||||
}
|
||||
|
||||
if asymCreds := s.getCreds(); asymCreds == nil {
|
||||
return symCreds, nil
|
||||
}
|
||||
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
|
||||
asymCreds := s.getCreds()
|
||||
if asymCreds == nil {
|
||||
return symCreds, nil
|
||||
}
|
||||
|
||||
// if the context does not match the access key id clear it
|
||||
if asymCreds.Context != symCreds.AccessKeyID {
|
||||
s.asymmetric.Store((*Credentials)(nil))
|
||||
}
|
||||
|
||||
return symCreds, nil
|
||||
}
|
||||
|
||||
// RetrievePrivateKey returns credentials suitable for SigV4a signing
|
||||
func (s *SymmetricCredentialAdaptor) RetrievePrivateKey(ctx context.Context) (Credentials, error) {
|
||||
if asymCreds := s.getCreds(); asymCreds != nil {
|
||||
return *asymCreds, nil
|
||||
}
|
||||
|
||||
s.m.Lock()
|
||||
defer s.m.Unlock()
|
||||
|
||||
if asymCreds := s.getCreds(); asymCreds != nil {
|
||||
return *asymCreds, nil
|
||||
}
|
||||
|
||||
symmetricCreds, err := s.retrieveFromSymmetricProvider(ctx)
|
||||
if err != nil {
|
||||
return Credentials{}, fmt.Errorf("failed to retrieve symmetric credentials: %v", err)
|
||||
}
|
||||
|
||||
privateKey, err := deriveKeyFromAccessKeyPair(symmetricCreds.AccessKeyID, symmetricCreds.SecretAccessKey)
|
||||
if err != nil {
|
||||
return Credentials{}, fmt.Errorf("failed to derive assymetric key from credentials")
|
||||
}
|
||||
|
||||
creds := Credentials{
|
||||
Context: symmetricCreds.AccessKeyID,
|
||||
PrivateKey: privateKey,
|
||||
SessionToken: symmetricCreds.SessionToken,
|
||||
CanExpire: symmetricCreds.CanExpire,
|
||||
Expires: symmetricCreds.Expires,
|
||||
}
|
||||
|
||||
s.asymmetric.Store(&creds)
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
func (s *SymmetricCredentialAdaptor) getCreds() *Credentials {
|
||||
v := s.asymmetric.Load()
|
||||
|
||||
if v == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
c := v.(*Credentials)
|
||||
if c != nil && c.HasKeys() && !c.Expired() {
|
||||
return c
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *SymmetricCredentialAdaptor) retrieveFromSymmetricProvider(ctx context.Context) (aws.Credentials, error) {
|
||||
credentials, err := s.SymmetricProvider.Retrieve(ctx)
|
||||
if err != nil {
|
||||
return aws.Credentials{}, err
|
||||
}
|
||||
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
// CredentialsProvider is the interface for a provider to retrieve credentials
|
||||
// to sign requests with.
|
||||
type CredentialsProvider interface {
|
||||
RetrievePrivateKey(context.Context) (Credentials, error)
|
||||
}
|
79
api/auth/signer/v4asdk2/credentials_test.go
Normal file
79
api/auth/signer/v4asdk2/credentials_test.go
Normal file
|
@ -0,0 +1,79 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/credentials_test.go
|
||||
|
||||
package v4a
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
)
|
||||
|
||||
type rotatingCredsProvider struct {
|
||||
count int
|
||||
fail chan struct{}
|
||||
}
|
||||
|
||||
func (r *rotatingCredsProvider) Retrieve(ctx context.Context) (aws.Credentials, error) {
|
||||
select {
|
||||
case <-r.fail:
|
||||
return aws.Credentials{}, fmt.Errorf("rotatingCredsProvider error")
|
||||
default:
|
||||
}
|
||||
credentials := aws.Credentials{
|
||||
AccessKeyID: fmt.Sprintf("ACCESS_KEY_ID_%d", r.count),
|
||||
SecretAccessKey: fmt.Sprintf("SECRET_ACCESS_KEY_%d", r.count),
|
||||
SessionToken: fmt.Sprintf("SESSION_TOKEN_%d", r.count),
|
||||
}
|
||||
return credentials, nil
|
||||
}
|
||||
|
||||
func TestSymmetricCredentialAdaptor(t *testing.T) {
|
||||
provider := &rotatingCredsProvider{
|
||||
count: 0,
|
||||
fail: make(chan struct{}),
|
||||
}
|
||||
|
||||
adaptor := &SymmetricCredentialAdaptor{SymmetricProvider: provider}
|
||||
|
||||
if symCreds, err := adaptor.Retrieve(context.Background()); err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
} else if !symCreds.HasKeys() {
|
||||
t.Fatalf("expect symmetric credentials to have keys")
|
||||
}
|
||||
|
||||
if load := adaptor.asymmetric.Load(); load != nil {
|
||||
t.Errorf("expect asymmetric credentials to be nil")
|
||||
}
|
||||
|
||||
if asymCreds, err := adaptor.RetrievePrivateKey(context.Background()); err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
} else if !asymCreds.HasKeys() {
|
||||
t.Fatalf("expect asymmetric credentials to have keys")
|
||||
}
|
||||
|
||||
if _, err := adaptor.Retrieve(context.Background()); err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
if load := adaptor.asymmetric.Load(); load.(*Credentials) == nil {
|
||||
t.Errorf("expect asymmetric credentials to be not nil")
|
||||
}
|
||||
|
||||
provider.count++
|
||||
|
||||
if _, err := adaptor.Retrieve(context.Background()); err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
if load := adaptor.asymmetric.Load(); load.(*Credentials) != nil {
|
||||
t.Errorf("expect asymmetric credentials to be nil")
|
||||
}
|
||||
|
||||
close(provider.fail) // All requests to the original provider will now fail from this point-on.
|
||||
_, err := adaptor.Retrieve(context.Background())
|
||||
if err == nil {
|
||||
t.Error("expect error, got nil")
|
||||
}
|
||||
}
|
32
api/auth/signer/v4asdk2/internal/crypto/compare.go
Normal file
32
api/auth/signer/v4asdk2/internal/crypto/compare.go
Normal file
|
@ -0,0 +1,32 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/compare.go
|
||||
|
||||
package crypto
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ConstantTimeByteCompare is a constant-time byte comparison of x and y. This function performs an absolute comparison
|
||||
// if the two byte slices assuming they represent a big-endian number.
|
||||
//
|
||||
// error if len(x) != len(y)
|
||||
// -1 if x < y
|
||||
// 0 if x == y
|
||||
// +1 if x > y
|
||||
func ConstantTimeByteCompare(x, y []byte) (int, error) {
|
||||
if len(x) != len(y) {
|
||||
return 0, fmt.Errorf("slice lengths do not match")
|
||||
}
|
||||
|
||||
xLarger, yLarger := 0, 0
|
||||
|
||||
for i := 0; i < len(x); i++ {
|
||||
xByte, yByte := int(x[i]), int(y[i])
|
||||
|
||||
x := ((yByte - xByte) >> 8) & 1
|
||||
y := ((xByte - yByte) >> 8) & 1
|
||||
|
||||
xLarger |= x &^ yLarger
|
||||
yLarger |= y &^ xLarger
|
||||
}
|
||||
|
||||
return xLarger - yLarger, nil
|
||||
}
|
62
api/auth/signer/v4asdk2/internal/crypto/compare_test.go
Normal file
62
api/auth/signer/v4asdk2/internal/crypto/compare_test.go
Normal file
|
@ -0,0 +1,62 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/compare_test.go
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"math/big"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConstantTimeByteCompare(t *testing.T) {
|
||||
cases := []struct {
|
||||
x, y []byte
|
||||
r int
|
||||
expectErr bool
|
||||
}{
|
||||
{x: []byte{}, y: []byte{}, r: 0},
|
||||
{x: []byte{40}, y: []byte{30}, r: 1},
|
||||
{x: []byte{30}, y: []byte{40}, r: -1},
|
||||
{x: []byte{60, 40, 30, 10, 20}, y: []byte{50, 30, 20, 0, 10}, r: 1},
|
||||
{x: []byte{50, 30, 20, 0, 10}, y: []byte{60, 40, 30, 10, 20}, r: -1},
|
||||
{x: nil, y: []byte{}, r: 0},
|
||||
{x: []byte{}, y: nil, r: 0},
|
||||
{x: []byte{}, y: []byte{10}, expectErr: true},
|
||||
{x: []byte{10}, y: []byte{}, expectErr: true},
|
||||
{x: []byte{10, 20}, y: []byte{10}, expectErr: true},
|
||||
}
|
||||
|
||||
for _, tt := range cases {
|
||||
compare, err := ConstantTimeByteCompare(tt.x, tt.y)
|
||||
if (err != nil) != tt.expectErr {
|
||||
t.Fatalf("expectErr=%v, got %v", tt.expectErr, err)
|
||||
}
|
||||
if e, a := tt.r, compare; e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkConstantTimeCompare(b *testing.B) {
|
||||
x, y := big.NewInt(1023), big.NewInt(1024)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ConstantTimeByteCompare(x.Bytes(), y.Bytes())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkCompare(b *testing.B) {
|
||||
x, y := big.NewInt(1023).Bytes(), big.NewInt(1024).Bytes()
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
bytes.Compare(x, y)
|
||||
}
|
||||
}
|
||||
|
||||
func mustBigInt(s string) *big.Int {
|
||||
b, ok := (&big.Int{}).SetString(s, 16)
|
||||
if !ok {
|
||||
panic("can't parse as big.Int")
|
||||
}
|
||||
return b
|
||||
}
|
115
api/auth/signer/v4asdk2/internal/crypto/ecc.go
Normal file
115
api/auth/signer/v4asdk2/internal/crypto/ecc.go
Normal file
|
@ -0,0 +1,115 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/ecc.go
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/hmac"
|
||||
"encoding/asn1"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"hash"
|
||||
"math"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
type ecdsaSignature struct {
|
||||
R, S *big.Int
|
||||
}
|
||||
|
||||
// ECDSAKey takes the given elliptic curve, and private key (d) byte slice
|
||||
// and returns the private ECDSA key.
|
||||
func ECDSAKey(curve elliptic.Curve, d []byte) *ecdsa.PrivateKey {
|
||||
return ECDSAKeyFromPoint(curve, (&big.Int{}).SetBytes(d))
|
||||
}
|
||||
|
||||
// ECDSAKeyFromPoint takes the given elliptic curve and point and returns the
|
||||
// private and public keypair
|
||||
func ECDSAKeyFromPoint(curve elliptic.Curve, d *big.Int) *ecdsa.PrivateKey {
|
||||
pX, pY := curve.ScalarBaseMult(d.Bytes())
|
||||
|
||||
privKey := &ecdsa.PrivateKey{
|
||||
PublicKey: ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: pX,
|
||||
Y: pY,
|
||||
},
|
||||
D: d,
|
||||
}
|
||||
|
||||
return privKey
|
||||
}
|
||||
|
||||
// ECDSAPublicKey takes the provide curve and (x, y) coordinates and returns
|
||||
// *ecdsa.PublicKey. Returns an error if the given points are not on the curve.
|
||||
func ECDSAPublicKey(curve elliptic.Curve, x, y []byte) (*ecdsa.PublicKey, error) {
|
||||
xPoint := (&big.Int{}).SetBytes(x)
|
||||
yPoint := (&big.Int{}).SetBytes(y)
|
||||
|
||||
if !curve.IsOnCurve(xPoint, yPoint) {
|
||||
return nil, fmt.Errorf("point(%v, %v) is not on the given curve", xPoint.String(), yPoint.String())
|
||||
}
|
||||
|
||||
return &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: xPoint,
|
||||
Y: yPoint,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifySignature takes the provided public key, hash, and asn1 encoded signature and returns
|
||||
// whether the given signature is valid.
|
||||
func VerifySignature(key *ecdsa.PublicKey, hash []byte, signature []byte) (bool, error) {
|
||||
var ecdsaSignature ecdsaSignature
|
||||
|
||||
_, err := asn1.Unmarshal(signature, &ecdsaSignature)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
return ecdsa.Verify(key, hash, ecdsaSignature.R, ecdsaSignature.S), nil
|
||||
}
|
||||
|
||||
// HMACKeyDerivation provides an implementation of a NIST-800-108 of a KDF (Key Derivation Function) in Counter Mode.
|
||||
// For the purposes of this implantation HMAC is used as the PRF (Pseudorandom function), where the value of
|
||||
// `r` is defined as a 4 byte counter.
|
||||
func HMACKeyDerivation(hash func() hash.Hash, bitLen int, key []byte, label, context []byte) ([]byte, error) {
|
||||
// verify that we won't overflow the counter
|
||||
n := int64(math.Ceil((float64(bitLen) / 8) / float64(hash().Size())))
|
||||
if n > 0x7FFFFFFF {
|
||||
return nil, fmt.Errorf("unable to derive key of size %d using 32-bit counter", bitLen)
|
||||
}
|
||||
|
||||
// verify the requested bit length is not larger then the length encoding size
|
||||
if int64(bitLen) > 0x7FFFFFFF {
|
||||
return nil, fmt.Errorf("bitLen is greater than 32-bits")
|
||||
}
|
||||
|
||||
fixedInput := bytes.NewBuffer(nil)
|
||||
fixedInput.Write(label)
|
||||
fixedInput.WriteByte(0x00)
|
||||
fixedInput.Write(context)
|
||||
if err := binary.Write(fixedInput, binary.BigEndian, int32(bitLen)); err != nil {
|
||||
return nil, fmt.Errorf("failed to write bit length to fixed input string: %v", err)
|
||||
}
|
||||
|
||||
var output []byte
|
||||
|
||||
h := hmac.New(hash, key)
|
||||
|
||||
for i := int64(1); i <= n; i++ {
|
||||
h.Reset()
|
||||
if err := binary.Write(h, binary.BigEndian, int32(i)); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
_, err := h.Write(fixedInput.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output = append(output, h.Sum(nil)...)
|
||||
}
|
||||
|
||||
return output[:bitLen/8], nil
|
||||
}
|
279
api/auth/signer/v4asdk2/internal/crypto/ecc_test.go
Normal file
279
api/auth/signer/v4asdk2/internal/crypto/ecc_test.go
Normal file
|
@ -0,0 +1,279 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/crypto/ecc_test.go
|
||||
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestECDSAPublicKeyDerivation_P256(t *testing.T) {
|
||||
d := []byte{
|
||||
0xc9, 0x80, 0x68, 0x98, 0xa0, 0x33, 0x49, 0x16, 0xc8, 0x60, 0x74, 0x88, 0x80, 0xa5, 0x41, 0xf0,
|
||||
0x93, 0xb5, 0x79, 0xa9, 0xb1, 0xf3, 0x29, 0x34, 0xd8, 0x6c, 0x36, 0x3c, 0x39, 0x80, 0x03, 0x57,
|
||||
}
|
||||
|
||||
x := []byte{
|
||||
0xd0, 0x72, 0x0d, 0xc6, 0x91, 0xaa, 0x80, 0x09, 0x6b, 0xa3, 0x2f, 0xed, 0x1c, 0xb9, 0x7c, 0x2b,
|
||||
0x62, 0x06, 0x90, 0xd0, 0x6d, 0xe0, 0x31, 0x7b, 0x86, 0x18, 0xd5, 0xce, 0x65, 0xeb, 0x72, 0x8f,
|
||||
}
|
||||
|
||||
y := []byte{
|
||||
0x96, 0x81, 0xb5, 0x17, 0xb1, 0xcd, 0xa1, 0x7d, 0x0d, 0x83, 0xd3, 0x35, 0xd9, 0xc4, 0xa8, 0xa9,
|
||||
0xa9, 0xb0, 0xb1, 0xb3, 0xc7, 0x10, 0x6d, 0x8f, 0x3c, 0x72, 0xbc, 0x50, 0x93, 0xdc, 0x27, 0x5f,
|
||||
}
|
||||
|
||||
testKeyDerivation(t, elliptic.P256(), d, x, y)
|
||||
}
|
||||
|
||||
func TestECDSAPublicKeyDerivation_P384(t *testing.T) {
|
||||
d := []byte{
|
||||
0x53, 0x94, 0xf7, 0x97, 0x3e, 0xa8, 0x68, 0xc5, 0x2b, 0xf3, 0xff, 0x8d, 0x8c, 0xee, 0xb4, 0xdb,
|
||||
0x90, 0xa6, 0x83, 0x65, 0x3b, 0x12, 0x48, 0x5d, 0x5f, 0x62, 0x7c, 0x3c, 0xe5, 0xab, 0xd8, 0x97,
|
||||
0x8f, 0xc9, 0x67, 0x3d, 0x14, 0xa7, 0x1d, 0x92, 0x57, 0x47, 0x93, 0x16, 0x62, 0x49, 0x3c, 0x37,
|
||||
}
|
||||
|
||||
x := []byte{
|
||||
0xfd, 0x3c, 0x84, 0xe5, 0x68, 0x9b, 0xed, 0x27, 0x0e, 0x60, 0x1b, 0x3d, 0x80, 0xf9, 0x0d, 0x67,
|
||||
0xa9, 0xae, 0x45, 0x1c, 0xce, 0x89, 0x0f, 0x53, 0xe5, 0x83, 0x22, 0x9a, 0xd0, 0xe2, 0xee, 0x64,
|
||||
0x56, 0x11, 0xfa, 0x99, 0x36, 0xdf, 0xa4, 0x53, 0x06, 0xec, 0x18, 0x06, 0x67, 0x74, 0xaa, 0x24,
|
||||
}
|
||||
|
||||
y := []byte{
|
||||
0xb8, 0x3c, 0xa4, 0x12, 0x6c, 0xfc, 0x4c, 0x4d, 0x1d, 0x18, 0xa4, 0xb6, 0xc2, 0x1c, 0x7f, 0x69,
|
||||
0x9d, 0x51, 0x23, 0xdd, 0x9c, 0x24, 0xf6, 0x6f, 0x83, 0x38, 0x46, 0xee, 0xb5, 0x82, 0x96, 0x19,
|
||||
0x6b, 0x42, 0xec, 0x06, 0x42, 0x5d, 0xb5, 0xb7, 0x0a, 0x4b, 0x81, 0xb7, 0xfc, 0xf7, 0x05, 0xa0,
|
||||
}
|
||||
|
||||
testKeyDerivation(t, elliptic.P384(), d, x, y)
|
||||
}
|
||||
|
||||
func TestECDSAKnownSigningValue_P256(t *testing.T) {
|
||||
d := []byte{
|
||||
0x51, 0x9b, 0x42, 0x3d, 0x71, 0x5f, 0x8b, 0x58, 0x1f, 0x4f, 0xa8, 0xee, 0x59, 0xf4, 0x77, 0x1a,
|
||||
0x5b, 0x44, 0xc8, 0x13, 0x0b, 0x4e, 0x3e, 0xac, 0xca, 0x54, 0xa5, 0x6d, 0xda, 0x72, 0xb4, 0x64,
|
||||
}
|
||||
|
||||
testKnownSigningValue(t, elliptic.P256(), d)
|
||||
}
|
||||
|
||||
func TestECDSAKnownSigningValue_P384(t *testing.T) {
|
||||
d := []byte{
|
||||
0x53, 0x94, 0xf7, 0x97, 0x3e, 0xa8, 0x68, 0xc5, 0x2b, 0xf3, 0xff, 0x8d, 0x8c, 0xee, 0xb4, 0xdb,
|
||||
0x90, 0xa6, 0x83, 0x65, 0x3b, 0x12, 0x48, 0x5d, 0x5f, 0x62, 0x7c, 0x3c, 0xe5, 0xab, 0xd8, 0x97,
|
||||
0x8f, 0xc9, 0x67, 0x3d, 0x14, 0xa7, 0x1d, 0x92, 0x57, 0x47, 0x93, 0x16, 0x62, 0x49, 0x3c, 0x37,
|
||||
}
|
||||
|
||||
testKnownSigningValue(t, elliptic.P384(), d)
|
||||
}
|
||||
|
||||
func testKeyDerivation(t *testing.T, curve elliptic.Curve, d, expectedX, expectedY []byte) {
|
||||
privKey := ECDSAKey(curve, d)
|
||||
|
||||
if e, a := d, privKey.D.Bytes(); bytes.Compare(e, a) != 0 {
|
||||
t.Errorf("expected % x, got % x", e, a)
|
||||
}
|
||||
|
||||
if e, a := expectedX, privKey.X.Bytes(); bytes.Compare(e, a) != 0 {
|
||||
t.Errorf("expected % x, got % x", e, a)
|
||||
}
|
||||
|
||||
if e, a := expectedY, privKey.Y.Bytes(); bytes.Compare(e, a) != 0 {
|
||||
t.Errorf("expected % x, got % x", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func testKnownSigningValue(t *testing.T, curve elliptic.Curve, d []byte) {
|
||||
signingKey := ECDSAKey(curve, d)
|
||||
|
||||
message := []byte{
|
||||
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
|
||||
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
|
||||
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
|
||||
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
|
||||
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
|
||||
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
|
||||
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
|
||||
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
|
||||
}
|
||||
|
||||
sha256Hash := sha256.New()
|
||||
_, err := io.Copy(sha256Hash, bytes.NewReader(message))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
msgHash := sha256Hash.Sum(nil)
|
||||
msgSignature, err := signingKey.Sign(rand.Reader, msgHash, crypto.SHA256)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
verified, err := VerifySignature(&signingKey.PublicKey, msgHash, msgSignature)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if !verified {
|
||||
t.Fatalf("failed to verify message msgSignature")
|
||||
}
|
||||
}
|
||||
|
||||
func TestECDSAInvalidSignature_P256(t *testing.T) {
|
||||
testInvalidSignature(t, elliptic.P256())
|
||||
}
|
||||
|
||||
func TestECDSAInvalidSignature_P384(t *testing.T) {
|
||||
testInvalidSignature(t, elliptic.P384())
|
||||
}
|
||||
|
||||
func TestECDSAGenKeySignature_P256(t *testing.T) {
|
||||
testGenKeySignature(t, elliptic.P256())
|
||||
}
|
||||
|
||||
func TestECDSAGenKeySignature_P384(t *testing.T) {
|
||||
testGenKeySignature(t, elliptic.P384())
|
||||
}
|
||||
|
||||
func testInvalidSignature(t *testing.T, curve elliptic.Curve) {
|
||||
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
message := []byte{
|
||||
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
|
||||
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
|
||||
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
|
||||
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
|
||||
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
|
||||
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
|
||||
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
|
||||
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
|
||||
}
|
||||
|
||||
sha256Hash := sha256.New()
|
||||
_, err = io.Copy(sha256Hash, bytes.NewReader(message))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
msgHash := sha256Hash.Sum(nil)
|
||||
msgSignature, err := privateKey.Sign(rand.Reader, msgHash, crypto.SHA256)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
byteToFlip := 15
|
||||
switch msgSignature[byteToFlip] {
|
||||
case 0:
|
||||
msgSignature[byteToFlip] = 0x0a
|
||||
default:
|
||||
msgSignature[byteToFlip] &^= msgSignature[byteToFlip]
|
||||
}
|
||||
|
||||
verified, err := VerifySignature(&privateKey.PublicKey, msgHash, msgSignature)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if verified {
|
||||
t.Fatalf("expected message verification to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func testGenKeySignature(t *testing.T, curve elliptic.Curve) {
|
||||
privateKey, err := ecdsa.GenerateKey(curve, rand.Reader)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to generate key: %v", err)
|
||||
}
|
||||
|
||||
message := []byte{
|
||||
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
|
||||
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
|
||||
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
|
||||
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
|
||||
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
|
||||
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
|
||||
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
|
||||
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
|
||||
}
|
||||
|
||||
sha256Hash := sha256.New()
|
||||
_, err = io.Copy(sha256Hash, bytes.NewReader(message))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
msgHash := sha256Hash.Sum(nil)
|
||||
msgSignature, err := privateKey.Sign(rand.Reader, msgHash, crypto.SHA256)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
verified, err := VerifySignature(&privateKey.PublicKey, msgHash, msgSignature)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if !verified {
|
||||
t.Fatalf("expected message verification to fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestECDSASignatureFormat(t *testing.T) {
|
||||
asn1Signature := []byte{
|
||||
0x30, 0x45, 0x02, 0x21, 0x00, 0xd7, 0xc5, 0xb9, 0x9e, 0x0b, 0xb1, 0x1a, 0x1f, 0x32, 0xda, 0x66, 0xe0, 0xff,
|
||||
0x59, 0xb7, 0x8a, 0x5e, 0xb3, 0x94, 0x9c, 0x23, 0xb3, 0xfc, 0x1f, 0x18, 0xcc, 0xf6, 0x61, 0x67, 0x8b, 0xf1,
|
||||
0xc1, 0x02, 0x20, 0x26, 0x4d, 0x8b, 0x7c, 0xaa, 0x52, 0x4c, 0xc0, 0x2e, 0x5f, 0xf6, 0x7e, 0x24, 0x82, 0xe5,
|
||||
0xfb, 0xcb, 0xc7, 0x9b, 0x83, 0x0d, 0x19, 0x7e, 0x7a, 0x40, 0x37, 0x87, 0xdd, 0x1c, 0x93, 0x13, 0xc4,
|
||||
}
|
||||
|
||||
x := []byte{
|
||||
0x1c, 0xcb, 0xe9, 0x1c, 0x07, 0x5f, 0xc7, 0xf4, 0xf0, 0x33, 0xbf, 0xa2, 0x48, 0xdb, 0x8f, 0xcc,
|
||||
0xd3, 0x56, 0x5d, 0xe9, 0x4b, 0xbf, 0xb1, 0x2f, 0x3c, 0x59, 0xff, 0x46, 0xc2, 0x71, 0xbf, 0x83,
|
||||
}
|
||||
|
||||
y := []byte{
|
||||
0xce, 0x40, 0x14, 0xc6, 0x88, 0x11, 0xf9, 0xa2, 0x1a, 0x1f, 0xdb, 0x2c, 0x0e, 0x61, 0x13, 0xe0,
|
||||
0x6d, 0xb7, 0xca, 0x93, 0xb7, 0x40, 0x4e, 0x78, 0xdc, 0x7c, 0xcd, 0x5c, 0xa8, 0x9a, 0x4c, 0xa9,
|
||||
}
|
||||
|
||||
publicKey, err := ECDSAPublicKey(elliptic.P256(), x, y)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
message := []byte{
|
||||
0x59, 0x05, 0x23, 0x88, 0x77, 0xc7, 0x74, 0x21, 0xf7, 0x3e, 0x43, 0xee, 0x3d, 0xa6, 0xf2, 0xd9,
|
||||
0xe2, 0xcc, 0xad, 0x5f, 0xc9, 0x42, 0xdc, 0xec, 0x0c, 0xbd, 0x25, 0x48, 0x29, 0x35, 0xfa, 0xaf,
|
||||
0x41, 0x69, 0x83, 0xfe, 0x16, 0x5b, 0x1a, 0x04, 0x5e, 0xe2, 0xbc, 0xd2, 0xe6, 0xdc, 0xa3, 0xbd,
|
||||
0xf4, 0x6c, 0x43, 0x10, 0xa7, 0x46, 0x1f, 0x9a, 0x37, 0x96, 0x0c, 0xa6, 0x72, 0xd3, 0xfe, 0xb5,
|
||||
0x47, 0x3e, 0x25, 0x36, 0x05, 0xfb, 0x1d, 0xdf, 0xd2, 0x80, 0x65, 0xb5, 0x3c, 0xb5, 0x85, 0x8a,
|
||||
0x8a, 0xd2, 0x81, 0x75, 0xbf, 0x9b, 0xd3, 0x86, 0xa5, 0xe4, 0x71, 0xea, 0x7a, 0x65, 0xc1, 0x7c,
|
||||
0xc9, 0x34, 0xa9, 0xd7, 0x91, 0xe9, 0x14, 0x91, 0xeb, 0x37, 0x54, 0xd0, 0x37, 0x99, 0x79, 0x0f,
|
||||
0xe2, 0xd3, 0x08, 0xd1, 0x61, 0x46, 0xd5, 0xc9, 0xb0, 0xd0, 0xde, 0xbd, 0x97, 0xd7, 0x9c, 0xe8,
|
||||
}
|
||||
|
||||
hash := sha256.New()
|
||||
_, err = io.Copy(hash, bytes.NewReader(message))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
msgHash := hash.Sum(nil)
|
||||
|
||||
verifySignature, err := VerifySignature(publicKey, msgHash, asn1Signature)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if !verifySignature {
|
||||
t.Fatalf("failed to verify signature")
|
||||
}
|
||||
}
|
38
api/auth/signer/v4asdk2/internal/v4/const.go
Normal file
38
api/auth/signer/v4asdk2/internal/v4/const.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/const.go
|
||||
|
||||
package v4
|
||||
|
||||
const (
|
||||
// EmptyStringSHA256 is the hex encoded sha256 value of an empty string
|
||||
EmptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
|
||||
|
||||
// UnsignedPayload indicates that the request payload body is unsigned
|
||||
UnsignedPayload = "UNSIGNED-PAYLOAD"
|
||||
|
||||
// AmzAlgorithmKey indicates the signing algorithm
|
||||
AmzAlgorithmKey = "X-Amz-Algorithm"
|
||||
|
||||
// AmzSecurityTokenKey indicates the security token to be used with temporary credentials
|
||||
AmzSecurityTokenKey = "X-Amz-Security-Token"
|
||||
|
||||
// AmzDateKey is the UTC timestamp for the request in the format YYYYMMDD'T'HHMMSS'Z'
|
||||
AmzDateKey = "X-Amz-Date"
|
||||
|
||||
// AmzCredentialKey is the access key ID and credential scope
|
||||
AmzCredentialKey = "X-Amz-Credential"
|
||||
|
||||
// AmzSignedHeadersKey is the set of headers signed for the request
|
||||
AmzSignedHeadersKey = "X-Amz-SignedHeaders"
|
||||
|
||||
// AmzSignatureKey is the query parameter to store the SigV4 signature
|
||||
AmzSignatureKey = "X-Amz-Signature"
|
||||
|
||||
// TimeFormat is the time format to be used in the X-Amz-Date header or query parameter
|
||||
TimeFormat = "20060102T150405Z"
|
||||
|
||||
// ShortTimeFormat is the shorten time format used in the credential scope
|
||||
ShortTimeFormat = "20060102"
|
||||
|
||||
// ContentSHAKey is the SHA256 of request body
|
||||
ContentSHAKey = "X-Amz-Content-Sha256"
|
||||
)
|
90
api/auth/signer/v4asdk2/internal/v4/header_rules.go
Normal file
90
api/auth/signer/v4asdk2/internal/v4/header_rules.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/header_rules.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Rules houses a set of Rule needed for validation of a
|
||||
// string value
|
||||
type Rules []Rule
|
||||
|
||||
// Rule interface allows for more flexible rules and just simply
|
||||
// checks whether or not a value adheres to that Rule
|
||||
type Rule interface {
|
||||
IsValid(value string) bool
|
||||
}
|
||||
|
||||
// IsValid will iterate through all rules and see if any rules
|
||||
// apply to the value and supports nested rules
|
||||
func (r Rules) IsValid(value string) bool {
|
||||
for _, rule := range r {
|
||||
if rule.IsValid(value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MapRule generic Rule for maps
|
||||
type MapRule map[string]struct{}
|
||||
|
||||
// IsValid for the map Rule satisfies whether it exists in the map
|
||||
func (m MapRule) IsValid(value string) bool {
|
||||
_, ok := m[value]
|
||||
return ok
|
||||
}
|
||||
|
||||
// AllowList is a generic Rule for whitelisting
|
||||
type AllowList struct {
|
||||
Rule
|
||||
}
|
||||
|
||||
// IsValid for AllowList checks if the value is within the AllowList
|
||||
func (w AllowList) IsValid(value string) bool {
|
||||
return w.Rule.IsValid(value)
|
||||
}
|
||||
|
||||
// DenyList is a generic Rule for blacklisting
|
||||
type DenyList struct {
|
||||
Rule
|
||||
}
|
||||
|
||||
// IsValid for AllowList checks if the value is within the AllowList
|
||||
func (b DenyList) IsValid(value string) bool {
|
||||
return !b.Rule.IsValid(value)
|
||||
}
|
||||
|
||||
// Patterns is a list of strings to match against
|
||||
type Patterns []string
|
||||
|
||||
// IsValid for Patterns checks each pattern and returns if a match has
|
||||
// been found
|
||||
func (p Patterns) IsValid(value string) bool {
|
||||
for _, pattern := range p {
|
||||
if HasPrefixFold(value, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InclusiveRules rules allow for rules to depend on one another
|
||||
type InclusiveRules []Rule
|
||||
|
||||
// IsValid will return true if all rules are true
|
||||
func (r InclusiveRules) IsValid(value string) bool {
|
||||
for _, rule := range r {
|
||||
if !rule.IsValid(value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HasPrefixFold tests whether the string s begins with prefix, interpreted as UTF-8 strings,
|
||||
// under Unicode case-folding.
|
||||
func HasPrefixFold(s, prefix string) bool {
|
||||
return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix)
|
||||
}
|
83
api/auth/signer/v4asdk2/internal/v4/headers.go
Normal file
83
api/auth/signer/v4asdk2/internal/v4/headers.go
Normal file
|
@ -0,0 +1,83 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/header.go
|
||||
// with changes:
|
||||
// * drop User-Agent header from ignored
|
||||
|
||||
package v4
|
||||
|
||||
// IgnoredPresignedHeaders is a list of headers that are ignored during signing
|
||||
var IgnoredPresignedHeaders = Rules{
|
||||
DenyList{
|
||||
MapRule{
|
||||
"Authorization": struct{}{},
|
||||
"User-Agent": struct{}{},
|
||||
"X-Amzn-Trace-Id": struct{}{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// IgnoredHeaders is a list of headers that are ignored during signing
|
||||
// drop User-Agent header to be compatible with aws sdk java v1.
|
||||
var IgnoredHeaders = Rules{
|
||||
DenyList{
|
||||
MapRule{
|
||||
"Authorization": struct{}{},
|
||||
//"User-Agent": struct{}{},
|
||||
"X-Amzn-Trace-Id": struct{}{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// RequiredSignedHeaders is a whitelist for Build canonical headers.
|
||||
var RequiredSignedHeaders = Rules{
|
||||
AllowList{
|
||||
MapRule{
|
||||
"Cache-Control": struct{}{},
|
||||
"Content-Disposition": struct{}{},
|
||||
"Content-Encoding": struct{}{},
|
||||
"Content-Language": struct{}{},
|
||||
"Content-Md5": struct{}{},
|
||||
"Content-Type": struct{}{},
|
||||
"Expires": struct{}{},
|
||||
"If-Match": struct{}{},
|
||||
"If-Modified-Since": struct{}{},
|
||||
"If-None-Match": struct{}{},
|
||||
"If-Unmodified-Since": struct{}{},
|
||||
"Range": struct{}{},
|
||||
"X-Amz-Acl": struct{}{},
|
||||
"X-Amz-Copy-Source": struct{}{},
|
||||
"X-Amz-Copy-Source-If-Match": struct{}{},
|
||||
"X-Amz-Copy-Source-If-Modified-Since": struct{}{},
|
||||
"X-Amz-Copy-Source-If-None-Match": struct{}{},
|
||||
"X-Amz-Copy-Source-If-Unmodified-Since": struct{}{},
|
||||
"X-Amz-Copy-Source-Range": struct{}{},
|
||||
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||||
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{},
|
||||
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||||
"X-Amz-Grant-Full-control": struct{}{},
|
||||
"X-Amz-Grant-Read": struct{}{},
|
||||
"X-Amz-Grant-Read-Acp": struct{}{},
|
||||
"X-Amz-Grant-Write": struct{}{},
|
||||
"X-Amz-Grant-Write-Acp": struct{}{},
|
||||
"X-Amz-Metadata-Directive": struct{}{},
|
||||
"X-Amz-Mfa": struct{}{},
|
||||
"X-Amz-Request-Payer": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Customer-Key": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||||
"X-Amz-Storage-Class": struct{}{},
|
||||
"X-Amz-Website-Redirect-Location": struct{}{},
|
||||
"X-Amz-Content-Sha256": struct{}{},
|
||||
"X-Amz-Tagging": struct{}{},
|
||||
},
|
||||
},
|
||||
Patterns{"X-Amz-Meta-"},
|
||||
}
|
||||
|
||||
// AllowedQueryHoisting is a whitelist for Build query headers. The boolean value
|
||||
// represents whether or not it is a pattern.
|
||||
var AllowedQueryHoisting = InclusiveRules{
|
||||
DenyList{RequiredSignedHeaders},
|
||||
Patterns{"X-Amz-"},
|
||||
}
|
15
api/auth/signer/v4asdk2/internal/v4/hmac.go
Normal file
15
api/auth/signer/v4asdk2/internal/v4/hmac.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/hmac.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
)
|
||||
|
||||
// HMACSHA256 computes a HMAC-SHA256 of data given the provided key.
|
||||
func HMACSHA256(key []byte, data []byte) []byte {
|
||||
hash := hmac.New(sha256.New, key)
|
||||
hash.Write(data)
|
||||
return hash.Sum(nil)
|
||||
}
|
77
api/auth/signer/v4asdk2/internal/v4/host.go
Normal file
77
api/auth/signer/v4asdk2/internal/v4/host.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/host.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SanitizeHostForHeader removes default port from host and updates request.Host
|
||||
func SanitizeHostForHeader(r *http.Request) {
|
||||
host := getHost(r)
|
||||
port := portOnly(host)
|
||||
if port != "" && isDefaultPort(r.URL.Scheme, port) {
|
||||
r.Host = stripPort(host)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns host from request
|
||||
func getHost(r *http.Request) string {
|
||||
if r.Host != "" {
|
||||
return r.Host
|
||||
}
|
||||
|
||||
return r.URL.Host
|
||||
}
|
||||
|
||||
// Hostname returns u.Host, without any port number.
|
||||
//
|
||||
// If Host is an IPv6 literal with a port number, Hostname returns the
|
||||
// IPv6 literal without the square brackets. IPv6 literals may include
|
||||
// a zone identifier.
|
||||
//
|
||||
// Copied from the Go 1.8 standard library (net/url)
|
||||
func stripPort(hostport string) string {
|
||||
colon := strings.IndexByte(hostport, ':')
|
||||
if colon == -1 {
|
||||
return hostport
|
||||
}
|
||||
if i := strings.IndexByte(hostport, ']'); i != -1 {
|
||||
return strings.TrimPrefix(hostport[:i], "[")
|
||||
}
|
||||
return hostport[:colon]
|
||||
}
|
||||
|
||||
// Port returns the port part of u.Host, without the leading colon.
|
||||
// If u.Host doesn't contain a port, Port returns an empty string.
|
||||
//
|
||||
// Copied from the Go 1.8 standard library (net/url)
|
||||
func portOnly(hostport string) string {
|
||||
colon := strings.IndexByte(hostport, ':')
|
||||
if colon == -1 {
|
||||
return ""
|
||||
}
|
||||
if i := strings.Index(hostport, "]:"); i != -1 {
|
||||
return hostport[i+len("]:"):]
|
||||
}
|
||||
if strings.Contains(hostport, "]") {
|
||||
return ""
|
||||
}
|
||||
return hostport[colon+len(":"):]
|
||||
}
|
||||
|
||||
// Returns true if the specified URI is using the standard port
|
||||
// (i.e. port 80 for HTTP URIs or 443 for HTTPS URIs)
|
||||
func isDefaultPort(scheme, port string) bool {
|
||||
if port == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
lowerCaseScheme := strings.ToLower(scheme)
|
||||
if (lowerCaseScheme == "http" && port == "80") || (lowerCaseScheme == "https" && port == "443") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
38
api/auth/signer/v4asdk2/internal/v4/time.go
Normal file
38
api/auth/signer/v4asdk2/internal/v4/time.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/time.go
|
||||
|
||||
package v4
|
||||
|
||||
import "time"
|
||||
|
||||
// SigningTime provides a wrapper around a time.Time which provides cached values for SigV4 signing.
|
||||
type SigningTime struct {
|
||||
time.Time
|
||||
timeFormat string
|
||||
shortTimeFormat string
|
||||
}
|
||||
|
||||
// NewSigningTime creates a new SigningTime given a time.Time
|
||||
func NewSigningTime(t time.Time) SigningTime {
|
||||
return SigningTime{
|
||||
Time: t,
|
||||
}
|
||||
}
|
||||
|
||||
// TimeFormat provides a time formatted in the X-Amz-Date format.
|
||||
func (m *SigningTime) TimeFormat() string {
|
||||
return m.format(&m.timeFormat, TimeFormat)
|
||||
}
|
||||
|
||||
// ShortTimeFormat provides a time formatted of 20060102.
|
||||
func (m *SigningTime) ShortTimeFormat() string {
|
||||
return m.format(&m.shortTimeFormat, ShortTimeFormat)
|
||||
}
|
||||
|
||||
func (m *SigningTime) format(target *string, format string) string {
|
||||
if len(*target) > 0 {
|
||||
return *target
|
||||
}
|
||||
v := m.Time.Format(format)
|
||||
*target = v
|
||||
return v
|
||||
}
|
66
api/auth/signer/v4asdk2/internal/v4/util.go
Normal file
66
api/auth/signer/v4asdk2/internal/v4/util.go
Normal file
|
@ -0,0 +1,66 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/util.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const doubleSpace = " "
|
||||
|
||||
// StripExcessSpaces will rewrite the passed in slice's string values to not
|
||||
// contain muliple side-by-side spaces.
|
||||
func StripExcessSpaces(str string) string {
|
||||
var j, k, l, m, spaces int
|
||||
// Trim trailing spaces
|
||||
for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- {
|
||||
}
|
||||
|
||||
// Trim leading spaces
|
||||
for k = 0; k < j && str[k] == ' '; k++ {
|
||||
}
|
||||
str = str[k : j+1]
|
||||
|
||||
// Strip multiple spaces.
|
||||
j = strings.Index(str, doubleSpace)
|
||||
if j < 0 {
|
||||
return str
|
||||
}
|
||||
|
||||
buf := []byte(str)
|
||||
for k, m, l = j, j, len(buf); k < l; k++ {
|
||||
if buf[k] == ' ' {
|
||||
if spaces == 0 {
|
||||
// First space.
|
||||
buf[m] = buf[k]
|
||||
m++
|
||||
}
|
||||
spaces++
|
||||
} else {
|
||||
// End of multiple spaces.
|
||||
spaces = 0
|
||||
buf[m] = buf[k]
|
||||
m++
|
||||
}
|
||||
}
|
||||
|
||||
return string(buf[:m])
|
||||
}
|
||||
|
||||
// GetURIPath returns the escaped URI component from the provided URL
|
||||
func GetURIPath(u *url.URL) string {
|
||||
var uri string
|
||||
|
||||
if len(u.Opaque) > 0 {
|
||||
uri = "/" + strings.Join(strings.Split(u.Opaque, "/")[3:], "/")
|
||||
} else {
|
||||
uri = u.EscapedPath()
|
||||
}
|
||||
|
||||
if len(uri) == 0 {
|
||||
uri = "/"
|
||||
}
|
||||
|
||||
return uri
|
||||
}
|
77
api/auth/signer/v4asdk2/internal/v4/util_test.go
Normal file
77
api/auth/signer/v4asdk2/internal/v4/util_test.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/internal/v4/tuil_test.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStripExcessHeaders(t *testing.T) {
|
||||
vals := []string{
|
||||
"",
|
||||
"123",
|
||||
"1 2 3",
|
||||
"1 2 3 ",
|
||||
" 1 2 3",
|
||||
"1 2 3",
|
||||
"1 23",
|
||||
"1 2 3",
|
||||
"1 2 ",
|
||||
" 1 2 ",
|
||||
"12 3",
|
||||
"12 3 1",
|
||||
"12 3 1",
|
||||
"12 3 1abc123",
|
||||
}
|
||||
|
||||
expected := []string{
|
||||
"",
|
||||
"123",
|
||||
"1 2 3",
|
||||
"1 2 3",
|
||||
"1 2 3",
|
||||
"1 2 3",
|
||||
"1 23",
|
||||
"1 2 3",
|
||||
"1 2",
|
||||
"1 2",
|
||||
"12 3",
|
||||
"12 3 1",
|
||||
"12 3 1",
|
||||
"12 3 1abc123",
|
||||
}
|
||||
|
||||
for i := 0; i < len(vals); i++ {
|
||||
r := StripExcessSpaces(vals[i])
|
||||
if e, a := expected[i], r; e != a {
|
||||
t.Errorf("%d, expect %v, got %v", i, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stripExcessSpaceCases = []string{
|
||||
`AWS4-HMAC-SHA256 Credential=AKIDFAKEIDFAKEID/20160628/us-west-2/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=1234567890abcdef1234567890abcdef1234567890abcdef`,
|
||||
`123 321 123 321`,
|
||||
` 123 321 123 321 `,
|
||||
` 123 321 123 321 `,
|
||||
"123",
|
||||
"1 2 3",
|
||||
" 1 2 3",
|
||||
"1 2 3",
|
||||
"1 23",
|
||||
"1 2 3",
|
||||
"1 2 ",
|
||||
" 1 2 ",
|
||||
"12 3",
|
||||
"12 3 1",
|
||||
"12 3 1",
|
||||
"12 3 1abc123",
|
||||
}
|
||||
|
||||
func BenchmarkStripExcessSpaces(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range stripExcessSpaceCases {
|
||||
StripExcessSpaces(v)
|
||||
}
|
||||
}
|
||||
}
|
133
api/auth/signer/v4asdk2/stream.go
Normal file
133
api/auth/signer/v4asdk2/stream.go
Normal file
|
@ -0,0 +1,133 @@
|
|||
// 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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
signerCrypto "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/crypto"
|
||||
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/v4"
|
||||
)
|
||||
|
||||
// EventStreamSigner is an AWS EventStream protocol signer.
|
||||
type EventStreamSigner interface {
|
||||
GetSignature(ctx context.Context, headers, payload []byte, signingTime time.Time, optFns ...func(*StreamSignerOptions)) ([]byte, error)
|
||||
}
|
||||
|
||||
// StreamSignerOptions is the configuration options for StreamSigner.
|
||||
type StreamSignerOptions struct{}
|
||||
|
||||
// StreamSigner implements Signature Version 4 (SigV4) signing of event stream encoded payloads.
|
||||
type StreamSigner struct {
|
||||
options StreamSignerOptions
|
||||
|
||||
credentials Credentials
|
||||
service string
|
||||
|
||||
prevSignature []byte
|
||||
}
|
||||
|
||||
// NewStreamSigner returns a new AWS EventStream protocol signer.
|
||||
func NewStreamSigner(credentials Credentials, service string, seedSignature []byte, optFns ...func(*StreamSignerOptions)) *StreamSigner {
|
||||
o := StreamSignerOptions{}
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(&o)
|
||||
}
|
||||
|
||||
return &StreamSigner{
|
||||
options: o,
|
||||
credentials: credentials,
|
||||
service: service,
|
||||
prevSignature: seedSignature,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *StreamSigner) VerifySignature(headers, payload []byte, signingTime time.Time, signature []byte, optFns ...func(*StreamSignerOptions)) error {
|
||||
options := s.options
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(&options)
|
||||
}
|
||||
|
||||
prevSignature := s.prevSignature
|
||||
|
||||
st := v4Internal.NewSigningTime(signingTime)
|
||||
|
||||
scope := buildCredentialScope(st, s.service)
|
||||
|
||||
stringToSign := s.buildEventStreamStringToSign(headers, 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) buildEventStreamStringToSign(headers, payload, previousSignature []byte, credentialScope string, signingTime *v4Internal.SigningTime) string {
|
||||
hash := sha256.New()
|
||||
return strings.Join([]string{
|
||||
"AWS4-ECDSA-P256-SHA256-PAYLOAD",
|
||||
signingTime.TimeFormat(),
|
||||
credentialScope,
|
||||
hex.EncodeToString(previousSignature),
|
||||
hex.EncodeToString(makeHash(hash, headers)),
|
||||
hex.EncodeToString(makeHash(hash, payload)),
|
||||
}, "\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 {
|
||||
return strings.Join([]string{
|
||||
st.Format(shortTimeFormat),
|
||||
service,
|
||||
"aws4_request",
|
||||
}, "/")
|
||||
|
||||
}
|
591
api/auth/signer/v4asdk2/v4a.go
Normal file
591
api/auth/signer/v4asdk2/v4a.go
Normal file
|
@ -0,0 +1,591 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/v4a.go
|
||||
// with changes:
|
||||
// * adding exported VerifySignature methods
|
||||
// * using different ignore headers for sing/presign requests
|
||||
// * don't duplicate content-length as signed header
|
||||
// * use copy of smithy-go encoding/httpbinding package
|
||||
// * use zap.Logger instead of smithy-go/logging
|
||||
|
||||
package v4a
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/smithy/encoding/httpbinding"
|
||||
signerCrypto "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/crypto"
|
||||
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/v4"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
// AmzRegionSetKey represents the region set header used for sigv4a
|
||||
AmzRegionSetKey = "X-Amz-Region-Set"
|
||||
amzAlgorithmKey = v4Internal.AmzAlgorithmKey
|
||||
amzSecurityTokenKey = v4Internal.AmzSecurityTokenKey
|
||||
amzDateKey = v4Internal.AmzDateKey
|
||||
amzCredentialKey = v4Internal.AmzCredentialKey
|
||||
amzSignedHeadersKey = v4Internal.AmzSignedHeadersKey
|
||||
authorizationHeader = "Authorization"
|
||||
|
||||
signingAlgorithm = "AWS4-ECDSA-P256-SHA256"
|
||||
|
||||
timeFormat = "20060102T150405Z"
|
||||
shortTimeFormat = "20060102"
|
||||
|
||||
// EmptyStringSHA256 is a hex encoded SHA-256 hash of an empty string
|
||||
EmptyStringSHA256 = v4Internal.EmptyStringSHA256
|
||||
|
||||
// Version of signing v4a
|
||||
Version = "SigV4A"
|
||||
)
|
||||
|
||||
var (
|
||||
p256 elliptic.Curve
|
||||
nMinusTwoP256 *big.Int
|
||||
|
||||
one = new(big.Int).SetInt64(1)
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Ensure the elliptic curve parameters are initialized on package import rather then on first usage
|
||||
p256 = elliptic.P256()
|
||||
|
||||
nMinusTwoP256 = new(big.Int).SetBytes(p256.Params().N.Bytes())
|
||||
nMinusTwoP256 = nMinusTwoP256.Sub(nMinusTwoP256, new(big.Int).SetInt64(2))
|
||||
}
|
||||
|
||||
// SignerOptions is the SigV4a signing options for constructing a Signer.
|
||||
type SignerOptions struct {
|
||||
Logger *zap.Logger
|
||||
LogSigning bool
|
||||
|
||||
// Disables the Signer's moving HTTP header key/value pairs from the HTTP
|
||||
// request header to the request's query string. This is most commonly used
|
||||
// with pre-signed requests preventing headers from being added to the
|
||||
// request's query string.
|
||||
DisableHeaderHoisting bool
|
||||
|
||||
// Disables the automatic escaping of the URI path of the request for the
|
||||
// siganture's canonical string's path. For services that do not need additional
|
||||
// escaping then use this to disable the signer escaping the path.
|
||||
//
|
||||
// S3 is an example of a service that does not need additional escaping.
|
||||
//
|
||||
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||
DisableURIPathEscaping bool
|
||||
}
|
||||
|
||||
// Signer is a SigV4a HTTP signing implementation
|
||||
type Signer struct {
|
||||
options SignerOptions
|
||||
}
|
||||
|
||||
// NewSigner constructs a SigV4a Signer.
|
||||
func NewSigner(optFns ...func(*SignerOptions)) *Signer {
|
||||
options := SignerOptions{}
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(&options)
|
||||
}
|
||||
|
||||
return &Signer{options: options}
|
||||
}
|
||||
|
||||
// deriveKeyFromAccessKeyPair derives a NIST P-256 PrivateKey from the given
|
||||
// IAM AccessKey and SecretKey pair.
|
||||
//
|
||||
// Based on FIPS.186-4 Appendix B.4.2
|
||||
func deriveKeyFromAccessKeyPair(accessKey, secretKey string) (*ecdsa.PrivateKey, error) {
|
||||
params := p256.Params()
|
||||
bitLen := params.BitSize // Testing random candidates does not require an additional 64 bits
|
||||
counter := 0x01
|
||||
|
||||
buffer := make([]byte, 1+len(accessKey)) // 1 byte counter + len(accessKey)
|
||||
kdfContext := bytes.NewBuffer(buffer)
|
||||
|
||||
inputKey := append([]byte("AWS4A"), []byte(secretKey)...)
|
||||
|
||||
d := new(big.Int)
|
||||
for {
|
||||
kdfContext.Reset()
|
||||
kdfContext.WriteString(accessKey)
|
||||
kdfContext.WriteByte(byte(counter))
|
||||
|
||||
key, err := signerCrypto.HMACKeyDerivation(sha256.New, bitLen, inputKey, []byte(signingAlgorithm), kdfContext.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Check key first before calling SetBytes if key key is in fact a valid candidate.
|
||||
// This ensures the byte slice is the correct length (32-bytes) to compare in constant-time
|
||||
cmp, err := signerCrypto.ConstantTimeByteCompare(key, nMinusTwoP256.Bytes())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if cmp == -1 {
|
||||
d.SetBytes(key)
|
||||
break
|
||||
}
|
||||
|
||||
counter++
|
||||
if counter > 0xFF {
|
||||
return nil, fmt.Errorf("exhausted single byte external counter")
|
||||
}
|
||||
}
|
||||
d = d.Add(d, one)
|
||||
|
||||
priv := new(ecdsa.PrivateKey)
|
||||
priv.PublicKey.Curve = p256
|
||||
priv.D = d
|
||||
priv.PublicKey.X, priv.PublicKey.Y = p256.ScalarBaseMult(d.Bytes())
|
||||
|
||||
return priv, nil
|
||||
}
|
||||
|
||||
type httpSigner struct {
|
||||
Request *http.Request
|
||||
ServiceName string
|
||||
RegionSet []string
|
||||
Time time.Time
|
||||
Credentials Credentials
|
||||
IsPreSign bool
|
||||
|
||||
Logger *zap.Logger
|
||||
Debug bool
|
||||
|
||||
// PayloadHash is the hex encoded SHA-256 hash of the request payload
|
||||
// If len(PayloadHash) == 0 the signer will attempt to send the request
|
||||
// as an unsigned payload. Note: Unsigned payloads only work for a subset of services.
|
||||
PayloadHash string
|
||||
|
||||
DisableHeaderHoisting bool
|
||||
DisableURIPathEscaping bool
|
||||
}
|
||||
|
||||
// SignHTTP takes the provided http.Request, payload hash, service, regionSet, and time and signs using SigV4a.
|
||||
// The passed in request will be modified in place.
|
||||
func (s *Signer) SignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) error {
|
||||
options := s.options
|
||||
for _, fn := range optFns {
|
||||
fn(&options)
|
||||
}
|
||||
|
||||
signer := &httpSigner{
|
||||
Request: r,
|
||||
PayloadHash: payloadHash,
|
||||
ServiceName: service,
|
||||
RegionSet: regionSet,
|
||||
Credentials: credentials,
|
||||
Time: signingTime.UTC(),
|
||||
DisableHeaderHoisting: options.DisableHeaderHoisting,
|
||||
DisableURIPathEscaping: options.DisableURIPathEscaping,
|
||||
Logger: options.Logger,
|
||||
}
|
||||
|
||||
signedRequest, err := signer.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logHTTPSigningInfo(ctx, options, signedRequest)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VerifySignature checks sigv4a.
|
||||
func (s *Signer) VerifySignature(credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, signature string, optFns ...func(*SignerOptions)) error {
|
||||
return s.verifySignature(credentials, r, payloadHash, service, regionSet, signingTime, signature, false, optFns...)
|
||||
}
|
||||
|
||||
// VerifyPresigned checks sigv4a.
|
||||
func (s *Signer) VerifyPresigned(credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, signature string, optFns ...func(*SignerOptions)) error {
|
||||
return s.verifySignature(credentials, r, payloadHash, service, regionSet, signingTime, signature, true, optFns...)
|
||||
}
|
||||
|
||||
func (s *Signer) verifySignature(credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, signature string, isPresigned bool, optFns ...func(*SignerOptions)) error {
|
||||
options := s.options
|
||||
for _, fn := range optFns {
|
||||
fn(&options)
|
||||
}
|
||||
|
||||
signer := &httpSigner{
|
||||
Request: r,
|
||||
PayloadHash: payloadHash,
|
||||
ServiceName: service,
|
||||
RegionSet: regionSet,
|
||||
Credentials: credentials,
|
||||
Time: signingTime.UTC(),
|
||||
IsPreSign: isPresigned,
|
||||
DisableHeaderHoisting: options.DisableHeaderHoisting,
|
||||
DisableURIPathEscaping: options.DisableURIPathEscaping,
|
||||
}
|
||||
|
||||
signedReq, err := signer.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logHTTPSigningInfo(context.TODO(), options, signedReq)
|
||||
|
||||
signatureRaw, err := hex.DecodeString(signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("decode hex signature: %w", err)
|
||||
}
|
||||
|
||||
ok, err := signerCrypto.VerifySignature(&credentials.PrivateKey.PublicKey, makeHash(sha256.New(), []byte(signedReq.StringToSign)), signatureRaw)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !ok {
|
||||
return fmt.Errorf("v4a: invalid signature")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PresignHTTP takes the provided http.Request, payload hash, service, regionSet, and time and presigns using SigV4a
|
||||
// Returns the presigned URL along with the headers that were signed with the request.
|
||||
//
|
||||
// PresignHTTP will not set the expires time of the presigned request
|
||||
// automatically. To specify the expire duration for a request add the
|
||||
// "X-Amz-Expires" query parameter on the request with the value as the
|
||||
// duration in seconds the presigned URL should be considered valid for. This
|
||||
// parameter is not used by all AWS services, and is most notable used by
|
||||
// Amazon S3 APIs.
|
||||
func (s *Signer) PresignHTTP(ctx context.Context, credentials Credentials, r *http.Request, payloadHash string, service string, regionSet []string, signingTime time.Time, optFns ...func(*SignerOptions)) (signedURI string, signedHeaders http.Header, err error) {
|
||||
options := s.options
|
||||
for _, fn := range optFns {
|
||||
fn(&options)
|
||||
}
|
||||
|
||||
signer := &httpSigner{
|
||||
Request: r,
|
||||
PayloadHash: payloadHash,
|
||||
ServiceName: service,
|
||||
RegionSet: regionSet,
|
||||
Credentials: credentials,
|
||||
Time: signingTime.UTC(),
|
||||
IsPreSign: true,
|
||||
DisableHeaderHoisting: options.DisableHeaderHoisting,
|
||||
DisableURIPathEscaping: options.DisableURIPathEscaping,
|
||||
}
|
||||
|
||||
signedRequest, err := signer.Build()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
logHTTPSigningInfo(ctx, options, signedRequest)
|
||||
|
||||
signedHeaders = make(http.Header)
|
||||
|
||||
// For the signed headers we canonicalize the header keys in the returned map.
|
||||
// This avoids situations where can standard library double headers like host header. For example the standard
|
||||
// library will set the Host header, even if it is present in lower-case form.
|
||||
for k, v := range signedRequest.SignedHeaders {
|
||||
key := textproto.CanonicalMIMEHeaderKey(k)
|
||||
signedHeaders[key] = append(signedHeaders[key], v...)
|
||||
}
|
||||
|
||||
return signedRequest.Request.URL.String(), signedHeaders, nil
|
||||
}
|
||||
|
||||
func (s *httpSigner) setRequiredSigningFields(headers http.Header, query url.Values) {
|
||||
amzDate := s.Time.Format(timeFormat)
|
||||
|
||||
if s.IsPreSign {
|
||||
query.Set(AmzRegionSetKey, strings.Join(s.RegionSet, ","))
|
||||
query.Set(amzDateKey, amzDate)
|
||||
query.Set(amzAlgorithmKey, signingAlgorithm)
|
||||
if len(s.Credentials.SessionToken) > 0 {
|
||||
query.Set(amzSecurityTokenKey, s.Credentials.SessionToken)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
headers.Set(AmzRegionSetKey, strings.Join(s.RegionSet, ","))
|
||||
headers.Set(amzDateKey, amzDate)
|
||||
if len(s.Credentials.SessionToken) > 0 {
|
||||
headers.Set(amzSecurityTokenKey, s.Credentials.SessionToken)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *httpSigner) Build() (signedRequest, error) {
|
||||
req := s.Request
|
||||
|
||||
query := req.URL.Query()
|
||||
headers := req.Header
|
||||
|
||||
s.setRequiredSigningFields(headers, query)
|
||||
|
||||
// Sort Each Query Key's Values
|
||||
for key := range query {
|
||||
sort.Strings(query[key])
|
||||
}
|
||||
|
||||
v4Internal.SanitizeHostForHeader(req)
|
||||
|
||||
credentialScope := s.buildCredentialScope()
|
||||
credentialStr := s.Credentials.Context + "/" + credentialScope
|
||||
if s.IsPreSign {
|
||||
query.Set(amzCredentialKey, credentialStr)
|
||||
}
|
||||
|
||||
unsignedHeaders := headers
|
||||
if s.IsPreSign && !s.DisableHeaderHoisting {
|
||||
urlValues := url.Values{}
|
||||
urlValues, unsignedHeaders = buildQuery(v4Internal.AllowedQueryHoisting, unsignedHeaders)
|
||||
for k := range urlValues {
|
||||
query[k] = urlValues[k]
|
||||
}
|
||||
}
|
||||
|
||||
host := req.URL.Host
|
||||
if len(req.Host) > 0 {
|
||||
host = req.Host
|
||||
}
|
||||
|
||||
var (
|
||||
signedHeaders http.Header
|
||||
signedHeadersStr string
|
||||
canonicalHeaderStr string
|
||||
)
|
||||
|
||||
if s.IsPreSign {
|
||||
signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredPresignedHeaders, unsignedHeaders, s.Request.ContentLength)
|
||||
} else {
|
||||
signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredHeaders, unsignedHeaders, s.Request.ContentLength)
|
||||
}
|
||||
|
||||
if s.IsPreSign {
|
||||
query.Set(amzSignedHeadersKey, signedHeadersStr)
|
||||
}
|
||||
|
||||
rawQuery := strings.Replace(query.Encode(), "+", "%20", -1)
|
||||
|
||||
canonicalURI := v4Internal.GetURIPath(req.URL)
|
||||
if !s.DisableURIPathEscaping {
|
||||
canonicalURI = httpbinding.EscapePath(canonicalURI, false)
|
||||
}
|
||||
|
||||
canonicalString := s.buildCanonicalString(
|
||||
req.Method,
|
||||
canonicalURI,
|
||||
rawQuery,
|
||||
signedHeadersStr,
|
||||
canonicalHeaderStr,
|
||||
)
|
||||
|
||||
strToSign := s.buildStringToSign(credentialScope, canonicalString)
|
||||
signingSignature, err := s.buildSignature(strToSign)
|
||||
if err != nil {
|
||||
return signedRequest{}, err
|
||||
}
|
||||
|
||||
if s.IsPreSign {
|
||||
rawQuery += "&X-Amz-Signature=" + signingSignature
|
||||
} else {
|
||||
headers[authorizationHeader] = append(headers[authorizationHeader][:0], buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature))
|
||||
}
|
||||
|
||||
req.URL.RawQuery = rawQuery
|
||||
|
||||
return signedRequest{
|
||||
Request: req,
|
||||
SignedHeaders: signedHeaders,
|
||||
CanonicalString: canonicalString,
|
||||
StringToSign: strToSign,
|
||||
PreSigned: s.IsPreSign,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature string) string {
|
||||
const credential = "Credential="
|
||||
const signedHeaders = "SignedHeaders="
|
||||
const signature = "Signature="
|
||||
const commaSpace = ", "
|
||||
|
||||
var parts strings.Builder
|
||||
parts.Grow(len(signingAlgorithm) + 1 +
|
||||
len(credential) + len(credentialStr) + len(commaSpace) +
|
||||
len(signedHeaders) + len(signedHeadersStr) + len(commaSpace) +
|
||||
len(signature) + len(signingSignature),
|
||||
)
|
||||
parts.WriteString(signingAlgorithm)
|
||||
parts.WriteRune(' ')
|
||||
parts.WriteString(credential)
|
||||
parts.WriteString(credentialStr)
|
||||
parts.WriteString(commaSpace)
|
||||
parts.WriteString(signedHeaders)
|
||||
parts.WriteString(signedHeadersStr)
|
||||
parts.WriteString(commaSpace)
|
||||
parts.WriteString(signature)
|
||||
parts.WriteString(signingSignature)
|
||||
return parts.String()
|
||||
}
|
||||
|
||||
func (s *httpSigner) buildCredentialScope() string {
|
||||
return strings.Join([]string{
|
||||
s.Time.Format(shortTimeFormat),
|
||||
s.ServiceName,
|
||||
"aws4_request",
|
||||
}, "/")
|
||||
|
||||
}
|
||||
|
||||
func buildQuery(r v4Internal.Rule, header http.Header) (url.Values, http.Header) {
|
||||
query := url.Values{}
|
||||
unsignedHeaders := http.Header{}
|
||||
for k, h := range header {
|
||||
if r.IsValid(k) {
|
||||
query[k] = h
|
||||
} else {
|
||||
unsignedHeaders[k] = h
|
||||
}
|
||||
}
|
||||
|
||||
return query, unsignedHeaders
|
||||
}
|
||||
|
||||
func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, header http.Header, length int64) (signed http.Header, signedHeaders, canonicalHeadersStr string) {
|
||||
signed = make(http.Header)
|
||||
|
||||
var headers []string
|
||||
const hostHeader = "host"
|
||||
headers = append(headers, hostHeader)
|
||||
signed[hostHeader] = append(signed[hostHeader], host)
|
||||
|
||||
//const contentLengthHeader = "content-length"
|
||||
//if length > 0 {
|
||||
// headers = append(headers, contentLengthHeader)
|
||||
// signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10))
|
||||
//}
|
||||
|
||||
for k, v := range header {
|
||||
if !rule.IsValid(k) {
|
||||
continue // ignored header
|
||||
}
|
||||
|
||||
lowerCaseKey := strings.ToLower(k)
|
||||
if _, ok := signed[lowerCaseKey]; ok {
|
||||
// include additional values
|
||||
signed[lowerCaseKey] = append(signed[lowerCaseKey], v...)
|
||||
continue
|
||||
}
|
||||
|
||||
headers = append(headers, lowerCaseKey)
|
||||
signed[lowerCaseKey] = v
|
||||
}
|
||||
sort.Strings(headers)
|
||||
|
||||
signedHeaders = strings.Join(headers, ";")
|
||||
|
||||
var canonicalHeaders strings.Builder
|
||||
n := len(headers)
|
||||
const colon = ':'
|
||||
for i := 0; i < n; i++ {
|
||||
if headers[i] == hostHeader {
|
||||
canonicalHeaders.WriteString(hostHeader)
|
||||
canonicalHeaders.WriteRune(colon)
|
||||
canonicalHeaders.WriteString(v4Internal.StripExcessSpaces(host))
|
||||
} else {
|
||||
canonicalHeaders.WriteString(headers[i])
|
||||
canonicalHeaders.WriteRune(colon)
|
||||
// Trim out leading, trailing, and dedup inner spaces from signed header values.
|
||||
values := signed[headers[i]]
|
||||
for j, v := range values {
|
||||
cleanedValue := strings.TrimSpace(v4Internal.StripExcessSpaces(v))
|
||||
canonicalHeaders.WriteString(cleanedValue)
|
||||
if j < len(values)-1 {
|
||||
canonicalHeaders.WriteRune(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
canonicalHeaders.WriteRune('\n')
|
||||
}
|
||||
canonicalHeadersStr = canonicalHeaders.String()
|
||||
|
||||
return signed, signedHeaders, canonicalHeadersStr
|
||||
}
|
||||
|
||||
func (s *httpSigner) buildCanonicalString(method, uri, query, signedHeaders, canonicalHeaders string) string {
|
||||
return strings.Join([]string{
|
||||
method,
|
||||
uri,
|
||||
query,
|
||||
canonicalHeaders,
|
||||
signedHeaders,
|
||||
s.PayloadHash,
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func (s *httpSigner) buildStringToSign(credentialScope, canonicalRequestString string) string {
|
||||
return strings.Join([]string{
|
||||
signingAlgorithm,
|
||||
s.Time.Format(timeFormat),
|
||||
credentialScope,
|
||||
hex.EncodeToString(makeHash(sha256.New(), []byte(canonicalRequestString))),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func makeHash(hash hash.Hash, b []byte) []byte {
|
||||
hash.Reset()
|
||||
hash.Write(b)
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
|
||||
func (s *httpSigner) buildSignature(strToSign string) (string, error) {
|
||||
sig, err := s.Credentials.PrivateKey.Sign(rand.Reader, makeHash(sha256.New(), []byte(strToSign)), crypto.SHA256)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(sig), nil
|
||||
}
|
||||
|
||||
const logSignInfoMsg = `Request Signature:
|
||||
---[ CANONICAL STRING ]-----------------------------
|
||||
%s
|
||||
---[ STRING TO SIGN ]--------------------------------
|
||||
%s%s
|
||||
-----------------------------------------------------`
|
||||
const logSignedURLMsg = `
|
||||
---[ SIGNED URL ]------------------------------------
|
||||
%s`
|
||||
|
||||
func logHTTPSigningInfo(_ context.Context, options SignerOptions, r signedRequest) {
|
||||
if !options.LogSigning {
|
||||
return
|
||||
}
|
||||
signedURLMsg := ""
|
||||
if r.PreSigned {
|
||||
signedURLMsg = fmt.Sprintf(logSignedURLMsg, r.Request.URL.String())
|
||||
}
|
||||
if options.Logger != nil {
|
||||
options.Logger.Debug(fmt.Sprintf(logSignInfoMsg, r.CanonicalString, r.StringToSign, signedURLMsg))
|
||||
}
|
||||
}
|
||||
|
||||
type signedRequest struct {
|
||||
Request *http.Request
|
||||
SignedHeaders http.Header
|
||||
CanonicalString string
|
||||
StringToSign string
|
||||
PreSigned bool
|
||||
}
|
425
api/auth/signer/v4asdk2/v4a_test.go
Normal file
425
api/auth/signer/v4asdk2/v4a_test.go
Normal file
|
@ -0,0 +1,425 @@
|
|||
// This file is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/internal/v4a/v4a_test.go
|
||||
// with changes:
|
||||
// * use zap.Logger instead of smithy-go/logging
|
||||
|
||||
package v4a
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4asdk2/internal/crypto"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"go.uber.org/zap/zaptest"
|
||||
)
|
||||
|
||||
const (
|
||||
accessKey = "AKISORANDOMAASORANDOM"
|
||||
secretKey = "q+jcrXGc+0zWN6uzclKVhvMmUsIfRPa4rlRandom"
|
||||
)
|
||||
|
||||
func TestDeriveECDSAKeyPairFromSecret(t *testing.T) {
|
||||
privateKey, err := deriveKeyFromAccessKeyPair(accessKey, secretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
expectedX := func() *big.Int {
|
||||
t.Helper()
|
||||
b, ok := new(big.Int).SetString("15D242CEEBF8D8169FD6A8B5A746C41140414C3B07579038DA06AF89190FFFCB", 16)
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse big integer")
|
||||
}
|
||||
return b
|
||||
}()
|
||||
expectedY := func() *big.Int {
|
||||
t.Helper()
|
||||
b, ok := new(big.Int).SetString("515242CEDD82E94799482E4C0514B505AFCCF2C0C98D6A553BF539F424C5EC0", 16)
|
||||
if !ok {
|
||||
t.Fatalf("failed to parse big integer")
|
||||
}
|
||||
return b
|
||||
}()
|
||||
|
||||
if privateKey.X.Cmp(expectedX) != 0 {
|
||||
t.Errorf("expected % X, got % X", expectedX, privateKey.X)
|
||||
}
|
||||
if privateKey.Y.Cmp(expectedY) != 0 {
|
||||
t.Errorf("expected % X, got % X", expectedY, privateKey.Y)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignHTTP(t *testing.T) {
|
||||
req := buildRequest("dynamodb", "us-east-1")
|
||||
|
||||
signer, credProvider := buildSigner(t, true)
|
||||
|
||||
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
err = signer.SignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
expectedDate := "19700101T000000Z"
|
||||
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||||
expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||||
expectedSignedHeaders := "content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-region-set;x-amz-security-token;x-amz-target"
|
||||
expectedStrToSignHash := "4ba7d0482cf4d5450cefdc067a00de1a4a715e444856fa3e1d85c35fb34d9730"
|
||||
|
||||
q := req.Header
|
||||
|
||||
validateAuthorization(t, q.Get("Authorization"), expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash)
|
||||
|
||||
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignHTTP_NoSessionToken(t *testing.T) {
|
||||
req := buildRequest("dynamodb", "us-east-1")
|
||||
|
||||
signer, credProvider := buildSigner(t, false)
|
||||
|
||||
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
err = signer.SignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||||
expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||||
expectedSignedHeaders := "content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-region-set;x-amz-target"
|
||||
expectedStrToSignHash := "1aeefb422ae6aa0de7aec829da813e55cff35553cac212dffd5f9474c71e47ee"
|
||||
|
||||
q := req.Header
|
||||
|
||||
validateAuthorization(t, q.Get("Authorization"), expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash)
|
||||
}
|
||||
|
||||
func TestPresignHTTP(t *testing.T) {
|
||||
req := buildRequest("dynamodb", "us-east-1")
|
||||
|
||||
signer, credProvider := buildSigner(t, false)
|
||||
|
||||
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Set("X-Amz-Expires", "18000")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
signedURL, _, err := signer.PresignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
expectedDate := "19700101T000000Z"
|
||||
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||||
expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore"
|
||||
expectedCredential := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||||
expectedStrToSignHash := "d7ffbd2fab644384c056957e6ac38de4ae68246764b5f5df171b3824153b6397"
|
||||
expectedTarget := "prefix.Operation"
|
||||
|
||||
signedReq, err := url.Parse(signedURL)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
q := signedReq.Query()
|
||||
|
||||
validateSignature(t, expectedStrToSignHash, q.Get("X-Amz-Signature"))
|
||||
|
||||
if e, a := expectedAlg, q.Get("X-Amz-Algorithm"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedCredential, q.Get("X-Amz-Credential"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 {
|
||||
t.Errorf("expect %v to be empty", a)
|
||||
}
|
||||
if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := "us-east-1", q.Get("X-Amz-Region-Set"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresignHTTP_BodyWithArrayRequest(t *testing.T) {
|
||||
req := buildRequest("dynamodb", "us-east-1")
|
||||
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||||
|
||||
signer, credProvider := buildSigner(t, true)
|
||||
|
||||
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Set("X-Amz-Expires", "300")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
signedURI, _, err := signer.PresignHTTP(context.Background(), key, req, EmptyStringSHA256, "dynamodb", []string{"us-east-1"}, time.Unix(0, 0))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
signedReq, err := url.Parse(signedURI)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
expectedAlg := "AWS4-ECDSA-P256-SHA256"
|
||||
expectedDate := "19700101T000000Z"
|
||||
expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore"
|
||||
expectedStrToSignHash := "acff64fd3689be96259d4112c3742ff79f4da0d813bc58a285dc1c4449760bec"
|
||||
expectedCred := "AKISORANDOMAASORANDOM/19700101/dynamodb/aws4_request"
|
||||
expectedTarget := "prefix.Operation"
|
||||
|
||||
q := signedReq.Query()
|
||||
|
||||
validateSignature(t, expectedStrToSignHash, q.Get("X-Amz-Signature"))
|
||||
|
||||
if e, a := expectedAlg, q.Get("X-Amz-Algorithm"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedCred, q.Get("X-Amz-Credential"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 {
|
||||
t.Errorf("expect %v to be empty, was not", a)
|
||||
}
|
||||
if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := "us-east-1", q.Get("X-Amz-Region-Set"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
func TestSign_buildCanonicalHeaders(t *testing.T) {
|
||||
serviceName := "mockAPI"
|
||||
region := "mock-region"
|
||||
endpoint := "https://" + serviceName + "." + region + ".amazonaws.com"
|
||||
|
||||
req, err := http.NewRequest("POST", endpoint, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request, %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("FooInnerSpace", " inner space ")
|
||||
req.Header.Set("FooLeadingSpace", " leading-space")
|
||||
req.Header.Add("FooMultipleSpace", "no-space")
|
||||
req.Header.Add("FooMultipleSpace", "\ttab-space")
|
||||
req.Header.Add("FooMultipleSpace", "trailing-space ")
|
||||
req.Header.Set("FooNoSpace", "no-space")
|
||||
req.Header.Set("FooTabSpace", "\ttab-space\t")
|
||||
req.Header.Set("FooTrailingSpace", "trailing-space ")
|
||||
req.Header.Set("FooWrappedSpace", " wrapped-space ")
|
||||
|
||||
credProvider := &SymmetricCredentialAdaptor{
|
||||
SymmetricProvider: staticCredentialsProvider{
|
||||
Value: aws.Credentials{
|
||||
AccessKeyID: accessKey,
|
||||
SecretAccessKey: secretKey,
|
||||
},
|
||||
},
|
||||
}
|
||||
key, err := credProvider.RetrievePrivateKey(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
ctx := &httpSigner{
|
||||
Request: req,
|
||||
ServiceName: serviceName,
|
||||
RegionSet: []string{region},
|
||||
Credentials: key,
|
||||
Time: time.Date(2021, 10, 20, 12, 42, 0, 0, time.UTC),
|
||||
}
|
||||
|
||||
build, err := ctx.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
expectCanonicalString := strings.Join([]string{
|
||||
`POST`,
|
||||
`/`,
|
||||
``,
|
||||
`fooinnerspace:inner space`,
|
||||
`fooleadingspace:leading-space`,
|
||||
`foomultiplespace:no-space,tab-space,trailing-space`,
|
||||
`foonospace:no-space`,
|
||||
`footabspace:tab-space`,
|
||||
`footrailingspace:trailing-space`,
|
||||
`foowrappedspace:wrapped-space`,
|
||||
`host:mockAPI.mock-region.amazonaws.com`,
|
||||
`x-amz-date:20211020T124200Z`,
|
||||
`x-amz-region-set:mock-region`,
|
||||
``,
|
||||
`fooinnerspace;fooleadingspace;foomultiplespace;foonospace;footabspace;footrailingspace;foowrappedspace;host;x-amz-date;x-amz-region-set`,
|
||||
``,
|
||||
}, "\n")
|
||||
if diff := cmpDiff(expectCanonicalString, build.CanonicalString); diff != "" {
|
||||
t.Errorf("expect match, got\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func validateAuthorization(t *testing.T, authorization, expectedAlg, expectedCredential, expectedSignedHeaders, expectedStrToSignHash string) {
|
||||
t.Helper()
|
||||
split := strings.SplitN(authorization, " ", 2)
|
||||
|
||||
if len(split) != 2 {
|
||||
t.Fatal("unexpected authorization header format")
|
||||
}
|
||||
|
||||
if e, a := split[0], expectedAlg; e != a {
|
||||
t.Errorf("expected %v, got %v", e, a)
|
||||
}
|
||||
|
||||
keyValues := strings.Split(split[1], ", ")
|
||||
seen := make(map[string]string)
|
||||
|
||||
for _, kv := range keyValues {
|
||||
idx := strings.Index(kv, "=")
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
key, value := kv[:idx], kv[idx+1:]
|
||||
seen[key] = value
|
||||
}
|
||||
|
||||
if a, ok := seen["Credential"]; ok {
|
||||
if expectedCredential != a {
|
||||
t.Errorf("expected credential %v, got %v", expectedCredential, a)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("Credential not found in authorization string")
|
||||
}
|
||||
|
||||
if a, ok := seen["SignedHeaders"]; ok {
|
||||
if expectedSignedHeaders != a {
|
||||
t.Errorf("expected signed headers %v, got %v", expectedSignedHeaders, a)
|
||||
}
|
||||
} else {
|
||||
t.Errorf("SignedHeaders not found in authorization string")
|
||||
}
|
||||
|
||||
if a, ok := seen["Signature"]; ok {
|
||||
validateSignature(t, expectedStrToSignHash, a)
|
||||
} else {
|
||||
t.Errorf("signature not found in authorization string")
|
||||
}
|
||||
}
|
||||
|
||||
func validateSignature(t *testing.T, expectedHash, signature string) {
|
||||
t.Helper()
|
||||
pair, err := deriveKeyFromAccessKeyPair(accessKey, secretKey)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
hash, _ := hex.DecodeString(expectedHash)
|
||||
sig, _ := hex.DecodeString(signature)
|
||||
|
||||
ok, err := crypto.VerifySignature(&pair.PublicKey, hash, sig)
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Errorf("failed to verify signing singature")
|
||||
}
|
||||
}
|
||||
|
||||
func buildRequest(serviceName, region string) *http.Request {
|
||||
endpoint := "https://" + serviceName + "." + region + ".amazonaws.com"
|
||||
req, _ := http.NewRequest("POST", endpoint, nil)
|
||||
req.URL.Opaque = "//example.org/bucket/key-._~,!@%23$%25^&*()"
|
||||
req.Header.Set("X-Amz-Target", "prefix.Operation")
|
||||
req.Header.Set("Content-Type", "application/x-amz-json-1.0")
|
||||
|
||||
req.Header.Set("Content-Length", strconv.Itoa(1024))
|
||||
|
||||
req.Header.Set("X-Amz-Meta-Other-Header", "some-value=!@#$%^&* (+)")
|
||||
req.Header.Add("X-Amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)")
|
||||
req.Header.Add("X-amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)")
|
||||
return req
|
||||
}
|
||||
|
||||
func buildSigner(t *testing.T, withToken bool) (*Signer, CredentialsProvider) {
|
||||
creds := aws.Credentials{
|
||||
AccessKeyID: accessKey,
|
||||
SecretAccessKey: secretKey,
|
||||
}
|
||||
|
||||
if withToken {
|
||||
creds.SessionToken = "TOKEN"
|
||||
}
|
||||
|
||||
return NewSigner(func(options *SignerOptions) {
|
||||
options.Logger = zaptest.NewLogger(t)
|
||||
}), &SymmetricCredentialAdaptor{
|
||||
SymmetricProvider: staticCredentialsProvider{
|
||||
Value: creds,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type staticCredentialsProvider struct {
|
||||
Value aws.Credentials
|
||||
}
|
||||
|
||||
func (s staticCredentialsProvider) Retrieve(_ context.Context) (aws.Credentials, error) {
|
||||
v := s.Value
|
||||
if v.AccessKeyID == "" || v.SecretAccessKey == "" {
|
||||
return aws.Credentials{
|
||||
Source: "Source Name",
|
||||
}, fmt.Errorf("static credentials are empty")
|
||||
}
|
||||
|
||||
if len(v.Source) == 0 {
|
||||
v.Source = "Source Name"
|
||||
}
|
||||
|
||||
return v, nil
|
||||
}
|
||||
|
||||
func cmpDiff(e, a interface{}) string {
|
||||
if !reflect.DeepEqual(e, a) {
|
||||
return fmt.Sprintf("%v != %v", e, a)
|
||||
}
|
||||
return ""
|
||||
}
|
117
api/auth/signer/v4sdk2/signer/internal/v4/cache.go
Normal file
117
api/auth/signer/v4sdk2/signer/internal/v4/cache.go
Normal file
|
@ -0,0 +1,117 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/cache.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
)
|
||||
|
||||
func lookupKey(service, region string) string {
|
||||
var s strings.Builder
|
||||
s.Grow(len(region) + len(service) + 3)
|
||||
s.WriteString(region)
|
||||
s.WriteRune('/')
|
||||
s.WriteString(service)
|
||||
return s.String()
|
||||
}
|
||||
|
||||
type derivedKey struct {
|
||||
AccessKey string
|
||||
Date time.Time
|
||||
Credential []byte
|
||||
}
|
||||
|
||||
type derivedKeyCache struct {
|
||||
values map[string]derivedKey
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func newDerivedKeyCache() derivedKeyCache {
|
||||
return derivedKeyCache{
|
||||
values: make(map[string]derivedKey),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *derivedKeyCache) Get(credentials aws.Credentials, service, region string, signingTime SigningTime) []byte {
|
||||
key := lookupKey(service, region)
|
||||
s.mutex.RLock()
|
||||
if cred, ok := s.get(key, credentials, signingTime.Time); ok {
|
||||
s.mutex.RUnlock()
|
||||
return cred
|
||||
}
|
||||
s.mutex.RUnlock()
|
||||
|
||||
s.mutex.Lock()
|
||||
if cred, ok := s.get(key, credentials, signingTime.Time); ok {
|
||||
s.mutex.Unlock()
|
||||
return cred
|
||||
}
|
||||
cred := deriveKey(credentials.SecretAccessKey, service, region, signingTime)
|
||||
entry := derivedKey{
|
||||
AccessKey: credentials.AccessKeyID,
|
||||
Date: signingTime.Time,
|
||||
Credential: cred,
|
||||
}
|
||||
s.values[key] = entry
|
||||
s.mutex.Unlock()
|
||||
|
||||
return cred
|
||||
}
|
||||
|
||||
func (s *derivedKeyCache) get(key string, credentials aws.Credentials, signingTime time.Time) ([]byte, bool) {
|
||||
cacheEntry, ok := s.retrieveFromCache(key)
|
||||
if ok && cacheEntry.AccessKey == credentials.AccessKeyID && isSameDay(signingTime, cacheEntry.Date) {
|
||||
return cacheEntry.Credential, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (s *derivedKeyCache) retrieveFromCache(key string) (derivedKey, bool) {
|
||||
if v, ok := s.values[key]; ok {
|
||||
return v, true
|
||||
}
|
||||
return derivedKey{}, false
|
||||
}
|
||||
|
||||
// SigningKeyDeriver derives a signing key from a set of credentials
|
||||
type SigningKeyDeriver struct {
|
||||
cache derivedKeyCache
|
||||
}
|
||||
|
||||
// NewSigningKeyDeriver returns a new SigningKeyDeriver
|
||||
func NewSigningKeyDeriver() *SigningKeyDeriver {
|
||||
return &SigningKeyDeriver{
|
||||
cache: newDerivedKeyCache(),
|
||||
}
|
||||
}
|
||||
|
||||
// DeriveKey returns a derived signing key from the given credentials to be used with SigV4 signing.
|
||||
func (k *SigningKeyDeriver) DeriveKey(credential aws.Credentials, service, region string, signingTime SigningTime) []byte {
|
||||
return k.cache.Get(credential, service, region, signingTime)
|
||||
}
|
||||
|
||||
func deriveKey(secret, service, region string, t SigningTime) []byte {
|
||||
hmacDate := HMACSHA256([]byte("AWS4"+secret), []byte(t.ShortTimeFormat()))
|
||||
hmacRegion := HMACSHA256(hmacDate, []byte(region))
|
||||
hmacService := HMACSHA256(hmacRegion, []byte(service))
|
||||
return HMACSHA256(hmacService, []byte("aws4_request"))
|
||||
}
|
||||
|
||||
func isSameDay(x, y time.Time) bool {
|
||||
xYear, xMonth, xDay := x.Date()
|
||||
yYear, yMonth, yDay := y.Date()
|
||||
|
||||
if xYear != yYear {
|
||||
return false
|
||||
}
|
||||
|
||||
if xMonth != yMonth {
|
||||
return false
|
||||
}
|
||||
|
||||
return xDay == yDay
|
||||
}
|
42
api/auth/signer/v4sdk2/signer/internal/v4/const.go
Normal file
42
api/auth/signer/v4sdk2/signer/internal/v4/const.go
Normal file
|
@ -0,0 +1,42 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/const.go
|
||||
|
||||
package v4
|
||||
|
||||
// Signature Version 4 (SigV4) Constants
|
||||
const (
|
||||
// EmptyStringSHA256 is the hex encoded sha256 value of an empty string
|
||||
EmptyStringSHA256 = `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
|
||||
|
||||
// UnsignedPayload indicates that the request payload body is unsigned
|
||||
UnsignedPayload = "UNSIGNED-PAYLOAD"
|
||||
|
||||
// AmzAlgorithmKey indicates the signing algorithm
|
||||
AmzAlgorithmKey = "X-Amz-Algorithm"
|
||||
|
||||
// AmzSecurityTokenKey indicates the security token to be used with temporary credentials
|
||||
AmzSecurityTokenKey = "X-Amz-Security-Token"
|
||||
|
||||
// AmzDateKey is the UTC timestamp for the request in the format YYYYMMDD'T'HHMMSS'Z'
|
||||
AmzDateKey = "X-Amz-Date"
|
||||
|
||||
// AmzCredentialKey is the access key ID and credential scope
|
||||
AmzCredentialKey = "X-Amz-Credential"
|
||||
|
||||
// AmzSignedHeadersKey is the set of headers signed for the request
|
||||
AmzSignedHeadersKey = "X-Amz-SignedHeaders"
|
||||
|
||||
// AmzSignatureKey is the query parameter to store the SigV4 signature
|
||||
AmzSignatureKey = "X-Amz-Signature"
|
||||
|
||||
// TimeFormat is the time format to be used in the X-Amz-Date header or query parameter
|
||||
TimeFormat = "20060102T150405Z"
|
||||
|
||||
// ShortTimeFormat is the shorten time format used in the credential scope
|
||||
ShortTimeFormat = "20060102"
|
||||
|
||||
// ContentSHAKey is the SHA256 of request body
|
||||
ContentSHAKey = "X-Amz-Content-Sha256"
|
||||
|
||||
// StreamingEventsPayload indicates that the request payload body is a signed event stream.
|
||||
StreamingEventsPayload = "STREAMING-AWS4-HMAC-SHA256-EVENTS"
|
||||
)
|
90
api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go
Normal file
90
api/auth/signer/v4sdk2/signer/internal/v4/header_rules.go
Normal file
|
@ -0,0 +1,90 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header_rules.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Rules houses a set of Rule needed for validation of a
|
||||
// string value
|
||||
type Rules []Rule
|
||||
|
||||
// Rule interface allows for more flexible rules and just simply
|
||||
// checks whether or not a value adheres to that Rule
|
||||
type Rule interface {
|
||||
IsValid(value string) bool
|
||||
}
|
||||
|
||||
// IsValid will iterate through all rules and see if any rules
|
||||
// apply to the value and supports nested rules
|
||||
func (r Rules) IsValid(value string) bool {
|
||||
for _, rule := range r {
|
||||
if rule.IsValid(value) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// MapRule generic Rule for maps
|
||||
type MapRule map[string]struct{}
|
||||
|
||||
// IsValid for the map Rule satisfies whether it exists in the map
|
||||
func (m MapRule) IsValid(value string) bool {
|
||||
_, ok := m[value]
|
||||
return ok
|
||||
}
|
||||
|
||||
// AllowList is a generic Rule for include listing
|
||||
type AllowList struct {
|
||||
Rule
|
||||
}
|
||||
|
||||
// IsValid for AllowList checks if the value is within the AllowList
|
||||
func (w AllowList) IsValid(value string) bool {
|
||||
return w.Rule.IsValid(value)
|
||||
}
|
||||
|
||||
// ExcludeList is a generic Rule for exclude listing
|
||||
type ExcludeList struct {
|
||||
Rule
|
||||
}
|
||||
|
||||
// IsValid for AllowList checks if the value is within the AllowList
|
||||
func (b ExcludeList) IsValid(value string) bool {
|
||||
return !b.Rule.IsValid(value)
|
||||
}
|
||||
|
||||
// Patterns is a list of strings to match against
|
||||
type Patterns []string
|
||||
|
||||
// IsValid for Patterns checks each pattern and returns if a match has
|
||||
// been found
|
||||
func (p Patterns) IsValid(value string) bool {
|
||||
for _, pattern := range p {
|
||||
if HasPrefixFold(value, pattern) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// InclusiveRules rules allow for rules to depend on one another
|
||||
type InclusiveRules []Rule
|
||||
|
||||
// IsValid will return true if all rules are true
|
||||
func (r InclusiveRules) IsValid(value string) bool {
|
||||
for _, rule := range r {
|
||||
if !rule.IsValid(value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// HasPrefixFold tests whether the string s begins with prefix, interpreted as UTF-8 strings,
|
||||
// under Unicode case-folding.
|
||||
func HasPrefixFold(s, prefix string) bool {
|
||||
return len(s) >= len(prefix) && strings.EqualFold(s[0:len(prefix)], prefix)
|
||||
}
|
88
api/auth/signer/v4sdk2/signer/internal/v4/headers.go
Normal file
88
api/auth/signer/v4sdk2/signer/internal/v4/headers.go
Normal file
|
@ -0,0 +1,88 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header.go
|
||||
// with changes:
|
||||
// * drop User-Agent header from ignored
|
||||
|
||||
package v4
|
||||
|
||||
// IgnoredPresignedHeaders is a list of headers that are ignored during signing
|
||||
var IgnoredPresignedHeaders = Rules{
|
||||
ExcludeList{
|
||||
MapRule{
|
||||
"Authorization": struct{}{},
|
||||
"User-Agent": struct{}{},
|
||||
"X-Amzn-Trace-Id": struct{}{},
|
||||
"Expect": struct{}{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// IgnoredHeaders is a list of headers that are ignored during signing
|
||||
// drop User-Agent header to be compatible with aws sdk java v1.
|
||||
var IgnoredHeaders = Rules{
|
||||
ExcludeList{
|
||||
MapRule{
|
||||
"Authorization": struct{}{},
|
||||
//"User-Agent": struct{}{},
|
||||
"X-Amzn-Trace-Id": struct{}{},
|
||||
"Expect": struct{}{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// RequiredSignedHeaders is a allow list for Build canonical headers.
|
||||
var RequiredSignedHeaders = Rules{
|
||||
AllowList{
|
||||
MapRule{
|
||||
"Cache-Control": struct{}{},
|
||||
"Content-Disposition": struct{}{},
|
||||
"Content-Encoding": struct{}{},
|
||||
"Content-Language": struct{}{},
|
||||
"Content-Md5": struct{}{},
|
||||
"Content-Type": struct{}{},
|
||||
"Expires": struct{}{},
|
||||
"If-Match": struct{}{},
|
||||
"If-Modified-Since": struct{}{},
|
||||
"If-None-Match": struct{}{},
|
||||
"If-Unmodified-Since": struct{}{},
|
||||
"Range": struct{}{},
|
||||
"X-Amz-Acl": struct{}{},
|
||||
"X-Amz-Copy-Source": struct{}{},
|
||||
"X-Amz-Copy-Source-If-Match": struct{}{},
|
||||
"X-Amz-Copy-Source-If-Modified-Since": struct{}{},
|
||||
"X-Amz-Copy-Source-If-None-Match": struct{}{},
|
||||
"X-Amz-Copy-Source-If-Unmodified-Since": struct{}{},
|
||||
"X-Amz-Copy-Source-Range": struct{}{},
|
||||
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||||
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key": struct{}{},
|
||||
"X-Amz-Copy-Source-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||||
"X-Amz-Expected-Bucket-Owner": struct{}{},
|
||||
"X-Amz-Grant-Full-control": struct{}{},
|
||||
"X-Amz-Grant-Read": struct{}{},
|
||||
"X-Amz-Grant-Read-Acp": struct{}{},
|
||||
"X-Amz-Grant-Write": struct{}{},
|
||||
"X-Amz-Grant-Write-Acp": struct{}{},
|
||||
"X-Amz-Metadata-Directive": struct{}{},
|
||||
"X-Amz-Mfa": struct{}{},
|
||||
"X-Amz-Request-Payer": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Aws-Kms-Key-Id": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Context": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Customer-Algorithm": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Customer-Key": struct{}{},
|
||||
"X-Amz-Server-Side-Encryption-Customer-Key-Md5": struct{}{},
|
||||
"X-Amz-Storage-Class": struct{}{},
|
||||
"X-Amz-Website-Redirect-Location": struct{}{},
|
||||
"X-Amz-Content-Sha256": struct{}{},
|
||||
"X-Amz-Tagging": struct{}{},
|
||||
},
|
||||
},
|
||||
Patterns{"X-Amz-Object-Lock-"},
|
||||
Patterns{"X-Amz-Meta-"},
|
||||
}
|
||||
|
||||
// AllowedQueryHoisting is a allowed list for Build query headers. The boolean value
|
||||
// represents whether or not it is a pattern.
|
||||
var AllowedQueryHoisting = InclusiveRules{
|
||||
ExcludeList{RequiredSignedHeaders},
|
||||
Patterns{"X-Amz-"},
|
||||
}
|
65
api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go
Normal file
65
api/auth/signer/v4sdk2/signer/internal/v4/headers_test.go
Normal file
|
@ -0,0 +1,65 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/header_test.go
|
||||
|
||||
package v4
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestAllowedQueryHoisting(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Header string
|
||||
ExpectHoist bool
|
||||
}{
|
||||
"object-lock": {
|
||||
Header: "X-Amz-Object-Lock-Mode",
|
||||
ExpectHoist: false,
|
||||
},
|
||||
"s3 metadata": {
|
||||
Header: "X-Amz-Meta-SomeName",
|
||||
ExpectHoist: false,
|
||||
},
|
||||
"another header": {
|
||||
Header: "X-Amz-SomeOtherHeader",
|
||||
ExpectHoist: true,
|
||||
},
|
||||
"non X-AMZ header": {
|
||||
Header: "X-SomeOtherHeader",
|
||||
ExpectHoist: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if e, a := c.ExpectHoist, AllowedQueryHoisting.IsValid(c.Header); e != a {
|
||||
t.Errorf("expect hoist %v, was %v", e, a)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoredHeaders(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
Header string
|
||||
ExpectIgnored bool
|
||||
}{
|
||||
"expect": {
|
||||
Header: "Expect",
|
||||
ExpectIgnored: true,
|
||||
},
|
||||
"authorization": {
|
||||
Header: "Authorization",
|
||||
ExpectIgnored: true,
|
||||
},
|
||||
"X-AMZ header": {
|
||||
Header: "X-Amz-Content-Sha256",
|
||||
ExpectIgnored: false,
|
||||
},
|
||||
}
|
||||
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
if e, a := c.ExpectIgnored, IgnoredHeaders.IsValid(c.Header); e == a {
|
||||
t.Errorf("expect ignored %v, was %v", e, a)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
15
api/auth/signer/v4sdk2/signer/internal/v4/hmac.go
Normal file
15
api/auth/signer/v4sdk2/signer/internal/v4/hmac.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/hmac.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
)
|
||||
|
||||
// HMACSHA256 computes a HMAC-SHA256 of data given the provided key.
|
||||
func HMACSHA256(key []byte, data []byte) []byte {
|
||||
hash := hmac.New(sha256.New, key)
|
||||
hash.Write(data)
|
||||
return hash.Sum(nil)
|
||||
}
|
77
api/auth/signer/v4sdk2/signer/internal/v4/host.go
Normal file
77
api/auth/signer/v4sdk2/signer/internal/v4/host.go
Normal file
|
@ -0,0 +1,77 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/host.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// SanitizeHostForHeader removes default port from host and updates request.Host
|
||||
func SanitizeHostForHeader(r *http.Request) {
|
||||
host := getHost(r)
|
||||
port := portOnly(host)
|
||||
if port != "" && isDefaultPort(r.URL.Scheme, port) {
|
||||
r.Host = stripPort(host)
|
||||
}
|
||||
}
|
||||
|
||||
// Returns host from request
|
||||
func getHost(r *http.Request) string {
|
||||
if r.Host != "" {
|
||||
return r.Host
|
||||
}
|
||||
|
||||
return r.URL.Host
|
||||
}
|
||||
|
||||
// Hostname returns u.Host, without any port number.
|
||||
//
|
||||
// If Host is an IPv6 literal with a port number, Hostname returns the
|
||||
// IPv6 literal without the square brackets. IPv6 literals may include
|
||||
// a zone identifier.
|
||||
//
|
||||
// Copied from the Go 1.8 standard library (net/url)
|
||||
func stripPort(hostport string) string {
|
||||
colon := strings.IndexByte(hostport, ':')
|
||||
if colon == -1 {
|
||||
return hostport
|
||||
}
|
||||
if i := strings.IndexByte(hostport, ']'); i != -1 {
|
||||
return strings.TrimPrefix(hostport[:i], "[")
|
||||
}
|
||||
return hostport[:colon]
|
||||
}
|
||||
|
||||
// Port returns the port part of u.Host, without the leading colon.
|
||||
// If u.Host doesn't contain a port, Port returns an empty string.
|
||||
//
|
||||
// Copied from the Go 1.8 standard library (net/url)
|
||||
func portOnly(hostport string) string {
|
||||
colon := strings.IndexByte(hostport, ':')
|
||||
if colon == -1 {
|
||||
return ""
|
||||
}
|
||||
if i := strings.Index(hostport, "]:"); i != -1 {
|
||||
return hostport[i+len("]:"):]
|
||||
}
|
||||
if strings.Contains(hostport, "]") {
|
||||
return ""
|
||||
}
|
||||
return hostport[colon+len(":"):]
|
||||
}
|
||||
|
||||
// Returns true if the specified URI is using the standard port
|
||||
// (i.e. port 80 for HTTP URIs or 443 for HTTPS URIs)
|
||||
func isDefaultPort(scheme, port string) bool {
|
||||
if port == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
lowerCaseScheme := strings.ToLower(scheme)
|
||||
if (lowerCaseScheme == "http" && port == "80") || (lowerCaseScheme == "https" && port == "443") {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
15
api/auth/signer/v4sdk2/signer/internal/v4/scope.go
Normal file
15
api/auth/signer/v4sdk2/signer/internal/v4/scope.go
Normal file
|
@ -0,0 +1,15 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/scope.go
|
||||
|
||||
package v4
|
||||
|
||||
import "strings"
|
||||
|
||||
// BuildCredentialScope builds the Signature Version 4 (SigV4) signing scope
|
||||
func BuildCredentialScope(signingTime SigningTime, region, service string) string {
|
||||
return strings.Join([]string{
|
||||
signingTime.ShortTimeFormat(),
|
||||
region,
|
||||
service,
|
||||
"aws4_request",
|
||||
}, "/")
|
||||
}
|
38
api/auth/signer/v4sdk2/signer/internal/v4/time.go
Normal file
38
api/auth/signer/v4sdk2/signer/internal/v4/time.go
Normal file
|
@ -0,0 +1,38 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/time.go
|
||||
|
||||
package v4
|
||||
|
||||
import "time"
|
||||
|
||||
// SigningTime provides a wrapper around a time.Time which provides cached values for SigV4 signing.
|
||||
type SigningTime struct {
|
||||
time.Time
|
||||
timeFormat string
|
||||
shortTimeFormat string
|
||||
}
|
||||
|
||||
// NewSigningTime creates a new SigningTime given a time.Time
|
||||
func NewSigningTime(t time.Time) SigningTime {
|
||||
return SigningTime{
|
||||
Time: t,
|
||||
}
|
||||
}
|
||||
|
||||
// TimeFormat provides a time formatted in the X-Amz-Date format.
|
||||
func (m *SigningTime) TimeFormat() string {
|
||||
return m.format(&m.timeFormat, TimeFormat)
|
||||
}
|
||||
|
||||
// ShortTimeFormat provides a time formatted of 20060102.
|
||||
func (m *SigningTime) ShortTimeFormat() string {
|
||||
return m.format(&m.shortTimeFormat, ShortTimeFormat)
|
||||
}
|
||||
|
||||
func (m *SigningTime) format(target *string, format string) string {
|
||||
if len(*target) > 0 {
|
||||
return *target
|
||||
}
|
||||
v := m.Time.Format(format)
|
||||
*target = v
|
||||
return v
|
||||
}
|
82
api/auth/signer/v4sdk2/signer/internal/v4/util.go
Normal file
82
api/auth/signer/v4sdk2/signer/internal/v4/util.go
Normal file
|
@ -0,0 +1,82 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/util.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const doubleSpace = " "
|
||||
|
||||
// StripExcessSpaces will rewrite the passed in slice's string values to not
|
||||
// contain multiple side-by-side spaces.
|
||||
func StripExcessSpaces(str string) string {
|
||||
var j, k, l, m, spaces int
|
||||
// Trim trailing spaces
|
||||
for j = len(str) - 1; j >= 0 && str[j] == ' '; j-- {
|
||||
}
|
||||
|
||||
// Trim leading spaces
|
||||
for k = 0; k < j && str[k] == ' '; k++ {
|
||||
}
|
||||
str = str[k : j+1]
|
||||
|
||||
// Strip multiple spaces.
|
||||
j = strings.Index(str, doubleSpace)
|
||||
if j < 0 {
|
||||
return str
|
||||
}
|
||||
|
||||
buf := []byte(str)
|
||||
for k, m, l = j, j, len(buf); k < l; k++ {
|
||||
if buf[k] == ' ' {
|
||||
if spaces == 0 {
|
||||
// First space.
|
||||
buf[m] = buf[k]
|
||||
m++
|
||||
}
|
||||
spaces++
|
||||
} else {
|
||||
// End of multiple spaces.
|
||||
spaces = 0
|
||||
buf[m] = buf[k]
|
||||
m++
|
||||
}
|
||||
}
|
||||
|
||||
return string(buf[:m])
|
||||
}
|
||||
|
||||
// GetURIPath returns the escaped URI component from the provided URL.
|
||||
func GetURIPath(u *url.URL) string {
|
||||
var uriPath string
|
||||
|
||||
if len(u.Opaque) > 0 {
|
||||
const schemeSep, pathSep, queryStart = "//", "/", "?"
|
||||
|
||||
opaque := u.Opaque
|
||||
// Cut off the query string if present.
|
||||
if idx := strings.Index(opaque, queryStart); idx >= 0 {
|
||||
opaque = opaque[:idx]
|
||||
}
|
||||
|
||||
// Cutout the scheme separator if present.
|
||||
if strings.Index(opaque, schemeSep) == 0 {
|
||||
opaque = opaque[len(schemeSep):]
|
||||
}
|
||||
|
||||
// capture URI path starting with first path separator.
|
||||
if idx := strings.Index(opaque, pathSep); idx >= 0 {
|
||||
uriPath = opaque[idx:]
|
||||
}
|
||||
} else {
|
||||
uriPath = u.EscapedPath()
|
||||
}
|
||||
|
||||
if len(uriPath) == 0 {
|
||||
uriPath = "/"
|
||||
}
|
||||
|
||||
return uriPath
|
||||
}
|
160
api/auth/signer/v4sdk2/signer/internal/v4/util_test.go
Normal file
160
api/auth/signer/v4sdk2/signer/internal/v4/util_test.go
Normal file
|
@ -0,0 +1,160 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/internal/v4/util_test.go
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func lazyURLParse(v string) func() (*url.URL, error) {
|
||||
return func() (*url.URL, error) {
|
||||
return url.Parse(v)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetURIPath(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
getURL func() (*url.URL, error)
|
||||
expect string
|
||||
}{
|
||||
// Cases
|
||||
"with scheme": {
|
||||
getURL: lazyURLParse("https://localhost:9000"),
|
||||
expect: "/",
|
||||
},
|
||||
"no port, with scheme": {
|
||||
getURL: lazyURLParse("https://localhost"),
|
||||
expect: "/",
|
||||
},
|
||||
"without scheme": {
|
||||
getURL: lazyURLParse("localhost:9000"),
|
||||
expect: "/",
|
||||
},
|
||||
"without scheme, with path": {
|
||||
getURL: lazyURLParse("localhost:9000/abc123"),
|
||||
expect: "/abc123",
|
||||
},
|
||||
"without scheme, with separator": {
|
||||
getURL: lazyURLParse("//localhost:9000"),
|
||||
expect: "/",
|
||||
},
|
||||
"no port, without scheme, with separator": {
|
||||
getURL: lazyURLParse("//localhost"),
|
||||
expect: "/",
|
||||
},
|
||||
"without scheme, with separator, with path": {
|
||||
getURL: lazyURLParse("//localhost:9000/abc123"),
|
||||
expect: "/abc123",
|
||||
},
|
||||
"no port, without scheme, with separator, with path": {
|
||||
getURL: lazyURLParse("//localhost/abc123"),
|
||||
expect: "/abc123",
|
||||
},
|
||||
"opaque with query string": {
|
||||
getURL: lazyURLParse("localhost:9000/abc123?efg=456"),
|
||||
expect: "/abc123",
|
||||
},
|
||||
"failing test": {
|
||||
getURL: func() (*url.URL, error) {
|
||||
endpoint := "https://service.region.amazonaws.com"
|
||||
req, _ := http.NewRequest("POST", endpoint, nil)
|
||||
u := req.URL
|
||||
|
||||
u.Opaque = "//example.org/bucket/key-._~,!@#$%^&*()"
|
||||
|
||||
query := u.Query()
|
||||
query.Set("some-query-key", "value")
|
||||
u.RawQuery = query.Encode()
|
||||
|
||||
return u, nil
|
||||
},
|
||||
expect: "/bucket/key-._~,!@#$%^&*()",
|
||||
},
|
||||
}
|
||||
|
||||
for name, c := range cases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
u, err := c.getURL()
|
||||
if err != nil {
|
||||
t.Fatalf("failed to get URL, %v", err)
|
||||
}
|
||||
|
||||
actual := GetURIPath(u)
|
||||
if e, a := c.expect, actual; e != a {
|
||||
t.Errorf("expect %v path, got %v", e, a)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStripExcessHeaders(t *testing.T) {
|
||||
vals := []string{
|
||||
"",
|
||||
"123",
|
||||
"1 2 3",
|
||||
"1 2 3 ",
|
||||
" 1 2 3",
|
||||
"1 2 3",
|
||||
"1 23",
|
||||
"1 2 3",
|
||||
"1 2 ",
|
||||
" 1 2 ",
|
||||
"12 3",
|
||||
"12 3 1",
|
||||
"12 3 1",
|
||||
"12 3 1abc123",
|
||||
}
|
||||
|
||||
expected := []string{
|
||||
"",
|
||||
"123",
|
||||
"1 2 3",
|
||||
"1 2 3",
|
||||
"1 2 3",
|
||||
"1 2 3",
|
||||
"1 23",
|
||||
"1 2 3",
|
||||
"1 2",
|
||||
"1 2",
|
||||
"12 3",
|
||||
"12 3 1",
|
||||
"12 3 1",
|
||||
"12 3 1abc123",
|
||||
}
|
||||
|
||||
for i := 0; i < len(vals); i++ {
|
||||
r := StripExcessSpaces(vals[i])
|
||||
if e, a := expected[i], r; e != a {
|
||||
t.Errorf("%d, expect %v, got %v", i, e, a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var stripExcessSpaceCases = []string{
|
||||
`AWS4-HMAC-SHA256 Credential=AKIDFAKEIDFAKEID/20160628/us-west-2/s3/aws4_request, SignedHeaders=host;x-amz-date, Signature=1234567890abcdef1234567890abcdef1234567890abcdef`,
|
||||
`123 321 123 321`,
|
||||
` 123 321 123 321 `,
|
||||
` 123 321 123 321 `,
|
||||
"123",
|
||||
"1 2 3",
|
||||
" 1 2 3",
|
||||
"1 2 3",
|
||||
"1 23",
|
||||
"1 2 3",
|
||||
"1 2 ",
|
||||
" 1 2 ",
|
||||
"12 3",
|
||||
"12 3 1",
|
||||
"12 3 1",
|
||||
"12 3 1abc123",
|
||||
}
|
||||
|
||||
func BenchmarkStripExcessSpaces(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, v := range stripExcessSpaceCases {
|
||||
StripExcessSpaces(v)
|
||||
}
|
||||
}
|
||||
}
|
120
api/auth/signer/v4sdk2/signer/v4/stream.go
Normal file
120
api/auth/signer/v4sdk2/signer/v4/stream.go
Normal file
|
@ -0,0 +1,120 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/stream.go
|
||||
// with changes
|
||||
// * add GetTrailingSignature
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
)
|
||||
|
||||
// EventStreamSigner is an AWS EventStream protocol signer.
|
||||
type EventStreamSigner interface {
|
||||
GetSignature(ctx context.Context, headers, payload []byte, signingTime time.Time, optFns ...func(*StreamSignerOptions)) ([]byte, error)
|
||||
}
|
||||
|
||||
// StreamSignerOptions is the configuration options for StreamSigner.
|
||||
type StreamSignerOptions struct{}
|
||||
|
||||
// StreamSigner implements Signature Version 4 (SigV4) signing of event stream encoded payloads.
|
||||
type StreamSigner struct {
|
||||
options StreamSignerOptions
|
||||
|
||||
credentials aws.Credentials
|
||||
service string
|
||||
region string
|
||||
|
||||
prevSignature []byte
|
||||
|
||||
signingKeyDeriver *v4Internal.SigningKeyDeriver
|
||||
}
|
||||
|
||||
// NewStreamSigner returns a new AWS EventStream protocol signer.
|
||||
func NewStreamSigner(credentials aws.Credentials, service, region string, seedSignature []byte, optFns ...func(*StreamSignerOptions)) *StreamSigner {
|
||||
o := StreamSignerOptions{}
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(&o)
|
||||
}
|
||||
|
||||
return &StreamSigner{
|
||||
options: o,
|
||||
credentials: credentials,
|
||||
service: service,
|
||||
region: region,
|
||||
signingKeyDeriver: v4Internal.NewSigningKeyDeriver(),
|
||||
prevSignature: seedSignature,
|
||||
}
|
||||
}
|
||||
|
||||
// GetSignature signs the provided header and payload bytes.
|
||||
func (s *StreamSigner) GetSignature(ctx context.Context, headers, payload []byte, signingTime time.Time, optFns ...func(*StreamSignerOptions)) ([]byte, error) {
|
||||
options := s.options
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(&options)
|
||||
}
|
||||
|
||||
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.buildEventStreamStringToSign(headers, payload, prevSignature, scope, &st)
|
||||
|
||||
signature := v4Internal.HMACSHA256(sigKey, []byte(stringToSign))
|
||||
s.prevSignature = signature
|
||||
|
||||
return signature, nil
|
||||
}
|
||||
|
||||
func (s *StreamSigner) buildEventStreamStringToSign(headers, payload, previousSignature []byte, credentialScope string, signingTime *v4Internal.SigningTime) string {
|
||||
hash := sha256.New()
|
||||
return strings.Join([]string{
|
||||
"AWS4-HMAC-SHA256-PAYLOAD",
|
||||
signingTime.TimeFormat(),
|
||||
credentialScope,
|
||||
hex.EncodeToString(previousSignature),
|
||||
hex.EncodeToString(makeHash(hash, headers)),
|
||||
hex.EncodeToString(makeHash(hash, payload)),
|
||||
}, "\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")
|
||||
}
|
582
api/auth/signer/v4sdk2/signer/v4/v4.go
Normal file
582
api/auth/signer/v4sdk2/signer/v4/v4.go
Normal file
|
@ -0,0 +1,582 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/v4.go
|
||||
// with changes:
|
||||
// * using different headers for sign/presign
|
||||
// * don't duplicate content-length as signed header
|
||||
// * use copy of smithy-go encoding/httpbinding package
|
||||
// * use zap.Logger instead of smithy-go/logging
|
||||
|
||||
// Package v4 implements the AWS signature version 4 algorithm (commonly known
|
||||
// as SigV4).
|
||||
//
|
||||
// For more information about SigV4, see [Signing AWS API requests] in the IAM
|
||||
// user guide.
|
||||
//
|
||||
// While this implementation CAN work in an external context, it is developed
|
||||
// primarily for SDK use and you may encounter fringe behaviors around header
|
||||
// canonicalization.
|
||||
//
|
||||
// # Pre-escaping a request URI
|
||||
//
|
||||
// AWS v4 signature validation requires that the canonical string's URI path
|
||||
// component must be the escaped form of the HTTP request's path.
|
||||
//
|
||||
// The Go HTTP client will perform escaping automatically on the HTTP request.
|
||||
// This may cause signature validation errors because the request differs from
|
||||
// the URI path or query from which the signature was generated.
|
||||
//
|
||||
// Because of this, we recommend that you explicitly escape the request when
|
||||
// using this signer outside of the SDK to prevent possible signature mismatch.
|
||||
// This can be done by setting URL.Opaque on the request. The signer will
|
||||
// prefer that value, falling back to the return of URL.EscapedPath if unset.
|
||||
//
|
||||
// When setting URL.Opaque you must do so in the form of:
|
||||
//
|
||||
// "//<hostname>/<path>"
|
||||
//
|
||||
// // e.g.
|
||||
// "//example.com/some/path"
|
||||
//
|
||||
// The leading "//" and hostname are required or the escaping will not work
|
||||
// correctly.
|
||||
//
|
||||
// The TestStandaloneSign unit test provides a complete example of using the
|
||||
// signer outside of the SDK and pre-escaping the URI path.
|
||||
//
|
||||
// [Signing AWS API requests]: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-signing.html
|
||||
package v4
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"hash"
|
||||
"net/http"
|
||||
"net/textproto"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/smithy/encoding/httpbinding"
|
||||
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
const (
|
||||
signingAlgorithm = "AWS4-HMAC-SHA256"
|
||||
authorizationHeader = "Authorization"
|
||||
|
||||
// Version of signing v4
|
||||
Version = "SigV4"
|
||||
)
|
||||
|
||||
// HTTPSigner is an interface to a SigV4 signer that can sign HTTP requests
|
||||
type HTTPSigner interface {
|
||||
SignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request, payloadHash string, service string, region string, signingTime time.Time, optFns ...func(*SignerOptions)) error
|
||||
}
|
||||
|
||||
type keyDerivator interface {
|
||||
DeriveKey(credential aws.Credentials, service, region string, signingTime v4Internal.SigningTime) []byte
|
||||
}
|
||||
|
||||
// SignerOptions is the SigV4 Signer options.
|
||||
type SignerOptions struct {
|
||||
// Disables the Signer's moving HTTP header key/value pairs from the HTTP
|
||||
// request header to the request's query string. This is most commonly used
|
||||
// with pre-signed requests preventing headers from being added to the
|
||||
// request's query string.
|
||||
DisableHeaderHoisting bool
|
||||
|
||||
// Disables the automatic escaping of the URI path of the request for the
|
||||
// siganture's canonical string's path. For services that do not need additional
|
||||
// escaping then use this to disable the signer escaping the path.
|
||||
//
|
||||
// S3 is an example of a service that does not need additional escaping.
|
||||
//
|
||||
// http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
|
||||
DisableURIPathEscaping bool
|
||||
|
||||
// The logger to send log messages to.
|
||||
Logger *zap.Logger
|
||||
|
||||
// Enable logging of signed requests.
|
||||
// This will enable logging of the canonical request, the string to sign, and for presigning the subsequent
|
||||
// presigned URL.
|
||||
LogSigning bool
|
||||
|
||||
// Disables setting the session token on the request as part of signing
|
||||
// through X-Amz-Security-Token. This is needed for variations of v4 that
|
||||
// present the token elsewhere.
|
||||
DisableSessionToken bool
|
||||
}
|
||||
|
||||
// Signer applies AWS v4 signing to given request. Use this to sign requests
|
||||
// that need to be signed with AWS V4 Signatures.
|
||||
type Signer struct {
|
||||
options SignerOptions
|
||||
keyDerivator keyDerivator
|
||||
}
|
||||
|
||||
// NewSigner returns a new SigV4 Signer
|
||||
func NewSigner(optFns ...func(signer *SignerOptions)) *Signer {
|
||||
options := SignerOptions{}
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(&options)
|
||||
}
|
||||
|
||||
return &Signer{options: options, keyDerivator: v4Internal.NewSigningKeyDeriver()}
|
||||
}
|
||||
|
||||
type httpSigner struct {
|
||||
Request *http.Request
|
||||
ServiceName string
|
||||
Region string
|
||||
Time v4Internal.SigningTime
|
||||
Credentials aws.Credentials
|
||||
KeyDerivator keyDerivator
|
||||
IsPreSign bool
|
||||
|
||||
PayloadHash string
|
||||
|
||||
DisableHeaderHoisting bool
|
||||
DisableURIPathEscaping bool
|
||||
DisableSessionToken bool
|
||||
}
|
||||
|
||||
func (s *httpSigner) Build() (signedRequest, error) {
|
||||
req := s.Request
|
||||
|
||||
query := req.URL.Query()
|
||||
headers := req.Header
|
||||
|
||||
s.setRequiredSigningFields(headers, query)
|
||||
|
||||
// Sort Each Query Key's Values
|
||||
for key := range query {
|
||||
sort.Strings(query[key])
|
||||
}
|
||||
|
||||
v4Internal.SanitizeHostForHeader(req)
|
||||
|
||||
credentialScope := s.buildCredentialScope()
|
||||
credentialStr := s.Credentials.AccessKeyID + "/" + credentialScope
|
||||
if s.IsPreSign {
|
||||
query.Set(v4Internal.AmzCredentialKey, credentialStr)
|
||||
}
|
||||
|
||||
unsignedHeaders := headers
|
||||
if s.IsPreSign && !s.DisableHeaderHoisting {
|
||||
var urlValues url.Values
|
||||
urlValues, unsignedHeaders = buildQuery(v4Internal.AllowedQueryHoisting, headers)
|
||||
for k := range urlValues {
|
||||
query[k] = urlValues[k]
|
||||
}
|
||||
}
|
||||
|
||||
host := req.URL.Host
|
||||
if len(req.Host) > 0 {
|
||||
host = req.Host
|
||||
}
|
||||
|
||||
var (
|
||||
signedHeaders http.Header
|
||||
signedHeadersStr string
|
||||
canonicalHeaderStr string
|
||||
)
|
||||
|
||||
if s.IsPreSign {
|
||||
signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredPresignedHeaders, unsignedHeaders, s.Request.ContentLength)
|
||||
} else {
|
||||
signedHeaders, signedHeadersStr, canonicalHeaderStr = s.buildCanonicalHeaders(host, v4Internal.IgnoredHeaders, unsignedHeaders, s.Request.ContentLength)
|
||||
}
|
||||
|
||||
if s.IsPreSign {
|
||||
query.Set(v4Internal.AmzSignedHeadersKey, signedHeadersStr)
|
||||
}
|
||||
|
||||
var rawQuery strings.Builder
|
||||
rawQuery.WriteString(strings.Replace(query.Encode(), "+", "%20", -1))
|
||||
|
||||
canonicalURI := v4Internal.GetURIPath(req.URL)
|
||||
if !s.DisableURIPathEscaping {
|
||||
canonicalURI = httpbinding.EscapePath(canonicalURI, false)
|
||||
}
|
||||
|
||||
canonicalString := s.buildCanonicalString(
|
||||
req.Method,
|
||||
canonicalURI,
|
||||
rawQuery.String(),
|
||||
signedHeadersStr,
|
||||
canonicalHeaderStr,
|
||||
)
|
||||
|
||||
strToSign := s.buildStringToSign(credentialScope, canonicalString)
|
||||
signingSignature, err := s.buildSignature(strToSign)
|
||||
if err != nil {
|
||||
return signedRequest{}, err
|
||||
}
|
||||
|
||||
if s.IsPreSign {
|
||||
rawQuery.WriteString("&X-Amz-Signature=")
|
||||
rawQuery.WriteString(signingSignature)
|
||||
} else {
|
||||
headers[authorizationHeader] = append(headers[authorizationHeader][:0], buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature))
|
||||
}
|
||||
|
||||
req.URL.RawQuery = rawQuery.String()
|
||||
|
||||
return signedRequest{
|
||||
Request: req,
|
||||
SignedHeaders: signedHeaders,
|
||||
CanonicalString: canonicalString,
|
||||
StringToSign: strToSign,
|
||||
PreSigned: s.IsPreSign,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildAuthorizationHeader(credentialStr, signedHeadersStr, signingSignature string) string {
|
||||
const credential = "Credential="
|
||||
const signedHeaders = "SignedHeaders="
|
||||
const signature = "Signature="
|
||||
const commaSpace = ", "
|
||||
|
||||
var parts strings.Builder
|
||||
parts.Grow(len(signingAlgorithm) + 1 +
|
||||
len(credential) + len(credentialStr) + 2 +
|
||||
len(signedHeaders) + len(signedHeadersStr) + 2 +
|
||||
len(signature) + len(signingSignature),
|
||||
)
|
||||
parts.WriteString(signingAlgorithm)
|
||||
parts.WriteRune(' ')
|
||||
parts.WriteString(credential)
|
||||
parts.WriteString(credentialStr)
|
||||
parts.WriteString(commaSpace)
|
||||
parts.WriteString(signedHeaders)
|
||||
parts.WriteString(signedHeadersStr)
|
||||
parts.WriteString(commaSpace)
|
||||
parts.WriteString(signature)
|
||||
parts.WriteString(signingSignature)
|
||||
return parts.String()
|
||||
}
|
||||
|
||||
// SignHTTP signs AWS v4 requests with the provided payload hash, service name, region the
|
||||
// request is made to, and time the request is signed at. The signTime allows
|
||||
// you to specify that a request is signed for the future, and cannot be
|
||||
// used until then.
|
||||
//
|
||||
// The payloadHash is the hex encoded SHA-256 hash of the request payload, and
|
||||
// must be provided. Even if the request has no payload (aka body). If the
|
||||
// request has no payload you should use the hex encoded SHA-256 of an empty
|
||||
// string as the payloadHash value.
|
||||
//
|
||||
// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
//
|
||||
// Some services such as Amazon S3 accept alternative values for the payload
|
||||
// hash, such as "UNSIGNED-PAYLOAD" for requests where the body will not be
|
||||
// included in the request signature.
|
||||
//
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
|
||||
//
|
||||
// Sign differs from Presign in that it will sign the request using HTTP
|
||||
// header values. This type of signing is intended for http.Request values that
|
||||
// will not be shared, or are shared in a way the header values on the request
|
||||
// will not be lost.
|
||||
//
|
||||
// The passed in request will be modified in place.
|
||||
func (s Signer) SignHTTP(ctx context.Context, credentials aws.Credentials, r *http.Request, payloadHash string, service string, region string, signingTime time.Time, optFns ...func(options *SignerOptions)) error {
|
||||
options := s.options
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(&options)
|
||||
}
|
||||
|
||||
signer := &httpSigner{
|
||||
Request: r,
|
||||
PayloadHash: payloadHash,
|
||||
ServiceName: service,
|
||||
Region: region,
|
||||
Credentials: credentials,
|
||||
Time: v4Internal.NewSigningTime(signingTime.UTC()),
|
||||
DisableHeaderHoisting: options.DisableHeaderHoisting,
|
||||
DisableURIPathEscaping: options.DisableURIPathEscaping,
|
||||
DisableSessionToken: options.DisableSessionToken,
|
||||
KeyDerivator: s.keyDerivator,
|
||||
}
|
||||
|
||||
signedRequest, err := signer.Build()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logSigningInfo(ctx, options, &signedRequest, false)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// PresignHTTP signs AWS v4 requests with the payload hash, service name, region
|
||||
// the request is made to, and time the request is signed at. The signTime
|
||||
// allows you to specify that a request is signed for the future, and cannot
|
||||
// be used until then.
|
||||
//
|
||||
// Returns the signed URL and the map of HTTP headers that were included in the
|
||||
// signature or an error if signing the request failed. For presigned requests
|
||||
// these headers and their values must be included on the HTTP request when it
|
||||
// is made. This is helpful to know what header values need to be shared with
|
||||
// the party the presigned request will be distributed to.
|
||||
//
|
||||
// The payloadHash is the hex encoded SHA-256 hash of the request payload, and
|
||||
// must be provided. Even if the request has no payload (aka body). If the
|
||||
// request has no payload you should use the hex encoded SHA-256 of an empty
|
||||
// string as the payloadHash value.
|
||||
//
|
||||
// "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
|
||||
//
|
||||
// Some services such as Amazon S3 accept alternative values for the payload
|
||||
// hash, such as "UNSIGNED-PAYLOAD" for requests where the body will not be
|
||||
// included in the request signature.
|
||||
//
|
||||
// https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-header-based-auth.html
|
||||
//
|
||||
// PresignHTTP differs from SignHTTP in that it will sign the request using
|
||||
// query string instead of header values. This allows you to share the
|
||||
// Presigned Request's URL with third parties, or distribute it throughout your
|
||||
// system with minimal dependencies.
|
||||
//
|
||||
// PresignHTTP will not set the expires time of the presigned request
|
||||
// automatically. To specify the expire duration for a request add the
|
||||
// "X-Amz-Expires" query parameter on the request with the value as the
|
||||
// duration in seconds the presigned URL should be considered valid for. This
|
||||
// parameter is not used by all AWS services, and is most notable used by
|
||||
// Amazon S3 APIs.
|
||||
//
|
||||
// expires := 20 * time.Minute
|
||||
// query := req.URL.Query()
|
||||
// query.Set("X-Amz-Expires", strconv.FormatInt(int64(expires/time.Second), 10))
|
||||
// req.URL.RawQuery = query.Encode()
|
||||
//
|
||||
// This method does not modify the provided request.
|
||||
func (s *Signer) PresignHTTP(
|
||||
ctx context.Context, credentials aws.Credentials, r *http.Request,
|
||||
payloadHash string, service string, region string, signingTime time.Time,
|
||||
optFns ...func(*SignerOptions),
|
||||
) (signedURI string, signedHeaders http.Header, err error) {
|
||||
options := s.options
|
||||
|
||||
for _, fn := range optFns {
|
||||
fn(&options)
|
||||
}
|
||||
|
||||
signer := &httpSigner{
|
||||
Request: r.Clone(r.Context()),
|
||||
PayloadHash: payloadHash,
|
||||
ServiceName: service,
|
||||
Region: region,
|
||||
Credentials: credentials,
|
||||
Time: v4Internal.NewSigningTime(signingTime.UTC()),
|
||||
IsPreSign: true,
|
||||
DisableHeaderHoisting: options.DisableHeaderHoisting,
|
||||
DisableURIPathEscaping: options.DisableURIPathEscaping,
|
||||
DisableSessionToken: options.DisableSessionToken,
|
||||
KeyDerivator: s.keyDerivator,
|
||||
}
|
||||
|
||||
signedRequest, err := signer.Build()
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
logSigningInfo(ctx, options, &signedRequest, true)
|
||||
|
||||
signedHeaders = make(http.Header)
|
||||
|
||||
// For the signed headers we canonicalize the header keys in the returned map.
|
||||
// This avoids situations where can standard library double headers like host header. For example the standard
|
||||
// library will set the Host header, even if it is present in lower-case form.
|
||||
for k, v := range signedRequest.SignedHeaders {
|
||||
key := textproto.CanonicalMIMEHeaderKey(k)
|
||||
signedHeaders[key] = append(signedHeaders[key], v...)
|
||||
}
|
||||
|
||||
return signedRequest.Request.URL.String(), signedHeaders, nil
|
||||
}
|
||||
|
||||
func (s *httpSigner) buildCredentialScope() string {
|
||||
return v4Internal.BuildCredentialScope(s.Time, s.Region, s.ServiceName)
|
||||
}
|
||||
|
||||
func buildQuery(r v4Internal.Rule, header http.Header) (url.Values, http.Header) {
|
||||
query := url.Values{}
|
||||
unsignedHeaders := http.Header{}
|
||||
|
||||
// A list of headers to be converted to lower case to mitigate a limitation from S3
|
||||
lowerCaseHeaders := map[string]string{
|
||||
"X-Amz-Expected-Bucket-Owner": "x-amz-expected-bucket-owner", // see #2508
|
||||
"X-Amz-Request-Payer": "x-amz-request-payer", // see #2764
|
||||
}
|
||||
|
||||
for k, h := range header {
|
||||
if newKey, ok := lowerCaseHeaders[k]; ok {
|
||||
k = newKey
|
||||
}
|
||||
|
||||
if r.IsValid(k) {
|
||||
query[k] = h
|
||||
} else {
|
||||
unsignedHeaders[k] = h
|
||||
}
|
||||
}
|
||||
|
||||
return query, unsignedHeaders
|
||||
}
|
||||
|
||||
func (s *httpSigner) buildCanonicalHeaders(host string, rule v4Internal.Rule, header http.Header, length int64) (signed http.Header, signedHeaders, canonicalHeadersStr string) {
|
||||
signed = make(http.Header)
|
||||
|
||||
var headers []string
|
||||
const hostHeader = "host"
|
||||
headers = append(headers, hostHeader)
|
||||
signed[hostHeader] = append(signed[hostHeader], host)
|
||||
|
||||
//const contentLengthHeader = "content-length"
|
||||
//if length > 0 {
|
||||
// headers = append(headers, contentLengthHeader)
|
||||
// signed[contentLengthHeader] = append(signed[contentLengthHeader], strconv.FormatInt(length, 10))
|
||||
//}
|
||||
|
||||
for k, v := range header {
|
||||
if !rule.IsValid(k) {
|
||||
continue // ignored header
|
||||
}
|
||||
//if strings.EqualFold(k, contentLengthHeader) {
|
||||
// // prevent signing already handled content-length header.
|
||||
// continue
|
||||
//}
|
||||
|
||||
lowerCaseKey := strings.ToLower(k)
|
||||
if _, ok := signed[lowerCaseKey]; ok {
|
||||
// include additional values
|
||||
signed[lowerCaseKey] = append(signed[lowerCaseKey], v...)
|
||||
continue
|
||||
}
|
||||
|
||||
headers = append(headers, lowerCaseKey)
|
||||
signed[lowerCaseKey] = v
|
||||
}
|
||||
sort.Strings(headers)
|
||||
|
||||
signedHeaders = strings.Join(headers, ";")
|
||||
|
||||
var canonicalHeaders strings.Builder
|
||||
n := len(headers)
|
||||
const colon = ':'
|
||||
for i := 0; i < n; i++ {
|
||||
if headers[i] == hostHeader {
|
||||
canonicalHeaders.WriteString(hostHeader)
|
||||
canonicalHeaders.WriteRune(colon)
|
||||
canonicalHeaders.WriteString(v4Internal.StripExcessSpaces(host))
|
||||
} else {
|
||||
canonicalHeaders.WriteString(headers[i])
|
||||
canonicalHeaders.WriteRune(colon)
|
||||
// Trim out leading, trailing, and dedup inner spaces from signed header values.
|
||||
values := signed[headers[i]]
|
||||
for j, v := range values {
|
||||
cleanedValue := strings.TrimSpace(v4Internal.StripExcessSpaces(v))
|
||||
canonicalHeaders.WriteString(cleanedValue)
|
||||
if j < len(values)-1 {
|
||||
canonicalHeaders.WriteRune(',')
|
||||
}
|
||||
}
|
||||
}
|
||||
canonicalHeaders.WriteRune('\n')
|
||||
}
|
||||
canonicalHeadersStr = canonicalHeaders.String()
|
||||
|
||||
return signed, signedHeaders, canonicalHeadersStr
|
||||
}
|
||||
|
||||
func (s *httpSigner) buildCanonicalString(method, uri, query, signedHeaders, canonicalHeaders string) string {
|
||||
return strings.Join([]string{
|
||||
method,
|
||||
uri,
|
||||
query,
|
||||
canonicalHeaders,
|
||||
signedHeaders,
|
||||
s.PayloadHash,
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func (s *httpSigner) buildStringToSign(credentialScope, canonicalRequestString string) string {
|
||||
return strings.Join([]string{
|
||||
signingAlgorithm,
|
||||
s.Time.TimeFormat(),
|
||||
credentialScope,
|
||||
hex.EncodeToString(makeHash(sha256.New(), []byte(canonicalRequestString))),
|
||||
}, "\n")
|
||||
}
|
||||
|
||||
func makeHash(hash hash.Hash, b []byte) []byte {
|
||||
hash.Reset()
|
||||
hash.Write(b)
|
||||
return hash.Sum(nil)
|
||||
}
|
||||
|
||||
func (s *httpSigner) buildSignature(strToSign string) (string, error) {
|
||||
key := s.KeyDerivator.DeriveKey(s.Credentials, s.ServiceName, s.Region, s.Time)
|
||||
return hex.EncodeToString(v4Internal.HMACSHA256(key, []byte(strToSign))), nil
|
||||
}
|
||||
|
||||
func (s *httpSigner) setRequiredSigningFields(headers http.Header, query url.Values) {
|
||||
amzDate := s.Time.TimeFormat()
|
||||
|
||||
if s.IsPreSign {
|
||||
query.Set(v4Internal.AmzAlgorithmKey, signingAlgorithm)
|
||||
sessionToken := s.Credentials.SessionToken
|
||||
if !s.DisableSessionToken && len(sessionToken) > 0 {
|
||||
query.Set("X-Amz-Security-Token", sessionToken)
|
||||
}
|
||||
|
||||
query.Set(v4Internal.AmzDateKey, amzDate)
|
||||
return
|
||||
}
|
||||
|
||||
headers[v4Internal.AmzDateKey] = append(headers[v4Internal.AmzDateKey][:0], amzDate)
|
||||
|
||||
if !s.DisableSessionToken && len(s.Credentials.SessionToken) > 0 {
|
||||
headers[v4Internal.AmzSecurityTokenKey] = append(headers[v4Internal.AmzSecurityTokenKey][:0], s.Credentials.SessionToken)
|
||||
}
|
||||
}
|
||||
|
||||
func logSigningInfo(_ context.Context, options SignerOptions, request *signedRequest, isPresign bool) {
|
||||
if !options.LogSigning {
|
||||
return
|
||||
}
|
||||
signedURLMsg := ""
|
||||
if isPresign {
|
||||
signedURLMsg = fmt.Sprintf(logSignedURLMsg, request.Request.URL.String())
|
||||
}
|
||||
|
||||
if options.Logger != nil {
|
||||
options.Logger.Debug(fmt.Sprintf(logSignInfoMsg, request.CanonicalString, request.StringToSign, signedURLMsg))
|
||||
}
|
||||
}
|
||||
|
||||
type signedRequest struct {
|
||||
Request *http.Request
|
||||
SignedHeaders http.Header
|
||||
CanonicalString string
|
||||
StringToSign string
|
||||
PreSigned bool
|
||||
}
|
||||
|
||||
const logSignInfoMsg = `Request Signature:
|
||||
---[ CANONICAL STRING ]-----------------------------
|
||||
%s
|
||||
---[ STRING TO SIGN ]--------------------------------
|
||||
%s%s
|
||||
-----------------------------------------------------`
|
||||
const logSignedURLMsg = `
|
||||
---[ SIGNED URL ]------------------------------------
|
||||
%s`
|
370
api/auth/signer/v4sdk2/signer/v4/v4_test.go
Normal file
370
api/auth/signer/v4sdk2/signer/v4/v4_test.go
Normal file
|
@ -0,0 +1,370 @@
|
|||
// This is https://github.com/aws/aws-sdk-go-v2/blob/a2b751d1ba71f59175a41f9cae5f159f1044360f/aws/signer/v4/v4_test.go
|
||||
// with changes:
|
||||
// * don't duplicate content-length as signed header
|
||||
|
||||
package v4
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
v4Internal "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/auth/signer/v4sdk2/signer/internal/v4"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
)
|
||||
|
||||
var testCredentials = aws.Credentials{AccessKeyID: "AKID", SecretAccessKey: "SECRET", SessionToken: "SESSION"}
|
||||
|
||||
func buildRequest(serviceName, region, body string) (*http.Request, string) {
|
||||
reader := strings.NewReader(body)
|
||||
return buildRequestWithBodyReader(serviceName, region, reader)
|
||||
}
|
||||
|
||||
func buildRequestWithBodyReader(serviceName, region string, body io.Reader) (*http.Request, string) {
|
||||
var bodyLen int
|
||||
|
||||
type lenner interface {
|
||||
Len() int
|
||||
}
|
||||
if lr, ok := body.(lenner); ok {
|
||||
bodyLen = lr.Len()
|
||||
}
|
||||
|
||||
endpoint := "https://" + serviceName + "." + region + ".amazonaws.com"
|
||||
req, _ := http.NewRequest("POST", endpoint, body)
|
||||
req.URL.Opaque = "//example.org/bucket/key-._~,!@#$%^&*()"
|
||||
req.Header.Set("X-Amz-Target", "prefix.Operation")
|
||||
req.Header.Set("Content-Type", "application/x-amz-json-1.0")
|
||||
|
||||
if bodyLen > 0 {
|
||||
req.ContentLength = int64(bodyLen)
|
||||
}
|
||||
|
||||
req.Header.Set("X-Amz-Meta-Other-Header", "some-value=!@#$%^&* (+)")
|
||||
req.Header.Add("X-Amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)")
|
||||
req.Header.Add("X-amz-Meta-Other-Header_With_Underscore", "some-value=!@#$%^&* (+)")
|
||||
|
||||
h := sha256.New()
|
||||
_, _ = io.Copy(h, body)
|
||||
payloadHash := hex.EncodeToString(h.Sum(nil))
|
||||
|
||||
return req, payloadHash
|
||||
}
|
||||
|
||||
func TestPresignRequest(t *testing.T) {
|
||||
req, body := buildRequest("dynamodb", "us-east-1", "{}")
|
||||
req.Header.Set("Content-Length", "2")
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Set("X-Amz-Expires", "300")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
signer := NewSigner()
|
||||
signed, headers, err := signer.PresignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0))
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
expectedDate := "19700101T000000Z"
|
||||
expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore"
|
||||
expectedSig := "122f0b9e091e4ba84286097e2b3404a1f1f4c4aad479adda95b7dff0ccbe5581"
|
||||
expectedCred := "AKID/19700101/us-east-1/dynamodb/aws4_request"
|
||||
expectedTarget := "prefix.Operation"
|
||||
|
||||
q, err := url.ParseQuery(signed[strings.Index(signed, "?"):])
|
||||
if err != nil {
|
||||
t.Errorf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
if e, a := expectedSig, q.Get("X-Amz-Signature"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedCred, q.Get("X-Amz-Credential"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 {
|
||||
t.Errorf("expect %v to be empty", a)
|
||||
}
|
||||
if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
|
||||
for _, h := range strings.Split(expectedHeaders, ";") {
|
||||
v := headers.Get(h)
|
||||
if len(v) == 0 {
|
||||
t.Errorf("expect %v, to be present in header map", h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPresignBodyWithArrayRequest(t *testing.T) {
|
||||
req, body := buildRequest("dynamodb", "us-east-1", "{}")
|
||||
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||||
req.Header.Set("Content-Length", "2")
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Set("X-Amz-Expires", "300")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
signer := NewSigner()
|
||||
signed, headers, err := signer.PresignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0))
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
q, err := url.ParseQuery(signed[strings.Index(signed, "?"):])
|
||||
if err != nil {
|
||||
t.Errorf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
expectedDate := "19700101T000000Z"
|
||||
expectedHeaders := "content-length;content-type;host;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore"
|
||||
expectedSig := "e3ac55addee8711b76c6d608d762cff285fe8b627a057f8b5ec9268cf82c08b1"
|
||||
expectedCred := "AKID/19700101/us-east-1/dynamodb/aws4_request"
|
||||
expectedTarget := "prefix.Operation"
|
||||
|
||||
if e, a := expectedSig, q.Get("X-Amz-Signature"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedCred, q.Get("X-Amz-Credential"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedHeaders, q.Get("X-Amz-SignedHeaders"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if a := q.Get("X-Amz-Meta-Other-Header"); len(a) != 0 {
|
||||
t.Errorf("expect %v to be empty, was not", a)
|
||||
}
|
||||
if e, a := expectedTarget, q.Get("X-Amz-Target"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
|
||||
for _, h := range strings.Split(expectedHeaders, ";") {
|
||||
v := headers.Get(h)
|
||||
if len(v) == 0 {
|
||||
t.Errorf("expect %v, to be present in header map", h)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignRequest(t *testing.T) {
|
||||
req, body := buildRequest("dynamodb", "us-east-1", "{}")
|
||||
req.Header.Set("Content-Length", "2")
|
||||
signer := NewSigner()
|
||||
err := signer.SignHTTP(context.Background(), testCredentials, req, body, "dynamodb", "us-east-1", time.Unix(0, 0))
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
expectedDate := "19700101T000000Z"
|
||||
expectedSig := "AWS4-HMAC-SHA256 Credential=AKID/19700101/us-east-1/dynamodb/aws4_request, SignedHeaders=content-length;content-type;host;x-amz-date;x-amz-meta-other-header;x-amz-meta-other-header_with_underscore;x-amz-security-token;x-amz-target, Signature=a518299330494908a70222cec6899f6f32f297f8595f6df1776d998936652ad9"
|
||||
|
||||
q := req.Header
|
||||
if e, a := expectedSig, q.Get("Authorization"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
if e, a := expectedDate, q.Get("X-Amz-Date"); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCanonicalRequest(t *testing.T) {
|
||||
req, _ := buildRequest("dynamodb", "us-east-1", "{}")
|
||||
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||||
|
||||
ctx := &httpSigner{
|
||||
ServiceName: "dynamodb",
|
||||
Region: "us-east-1",
|
||||
Request: req,
|
||||
Time: v4Internal.NewSigningTime(time.Now()),
|
||||
KeyDerivator: v4Internal.NewSigningKeyDeriver(),
|
||||
}
|
||||
|
||||
build, err := ctx.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
expected := "https://example.org/bucket/key-._~,!@#$%^&*()?Foo=a&Foo=m&Foo=o&Foo=z"
|
||||
if e, a := expected, build.Request.URL.String(); e != a {
|
||||
t.Errorf("expect %v, got %v", e, a)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSigner_SignHTTP_NoReplaceRequestBody(t *testing.T) {
|
||||
req, bodyHash := buildRequest("dynamodb", "us-east-1", "{}")
|
||||
req.Body = ioutil.NopCloser(bytes.NewReader([]byte{}))
|
||||
|
||||
s := NewSigner()
|
||||
|
||||
origBody := req.Body
|
||||
|
||||
err := s.SignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now())
|
||||
if err != nil {
|
||||
t.Fatalf("expect no error, got %v", err)
|
||||
}
|
||||
|
||||
if req.Body != origBody {
|
||||
t.Errorf("expect request body to not be chagned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestHost(t *testing.T) {
|
||||
req, _ := buildRequest("dynamodb", "us-east-1", "{}")
|
||||
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||||
req.Host = "myhost"
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Set("X-Amz-Expires", "5")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
ctx := &httpSigner{
|
||||
ServiceName: "dynamodb",
|
||||
Region: "us-east-1",
|
||||
Request: req,
|
||||
Time: v4Internal.NewSigningTime(time.Now()),
|
||||
KeyDerivator: v4Internal.NewSigningKeyDeriver(),
|
||||
}
|
||||
|
||||
build, err := ctx.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(build.CanonicalString, "host:"+req.Host) {
|
||||
t.Errorf("canonical host header invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSign_buildCanonicalHeadersContentLengthPresent(t *testing.T) {
|
||||
body := `{"description": "this is a test"}`
|
||||
req, _ := buildRequest("dynamodb", "us-east-1", body)
|
||||
req.URL.RawQuery = "Foo=z&Foo=o&Foo=m&Foo=a"
|
||||
req.Host = "myhost"
|
||||
|
||||
contentLength := fmt.Sprintf("%d", len([]byte(body)))
|
||||
req.Header.Add("Content-Length", contentLength)
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Set("X-Amz-Expires", "5")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
ctx := &httpSigner{
|
||||
ServiceName: "dynamodb",
|
||||
Region: "us-east-1",
|
||||
Request: req,
|
||||
Time: v4Internal.NewSigningTime(time.Now()),
|
||||
KeyDerivator: v4Internal.NewSigningKeyDeriver(),
|
||||
}
|
||||
|
||||
build, err := ctx.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
if !strings.Contains(build.CanonicalString, "content-length:"+contentLength+"\n") {
|
||||
t.Errorf("canonical header content-length invalid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSign_buildCanonicalHeaders(t *testing.T) {
|
||||
serviceName := "mockAPI"
|
||||
region := "mock-region"
|
||||
endpoint := "https://" + serviceName + "." + region + ".amazonaws.com"
|
||||
|
||||
req, err := http.NewRequest("POST", endpoint, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to create request, %v", err)
|
||||
}
|
||||
|
||||
req.Header.Set("FooInnerSpace", " inner space ")
|
||||
req.Header.Set("FooLeadingSpace", " leading-space")
|
||||
req.Header.Add("FooMultipleSpace", "no-space")
|
||||
req.Header.Add("FooMultipleSpace", "\ttab-space")
|
||||
req.Header.Add("FooMultipleSpace", "trailing-space ")
|
||||
req.Header.Set("FooNoSpace", "no-space")
|
||||
req.Header.Set("FooTabSpace", "\ttab-space\t")
|
||||
req.Header.Set("FooTrailingSpace", "trailing-space ")
|
||||
req.Header.Set("FooWrappedSpace", " wrapped-space ")
|
||||
|
||||
ctx := &httpSigner{
|
||||
ServiceName: serviceName,
|
||||
Region: region,
|
||||
Request: req,
|
||||
Time: v4Internal.NewSigningTime(time.Date(2021, 10, 20, 12, 42, 0, 0, time.UTC)),
|
||||
KeyDerivator: v4Internal.NewSigningKeyDeriver(),
|
||||
}
|
||||
|
||||
build, err := ctx.Build()
|
||||
if err != nil {
|
||||
t.Fatalf("expected no error, got %v", err)
|
||||
}
|
||||
|
||||
expectCanonicalString := strings.Join([]string{
|
||||
`POST`,
|
||||
`/`,
|
||||
``,
|
||||
`fooinnerspace:inner space`,
|
||||
`fooleadingspace:leading-space`,
|
||||
`foomultiplespace:no-space,tab-space,trailing-space`,
|
||||
`foonospace:no-space`,
|
||||
`footabspace:tab-space`,
|
||||
`footrailingspace:trailing-space`,
|
||||
`foowrappedspace:wrapped-space`,
|
||||
`host:mockAPI.mock-region.amazonaws.com`,
|
||||
`x-amz-date:20211020T124200Z`,
|
||||
``,
|
||||
`fooinnerspace;fooleadingspace;foomultiplespace;foonospace;footabspace;footrailingspace;foowrappedspace;host;x-amz-date`,
|
||||
``,
|
||||
}, "\n")
|
||||
if diff := cmpDiff(expectCanonicalString, build.CanonicalString); diff != "" {
|
||||
t.Errorf("expect match, got\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkPresignRequest(b *testing.B) {
|
||||
signer := NewSigner()
|
||||
req, bodyHash := buildRequest("dynamodb", "us-east-1", "{}")
|
||||
|
||||
query := req.URL.Query()
|
||||
query.Set("X-Amz-Expires", "5")
|
||||
req.URL.RawQuery = query.Encode()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
signer.PresignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkSignRequest(b *testing.B) {
|
||||
signer := NewSigner()
|
||||
req, bodyHash := buildRequest("dynamodb", "us-east-1", "{}")
|
||||
for i := 0; i < b.N; i++ {
|
||||
signer.SignHTTP(context.Background(), testCredentials, req, bodyHash, "dynamodb", "us-east-1", time.Now())
|
||||
}
|
||||
}
|
||||
|
||||
func cmpDiff(e, a interface{}) string {
|
||||
if !reflect.DeepEqual(e, a) {
|
||||
return fmt.Sprintf("%v != %v", e, a)
|
||||
}
|
||||
return ""
|
||||
}
|
2
api/cache/access_control.go
vendored
2
api/cache/access_control.go
vendored
|
@ -48,7 +48,7 @@ func (o *AccessControlCache) Get(owner user.ID, key string) bool {
|
|||
result, ok := entry.(bool)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
31
api/cache/accessbox.go
vendored
31
api/cache/accessbox.go
vendored
|
@ -6,6 +6,7 @@ import (
|
|||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/bluele/gcache"
|
||||
"go.uber.org/zap"
|
||||
|
@ -24,6 +25,13 @@ type (
|
|||
Lifetime time.Duration
|
||||
Logger *zap.Logger
|
||||
}
|
||||
|
||||
AccessBoxCacheValue struct {
|
||||
Box *accessbox.Box
|
||||
Attributes []object.Attribute
|
||||
PutTime time.Time
|
||||
Address *oid.Address
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -42,31 +50,36 @@ func DefaultAccessBoxConfig(logger *zap.Logger) *Config {
|
|||
}
|
||||
}
|
||||
|
||||
// NewAccessBoxCache creates an object of BucketCache.
|
||||
// NewAccessBoxCache creates an object of AccessBoxCache.
|
||||
func NewAccessBoxCache(config *Config) *AccessBoxCache {
|
||||
gc := gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build()
|
||||
|
||||
return &AccessBoxCache{cache: gc, logger: config.Logger}
|
||||
}
|
||||
|
||||
// Get returns a cached object.
|
||||
func (o *AccessBoxCache) Get(address oid.Address) *accessbox.Box {
|
||||
entry, err := o.cache.Get(address)
|
||||
// Get returns a cached accessbox.
|
||||
func (o *AccessBoxCache) Get(accessKeyID string) *AccessBoxCacheValue {
|
||||
entry, err := o.cache.Get(accessKeyID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, ok := entry.(*accessbox.Box)
|
||||
result, ok := entry.(*AccessBoxCacheValue)
|
||||
if !ok {
|
||||
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 result
|
||||
}
|
||||
|
||||
// Put stores an object to cache.
|
||||
func (o *AccessBoxCache) Put(address oid.Address, box *accessbox.Box) error {
|
||||
return o.cache.Set(address, box)
|
||||
// Put stores an accessbox to cache.
|
||||
func (o *AccessBoxCache) Put(accessKeyID string, val *AccessBoxCacheValue) error {
|
||||
return o.cache.Set(accessKeyID, val)
|
||||
}
|
||||
|
||||
// Delete removes an accessbox from cache.
|
||||
func (o *AccessBoxCache) Delete(accessKeyID string) {
|
||||
o.cache.Remove(accessKeyID)
|
||||
}
|
||||
|
|
67
api/cache/buckets.go
vendored
67
api/cache/buckets.go
vendored
|
@ -6,14 +6,16 @@ import (
|
|||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/bluele/gcache"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// BucketCache contains cache with objects and the lifetime of cache entries.
|
||||
type BucketCache struct {
|
||||
cache gcache.Cache
|
||||
logger *zap.Logger
|
||||
cache gcache.Cache
|
||||
cidCache gcache.Cache
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -33,13 +35,44 @@ func DefaultBucketConfig(logger *zap.Logger) *Config {
|
|||
}
|
||||
|
||||
// NewBucketCache creates an object of BucketCache.
|
||||
func NewBucketCache(config *Config) *BucketCache {
|
||||
gc := gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build()
|
||||
return &BucketCache{cache: gc, logger: config.Logger}
|
||||
func NewBucketCache(config *Config, cidCache bool) *BucketCache {
|
||||
cache := &BucketCache{
|
||||
cache: gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build(),
|
||||
logger: config.Logger,
|
||||
}
|
||||
|
||||
if cidCache {
|
||||
cache.cidCache = gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build()
|
||||
}
|
||||
return cache
|
||||
}
|
||||
|
||||
// Get returns a cached object.
|
||||
func (o *BucketCache) Get(key string) *data.BucketInfo {
|
||||
func (o *BucketCache) Get(ns, bktName string) *data.BucketInfo {
|
||||
return o.get(formKey(ns, bktName))
|
||||
}
|
||||
|
||||
func (o *BucketCache) GetByCID(cnrID cid.ID) *data.BucketInfo {
|
||||
if o.cidCache == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
entry, err := o.cidCache.Get(cnrID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
key, ok := entry.(string)
|
||||
if !ok {
|
||||
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
|
||||
zap.String("expected", fmt.Sprintf("%T", key)), logs.TagField(logs.TagDatapath))
|
||||
return nil
|
||||
}
|
||||
|
||||
return o.get(key)
|
||||
}
|
||||
|
||||
func (o *BucketCache) get(key string) *data.BucketInfo {
|
||||
entry, err := o.cache.Get(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
|
@ -48,7 +81,7 @@ func (o *BucketCache) Get(key string) *data.BucketInfo {
|
|||
result, ok := entry.(*data.BucketInfo)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -57,10 +90,24 @@ func (o *BucketCache) Get(key string) *data.BucketInfo {
|
|||
|
||||
// Put puts an object to cache.
|
||||
func (o *BucketCache) Put(bkt *data.BucketInfo) error {
|
||||
return o.cache.Set(bkt.Name, bkt)
|
||||
if o.cidCache != nil {
|
||||
if err := o.cidCache.Set(bkt.CID, formKey(bkt.Zone, bkt.Name)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return o.cache.Set(formKey(bkt.Zone, bkt.Name), bkt)
|
||||
}
|
||||
|
||||
// Delete deletes an object from cache.
|
||||
func (o *BucketCache) Delete(key string) bool {
|
||||
return o.cache.Remove(key)
|
||||
func (o *BucketCache) Delete(bkt *data.BucketInfo) bool {
|
||||
if o.cidCache != nil {
|
||||
o.cidCache.Remove(bkt.CID)
|
||||
}
|
||||
|
||||
return o.cache.Remove(formKey(bkt.Zone, bkt.Name))
|
||||
}
|
||||
|
||||
func formKey(zone, name string) string {
|
||||
return name + "." + zone
|
||||
}
|
||||
|
|
70
api/cache/cache_test.go
vendored
70
api/cache/cache_test.go
vendored
|
@ -1,12 +1,17 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/creds/accessbox"
|
||||
cidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id/test"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
oidtest "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id/test"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"github.com/stretchr/testify/require"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zaptest/observer"
|
||||
|
@ -18,33 +23,38 @@ func TestAccessBoxCacheType(t *testing.T) {
|
|||
|
||||
addr := oidtest.Address()
|
||||
box := &accessbox.Box{}
|
||||
val := &AccessBoxCacheValue{
|
||||
Box: box,
|
||||
}
|
||||
|
||||
err := cache.Put(addr, box)
|
||||
accessKeyID := getAccessKeyID(addr)
|
||||
|
||||
err := cache.Put(accessKeyID, val)
|
||||
require.NoError(t, err)
|
||||
val := cache.Get(addr)
|
||||
require.Equal(t, box, val)
|
||||
resVal := cache.Get(accessKeyID)
|
||||
require.Equal(t, box, resVal.Box)
|
||||
require.Equal(t, 0, observedLog.Len())
|
||||
|
||||
err = cache.cache.Set(addr, "tmp")
|
||||
err = cache.cache.Set(accessKeyID, "tmp")
|
||||
require.NoError(t, err)
|
||||
assertInvalidCacheEntry(t, cache.Get(addr), observedLog)
|
||||
assertInvalidCacheEntry(t, cache.Get(accessKeyID), observedLog)
|
||||
}
|
||||
|
||||
func TestBucketsCacheType(t *testing.T) {
|
||||
logger, observedLog := getObservedLogger()
|
||||
cache := NewBucketCache(DefaultBucketConfig(logger))
|
||||
cache := NewBucketCache(DefaultBucketConfig(logger), false)
|
||||
|
||||
bktInfo := &data.BucketInfo{Name: "bucket"}
|
||||
|
||||
err := cache.Put(bktInfo)
|
||||
require.NoError(t, err)
|
||||
val := cache.Get(bktInfo.Name)
|
||||
val := cache.Get("", bktInfo.Name)
|
||||
require.Equal(t, bktInfo, val)
|
||||
require.Equal(t, 0, observedLog.Len())
|
||||
|
||||
err = cache.cache.Set(bktInfo.Name, "tmp")
|
||||
err = cache.cache.Set(bktInfo.Name+"."+bktInfo.Zone, "tmp")
|
||||
require.NoError(t, err)
|
||||
assertInvalidCacheEntry(t, cache.Get(bktInfo.Name), observedLog)
|
||||
assertInvalidCacheEntry(t, cache.Get(bktInfo.Zone, bktInfo.Name), observedLog)
|
||||
}
|
||||
|
||||
func TestObjectNamesCacheType(t *testing.T) {
|
||||
|
@ -176,22 +186,42 @@ func TestSettingsCacheType(t *testing.T) {
|
|||
assertInvalidCacheEntry(t, cache.GetSettings(key), observedLog)
|
||||
}
|
||||
|
||||
func TestNotificationConfigurationCacheType(t *testing.T) {
|
||||
func TestFrostFSIDSubjectCacheType(t *testing.T) {
|
||||
logger, observedLog := getObservedLogger()
|
||||
cache := NewSystemCache(DefaultSystemConfig(logger))
|
||||
cache := NewFrostfsIDCache(DefaultFrostfsIDConfig(logger))
|
||||
|
||||
key := "key"
|
||||
notificationConfig := &data.NotificationConfiguration{}
|
||||
|
||||
err := cache.PutNotificationConfiguration(key, notificationConfig)
|
||||
key, err := util.Uint160DecodeStringLE("4ea976429703418ef00fc4912a409b6a0b973034")
|
||||
require.NoError(t, err)
|
||||
val := cache.GetNotificationConfiguration(key)
|
||||
require.Equal(t, notificationConfig, val)
|
||||
value := &client.SubjectExtended{}
|
||||
|
||||
err = cache.PutSubject(key, value)
|
||||
require.NoError(t, err)
|
||||
val := cache.GetSubject(key)
|
||||
require.Equal(t, value, val)
|
||||
require.Equal(t, 0, observedLog.Len())
|
||||
|
||||
err = cache.cache.Set(key, "tmp")
|
||||
require.NoError(t, err)
|
||||
assertInvalidCacheEntry(t, cache.GetNotificationConfiguration(key), observedLog)
|
||||
assertInvalidCacheEntry(t, cache.GetSubject(key), observedLog)
|
||||
}
|
||||
|
||||
func TestFrostFSIDUserKeyCacheType(t *testing.T) {
|
||||
logger, observedLog := getObservedLogger()
|
||||
cache := NewFrostfsIDCache(DefaultFrostfsIDConfig(logger))
|
||||
|
||||
ns, name := "ns", "name"
|
||||
value, err := keys.NewPrivateKey()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = cache.PutUserKey(ns, name, value.PublicKey())
|
||||
require.NoError(t, err)
|
||||
val := cache.GetUserKey(ns, name)
|
||||
require.Equal(t, value.PublicKey(), val)
|
||||
require.Equal(t, 0, observedLog.Len())
|
||||
|
||||
err = cache.cache.Set(ns+"/"+name, "tmp")
|
||||
require.NoError(t, err)
|
||||
assertInvalidCacheEntry(t, cache.GetUserKey(ns, name), observedLog)
|
||||
}
|
||||
|
||||
func assertInvalidCacheEntry(t *testing.T, val interface{}, observedLog *observer.ObservedLogs) {
|
||||
|
@ -204,3 +234,7 @@ func getObservedLogger() (*zap.Logger, *observer.ObservedLogs) {
|
|||
loggerCore, observedLog := observer.New(zap.WarnLevel)
|
||||
return zap.New(loggerCore), observedLog
|
||||
}
|
||||
|
||||
func getAccessKeyID(addr oid.Address) string {
|
||||
return strings.ReplaceAll(addr.EncodeToString(), "/", "0")
|
||||
}
|
||||
|
|
77
api/cache/frostfsid.go
vendored
Normal file
77
api/cache/frostfsid.go
vendored
Normal file
|
@ -0,0 +1,77 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-contract/frostfsid/client"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"github.com/bluele/gcache"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
"github.com/nspcc-dev/neo-go/pkg/util"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// FrostfsIDCache provides lru cache for frostfsid contract.
|
||||
type FrostfsIDCache struct {
|
||||
cache gcache.Cache
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultFrostfsIDCacheSize is a default maximum number of entries in cache.
|
||||
DefaultFrostfsIDCacheSize = 1e4
|
||||
// DefaultFrostfsIDCacheLifetime is a default lifetime of entries in cache.
|
||||
DefaultFrostfsIDCacheLifetime = time.Minute
|
||||
)
|
||||
|
||||
// DefaultFrostfsIDConfig returns new default cache expiration values.
|
||||
func DefaultFrostfsIDConfig(logger *zap.Logger) *Config {
|
||||
return &Config{
|
||||
Size: DefaultFrostfsIDCacheSize,
|
||||
Lifetime: DefaultFrostfsIDCacheLifetime,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// NewFrostfsIDCache creates an object of FrostfsIDCache.
|
||||
func NewFrostfsIDCache(config *Config) *FrostfsIDCache {
|
||||
gc := gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build()
|
||||
return &FrostfsIDCache{cache: gc, logger: config.Logger}
|
||||
}
|
||||
|
||||
// GetSubject returns a cached client.SubjectExtended. Returns nil if value is missing.
|
||||
func (c *FrostfsIDCache) GetSubject(key util.Uint160) *client.SubjectExtended {
|
||||
return get[client.SubjectExtended](c, key)
|
||||
}
|
||||
|
||||
// PutSubject puts a client.SubjectExtended to cache.
|
||||
func (c *FrostfsIDCache) PutSubject(key util.Uint160, subject *client.SubjectExtended) error {
|
||||
return c.cache.Set(key, subject)
|
||||
}
|
||||
|
||||
// GetUserKey returns a cached *keys.PublicKey. Returns nil if value is missing.
|
||||
func (c *FrostfsIDCache) GetUserKey(ns, name string) *keys.PublicKey {
|
||||
return get[keys.PublicKey](c, ns+"/"+name)
|
||||
}
|
||||
|
||||
// PutUserKey puts a client.SubjectExtended to cache.
|
||||
func (c *FrostfsIDCache) PutUserKey(ns, name string, userKey *keys.PublicKey) error {
|
||||
return c.cache.Set(ns+"/"+name, userKey)
|
||||
}
|
||||
|
||||
func get[T any](c *FrostfsIDCache, key any) *T {
|
||||
entry, err := c.cache.Get(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, ok := entry.(*T)
|
||||
if !ok {
|
||||
c.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
|
||||
zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
107
api/cache/listsession.go
vendored
Normal file
107
api/cache/listsession.go
vendored
Normal file
|
@ -0,0 +1,107 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"github.com/bluele/gcache"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
// ListSessionCache contains cache for list session (during pagination).
|
||||
ListSessionCache struct {
|
||||
cache gcache.Cache
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// ListSessionKey is a key to find a ListSessionCache's entry.
|
||||
ListSessionKey struct {
|
||||
cid cid.ID
|
||||
prefix string
|
||||
token string
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
// DefaultListSessionCacheLifetime is a default lifetime of entries in cache of ListObjects.
|
||||
DefaultListSessionCacheLifetime = time.Second * 60
|
||||
// DefaultListSessionCacheSize is a default size of cache of ListObjects.
|
||||
DefaultListSessionCacheSize = 100
|
||||
)
|
||||
|
||||
// DefaultListSessionConfig returns new default cache expiration values.
|
||||
func DefaultListSessionConfig(logger *zap.Logger) *Config {
|
||||
return &Config{
|
||||
Size: DefaultListSessionCacheSize,
|
||||
Lifetime: DefaultListSessionCacheLifetime,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (k *ListSessionKey) String() string {
|
||||
return k.cid.EncodeToString() + k.prefix + k.token
|
||||
}
|
||||
|
||||
// NewListSessionCache is a constructor which creates an object of ListObjectsCache with the given lifetime of entries.
|
||||
func NewListSessionCache(config *Config) *ListSessionCache {
|
||||
gc := gcache.New(config.Size).LRU().Expiration(config.Lifetime).EvictedFunc(func(_ interface{}, val interface{}) {
|
||||
session, ok := val.(*data.ListSession)
|
||||
if !ok {
|
||||
config.Logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", val)),
|
||||
zap.String("expected", fmt.Sprintf("%T", session)), logs.TagField(logs.TagDatapath))
|
||||
}
|
||||
|
||||
if !session.Acquired.Load() {
|
||||
session.Cancel()
|
||||
}
|
||||
}).Build()
|
||||
return &ListSessionCache{cache: gc, logger: config.Logger}
|
||||
}
|
||||
|
||||
// GetListSession returns a list of ObjectInfo.
|
||||
func (l *ListSessionCache) GetListSession(key ListSessionKey) *data.ListSession {
|
||||
entry, err := l.cache.Get(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, ok := entry.(*data.ListSession)
|
||||
if !ok {
|
||||
l.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
|
||||
zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// PutListSession puts a list of object versions to cache.
|
||||
func (l *ListSessionCache) PutListSession(key ListSessionKey, session *data.ListSession) error {
|
||||
s := l.GetListSession(key)
|
||||
if s != nil && s != session {
|
||||
if !s.Acquired.Load() {
|
||||
s.Cancel()
|
||||
}
|
||||
}
|
||||
return l.cache.Set(key, session)
|
||||
}
|
||||
|
||||
// DeleteListSession removes key from cache.
|
||||
func (l *ListSessionCache) DeleteListSession(key ListSessionKey) {
|
||||
l.cache.Remove(key)
|
||||
}
|
||||
|
||||
// CreateListSessionCacheKey returns ListSessionKey with the given CID, prefix and token.
|
||||
func CreateListSessionCacheKey(cnr cid.ID, prefix, token string) ListSessionKey {
|
||||
p := ListSessionKey{
|
||||
cid: cnr,
|
||||
prefix: prefix,
|
||||
token: token,
|
||||
}
|
||||
|
||||
return p
|
||||
}
|
2
api/cache/names.go
vendored
2
api/cache/names.go
vendored
|
@ -50,7 +50,7 @@ func (o *ObjectsNameCache) Get(key string) *oid.Address {
|
|||
result, ok := entry.(oid.Address)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
86
api/cache/network.go
vendored
Normal file
86
api/cache/network.go
vendored
Normal file
|
@ -0,0 +1,86 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"github.com/bluele/gcache"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
// NetworkCache provides cache for network-related values.
|
||||
NetworkCache struct {
|
||||
cache gcache.Cache
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
// NetworkCacheConfig stores expiration params for cache.
|
||||
NetworkCacheConfig struct {
|
||||
Lifetime time.Duration
|
||||
Logger *zap.Logger
|
||||
}
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultNetworkCacheLifetime = 1 * time.Minute
|
||||
networkCacheSize = 2
|
||||
networkInfoKey = "network_info"
|
||||
netmapKey = "netmap"
|
||||
)
|
||||
|
||||
// DefaultNetworkConfig returns new default cache expiration values.
|
||||
func DefaultNetworkConfig(logger *zap.Logger) *NetworkCacheConfig {
|
||||
return &NetworkCacheConfig{
|
||||
Lifetime: DefaultNetworkCacheLifetime,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// NewNetworkCache creates an object of NetworkCache.
|
||||
func NewNetworkCache(config *NetworkCacheConfig) *NetworkCache {
|
||||
gc := gcache.New(networkCacheSize).LRU().Expiration(config.Lifetime).Build()
|
||||
return &NetworkCache{cache: gc, logger: config.Logger}
|
||||
}
|
||||
|
||||
func (c *NetworkCache) GetNetworkInfo() *netmap.NetworkInfo {
|
||||
entry, err := c.cache.Get(networkInfoKey)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, ok := entry.(netmap.NetworkInfo)
|
||||
if !ok {
|
||||
c.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
|
||||
zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func (c *NetworkCache) PutNetworkInfo(info netmap.NetworkInfo) error {
|
||||
return c.cache.Set(networkInfoKey, info)
|
||||
}
|
||||
|
||||
func (c *NetworkCache) GetNetmap() *netmap.NetMap {
|
||||
entry, err := c.cache.Get(netmapKey)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, ok := entry.(netmap.NetMap)
|
||||
if !ok {
|
||||
c.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
|
||||
zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
|
||||
return nil
|
||||
}
|
||||
|
||||
return &result
|
||||
}
|
||||
|
||||
func (c *NetworkCache) PutNetmap(nm netmap.NetMap) error {
|
||||
return c.cache.Set(netmapKey, nm)
|
||||
}
|
2
api/cache/objects.go
vendored
2
api/cache/objects.go
vendored
|
@ -49,7 +49,7 @@ func (o *ObjectsCache) GetObject(address oid.Address) *data.ExtendedObjectInfo {
|
|||
result, ok := entry.(*data.ExtendedObjectInfo)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
|
|
4
api/cache/objectslist.go
vendored
4
api/cache/objectslist.go
vendored
|
@ -77,7 +77,7 @@ func (l *ObjectsListCache) GetVersions(key ObjectsListKey) []*data.NodeVersion {
|
|||
result, ok := entry.([]*data.NodeVersion)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -96,7 +96,7 @@ func (l *ObjectsListCache) CleanCacheEntriesContainingObject(objectName string,
|
|||
k, ok := key.(ObjectsListKey)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
if cnr.Equals(k.cid) && strings.HasPrefix(objectName, k.prefix) {
|
||||
|
|
72
api/cache/policy.go
vendored
Normal file
72
api/cache/policy.go
vendored
Normal file
|
@ -0,0 +1,72 @@
|
|||
package cache
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/engine"
|
||||
"github.com/bluele/gcache"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// MorphPolicyCache provides lru cache for listing policies stored in policy contract.
|
||||
type MorphPolicyCache struct {
|
||||
cache gcache.Cache
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
type MorphPolicyCacheKey struct {
|
||||
Target engine.Target
|
||||
Name chain.Name
|
||||
}
|
||||
|
||||
const (
|
||||
// DefaultMorphPolicyCacheSize is a default maximum number of entries in cache.
|
||||
DefaultMorphPolicyCacheSize = 1e4
|
||||
// DefaultMorphPolicyCacheLifetime is a default lifetime of entries in cache.
|
||||
DefaultMorphPolicyCacheLifetime = time.Minute
|
||||
)
|
||||
|
||||
// DefaultMorphPolicyConfig returns new default cache expiration values.
|
||||
func DefaultMorphPolicyConfig(logger *zap.Logger) *Config {
|
||||
return &Config{
|
||||
Size: DefaultMorphPolicyCacheSize,
|
||||
Lifetime: DefaultMorphPolicyCacheLifetime,
|
||||
Logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// NewMorphPolicyCache creates an object of MorphPolicyCache.
|
||||
func NewMorphPolicyCache(config *Config) *MorphPolicyCache {
|
||||
gc := gcache.New(config.Size).LRU().Expiration(config.Lifetime).Build()
|
||||
return &MorphPolicyCache{cache: gc, logger: config.Logger}
|
||||
}
|
||||
|
||||
// Get returns a cached object. Returns nil if value is missing.
|
||||
func (o *MorphPolicyCache) Get(key MorphPolicyCacheKey) []*chain.Chain {
|
||||
entry, err := o.cache.Get(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, ok := entry.([]*chain.Chain)
|
||||
if !ok {
|
||||
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
|
||||
zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Put puts an object to cache.
|
||||
func (o *MorphPolicyCache) Put(key MorphPolicyCacheKey, list []*chain.Chain) error {
|
||||
return o.cache.Set(key, list)
|
||||
}
|
||||
|
||||
// Delete deletes an object from cache.
|
||||
func (o *MorphPolicyCache) Delete(key MorphPolicyCacheKey) bool {
|
||||
return o.cache.Remove(key)
|
||||
}
|
46
api/cache/system.go
vendored
46
api/cache/system.go
vendored
|
@ -50,7 +50,7 @@ func (o *SystemCache) GetObject(key string) *data.ObjectInfo {
|
|||
result, ok := entry.(*data.ObjectInfo)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -81,7 +81,23 @@ func (o *SystemCache) GetCORS(key string) *data.CORSConfiguration {
|
|||
result, ok := entry.(*data.CORSConfiguration)
|
||||
if !ok {
|
||||
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 result
|
||||
}
|
||||
|
||||
func (o *SystemCache) GetLifecycleConfiguration(key string) *data.LifecycleConfiguration {
|
||||
entry, err := o.cache.Get(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, ok := entry.(*data.LifecycleConfiguration)
|
||||
if !ok {
|
||||
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
|
||||
zap.String("expected", fmt.Sprintf("%T", result)), logs.TagField(logs.TagDatapath))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -97,23 +113,7 @@ func (o *SystemCache) GetSettings(key string) *data.BucketSettings {
|
|||
result, ok := entry.(*data.BucketSettings)
|
||||
if !ok {
|
||||
o.logger.Warn(logs.InvalidCacheEntryType, zap.String("actual", fmt.Sprintf("%T", entry)),
|
||||
zap.String("expected", fmt.Sprintf("%T", result)))
|
||||
return nil
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func (o *SystemCache) GetNotificationConfiguration(key string) *data.NotificationConfiguration {
|
||||
entry, err := o.cache.Get(key)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
result, ok := entry.(*data.NotificationConfiguration)
|
||||
if !ok {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -149,12 +149,12 @@ func (o *SystemCache) PutCORS(key string, obj *data.CORSConfiguration) error {
|
|||
return o.cache.Set(key, obj)
|
||||
}
|
||||
|
||||
func (o *SystemCache) PutSettings(key string, settings *data.BucketSettings) error {
|
||||
return o.cache.Set(key, settings)
|
||||
func (o *SystemCache) PutLifecycleConfiguration(key string, obj *data.LifecycleConfiguration) error {
|
||||
return o.cache.Set(key, obj)
|
||||
}
|
||||
|
||||
func (o *SystemCache) PutNotificationConfiguration(key string, obj *data.NotificationConfiguration) error {
|
||||
return o.cache.Set(key, obj)
|
||||
func (o *SystemCache) PutSettings(key string, settings *data.BucketSettings) error {
|
||||
return o.cache.Set(key, settings)
|
||||
}
|
||||
|
||||
// PutTagging puts tags of a bucket or an object.
|
||||
|
|
|
@ -2,17 +2,20 @@ package data
|
|||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/user"
|
||||
"github.com/nspcc-dev/neo-go/pkg/crypto/keys"
|
||||
)
|
||||
|
||||
const (
|
||||
bktSettingsObject = ".s3-settings"
|
||||
bktCORSConfigurationObject = ".s3-cors"
|
||||
bktNotificationConfigurationObject = ".s3-notifications"
|
||||
bktSettingsObject = ".s3-settings"
|
||||
bktCORSConfigurationObject = ".s3-cors"
|
||||
bktLifecycleConfigurationObject = ".s3-lifecycle"
|
||||
|
||||
VersioningUnversioned = "Unversioned"
|
||||
VersioningEnabled = "Enabled"
|
||||
|
@ -22,21 +25,21 @@ const (
|
|||
type (
|
||||
// BucketInfo stores basic bucket data.
|
||||
BucketInfo struct {
|
||||
Name string // container name from system attribute
|
||||
Zone string // container zone from system attribute
|
||||
CID cid.ID
|
||||
Owner user.ID
|
||||
Created time.Time
|
||||
LocationConstraint string
|
||||
ObjectLockEnabled bool
|
||||
Name string // container name from system attribute
|
||||
Zone string // container zone from system attribute
|
||||
CID cid.ID
|
||||
Owner user.ID
|
||||
Created time.Time
|
||||
LocationConstraint string
|
||||
ObjectLockEnabled bool
|
||||
HomomorphicHashDisabled bool
|
||||
PlacementPolicy netmap.PlacementPolicy
|
||||
}
|
||||
|
||||
// ObjectInfo holds S3 object data.
|
||||
ObjectInfo struct {
|
||||
ID oid.ID
|
||||
CID cid.ID
|
||||
IsDir bool
|
||||
IsDeleteMarker bool
|
||||
ID oid.ID
|
||||
CID cid.ID
|
||||
|
||||
Bucket string
|
||||
Name string
|
||||
|
@ -45,22 +48,17 @@ type (
|
|||
Created time.Time
|
||||
CreationEpoch uint64
|
||||
HashSum string
|
||||
MD5Sum string
|
||||
Owner user.ID
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
// NotificationInfo store info to send s3 notification.
|
||||
NotificationInfo struct {
|
||||
Name string
|
||||
Version string
|
||||
Size uint64
|
||||
HashSum string
|
||||
}
|
||||
|
||||
// BucketSettings stores settings such as versioning.
|
||||
BucketSettings struct {
|
||||
Versioning string `json:"versioning"`
|
||||
LockConfiguration *ObjectLockConfiguration `json:"lock_configuration"`
|
||||
Versioning string
|
||||
LockConfiguration *ObjectLockConfiguration
|
||||
CannedACL string
|
||||
OwnerKey *keys.PublicKey
|
||||
}
|
||||
|
||||
// CORSConfiguration stores CORS configuration of a request.
|
||||
|
@ -78,26 +76,35 @@ type (
|
|||
ExposeHeaders []string `xml:"ExposeHeader" json:"ExposeHeaders"`
|
||||
MaxAgeSeconds int `xml:"MaxAgeSeconds,omitempty" json:"MaxAgeSeconds,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
// NotificationInfoFromObject creates new NotificationInfo from ObjectInfo.
|
||||
func NotificationInfoFromObject(objInfo *ObjectInfo) *NotificationInfo {
|
||||
return &NotificationInfo{
|
||||
Name: objInfo.Name,
|
||||
Version: objInfo.VersionID(),
|
||||
Size: objInfo.Size,
|
||||
HashSum: objInfo.HashSum,
|
||||
// ObjectVersion stores object version info.
|
||||
ObjectVersion struct {
|
||||
BktInfo *BucketInfo
|
||||
ObjectName string
|
||||
VersionID string
|
||||
NoErrorOnDeleteMarker bool
|
||||
}
|
||||
}
|
||||
|
||||
// CreatedObjectInfo stores created object info.
|
||||
CreatedObjectInfo struct {
|
||||
ID oid.ID
|
||||
Size uint64
|
||||
HashSum []byte
|
||||
MD5Sum []byte
|
||||
CreationEpoch uint64
|
||||
}
|
||||
)
|
||||
|
||||
// SettingsObjectName is a system name for a bucket settings file.
|
||||
func (b *BucketInfo) SettingsObjectName() string { return bktSettingsObject }
|
||||
|
||||
// CORSObjectName returns a system name for a bucket CORS configuration file.
|
||||
func (b *BucketInfo) CORSObjectName() string { return bktCORSConfigurationObject }
|
||||
func (b *BucketInfo) CORSObjectName() string {
|
||||
return b.CID.EncodeToString() + bktCORSConfigurationObject
|
||||
}
|
||||
|
||||
func (b *BucketInfo) NotificationConfigurationObjectName() string {
|
||||
return bktNotificationConfigurationObject
|
||||
func (b *BucketInfo) LifecycleConfigurationObjectName() string {
|
||||
return b.CID.EncodeToString() + bktLifecycleConfigurationObject
|
||||
}
|
||||
|
||||
// VersionID returns object version from ObjectInfo.
|
||||
|
@ -115,6 +122,13 @@ func (o *ObjectInfo) Address() oid.Address {
|
|||
return addr
|
||||
}
|
||||
|
||||
func (o *ObjectInfo) ETag(md5Enabled bool) string {
|
||||
if md5Enabled && len(o.MD5Sum) > 0 {
|
||||
return o.MD5Sum
|
||||
}
|
||||
return o.HashSum
|
||||
}
|
||||
|
||||
func (b BucketSettings) Unversioned() bool {
|
||||
return b.Versioning == VersioningUnversioned
|
||||
}
|
||||
|
@ -126,3 +140,11 @@ func (b BucketSettings) VersioningEnabled() bool {
|
|||
func (b BucketSettings) VersioningSuspended() bool {
|
||||
return b.Versioning == VersioningSuspended
|
||||
}
|
||||
|
||||
func Quote(val string) string {
|
||||
return "\"" + val + "\""
|
||||
}
|
||||
|
||||
func UnQuote(val string) string {
|
||||
return strings.Trim(val, "\"")
|
||||
}
|
||||
|
|
56
api/data/lifecycle.go
Normal file
56
api/data/lifecycle.go
Normal file
|
@ -0,0 +1,56 @@
|
|||
package data
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
const (
|
||||
LifecycleStatusEnabled = "Enabled"
|
||||
LifecycleStatusDisabled = "Disabled"
|
||||
)
|
||||
|
||||
type (
|
||||
LifecycleConfiguration struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ LifecycleConfiguration" json:"-"`
|
||||
Rules []LifecycleRule `xml:"Rule"`
|
||||
}
|
||||
|
||||
LifecycleRule struct {
|
||||
Status string `xml:"Status,omitempty"`
|
||||
AbortIncompleteMultipartUpload *AbortIncompleteMultipartUpload `xml:"AbortIncompleteMultipartUpload,omitempty"`
|
||||
Expiration *LifecycleExpiration `xml:"Expiration,omitempty"`
|
||||
Filter *LifecycleRuleFilter `xml:"Filter,omitempty"`
|
||||
ID string `xml:"ID,omitempty"`
|
||||
NonCurrentVersionExpiration *NonCurrentVersionExpiration `xml:"NoncurrentVersionExpiration,omitempty"`
|
||||
Prefix string `xml:"Prefix,omitempty"`
|
||||
}
|
||||
|
||||
AbortIncompleteMultipartUpload struct {
|
||||
DaysAfterInitiation *int `xml:"DaysAfterInitiation,omitempty"`
|
||||
}
|
||||
|
||||
LifecycleExpiration struct {
|
||||
Date string `xml:"Date,omitempty"`
|
||||
Days *int `xml:"Days,omitempty"`
|
||||
Epoch *uint64 `xml:"Epoch,omitempty"`
|
||||
ExpiredObjectDeleteMarker *bool `xml:"ExpiredObjectDeleteMarker,omitempty"`
|
||||
}
|
||||
|
||||
LifecycleRuleFilter struct {
|
||||
And *LifecycleRuleAndOperator `xml:"And,omitempty"`
|
||||
ObjectSizeGreaterThan *uint64 `xml:"ObjectSizeGreaterThan,omitempty"`
|
||||
ObjectSizeLessThan *uint64 `xml:"ObjectSizeLessThan,omitempty"`
|
||||
Prefix string `xml:"Prefix,omitempty"`
|
||||
Tag *Tag `xml:"Tag,omitempty"`
|
||||
}
|
||||
|
||||
LifecycleRuleAndOperator struct {
|
||||
ObjectSizeGreaterThan *uint64 `xml:"ObjectSizeGreaterThan,omitempty"`
|
||||
ObjectSizeLessThan *uint64 `xml:"ObjectSizeLessThan,omitempty"`
|
||||
Prefix string `xml:"Prefix,omitempty"`
|
||||
Tags []Tag `xml:"Tag"`
|
||||
}
|
||||
|
||||
NonCurrentVersionExpiration struct {
|
||||
NewerNonCurrentVersions *int `xml:"NewerNoncurrentVersions,omitempty"`
|
||||
NonCurrentDays *int `xml:"NoncurrentDays,omitempty"`
|
||||
}
|
||||
)
|
19
api/data/listsession.go
Normal file
19
api/data/listsession.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package data
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type VersionsStream interface {
|
||||
Next(ctx context.Context) (*NodeVersion, error)
|
||||
}
|
||||
|
||||
type ListSession struct {
|
||||
Next []*ExtendedNodeVersion
|
||||
Stream VersionsStream
|
||||
NamesMap map[string]struct{}
|
||||
Context context.Context
|
||||
Cancel context.CancelFunc
|
||||
Acquired atomic.Bool
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package data
|
||||
|
||||
type (
|
||||
NotificationConfiguration struct {
|
||||
QueueConfigurations []QueueConfiguration `xml:"QueueConfiguration" json:"QueueConfigurations"`
|
||||
// Not supported topics
|
||||
TopicConfigurations []TopicConfiguration `xml:"TopicConfiguration" json:"TopicConfigurations"`
|
||||
LambdaFunctionConfigurations []LambdaFunctionConfiguration `xml:"CloudFunctionConfiguration" json:"CloudFunctionConfigurations"`
|
||||
}
|
||||
|
||||
QueueConfiguration struct {
|
||||
ID string `xml:"Id" json:"Id"`
|
||||
QueueArn string `xml:"Queue" json:"Queue"`
|
||||
Events []string `xml:"Event" json:"Events"`
|
||||
Filter Filter `xml:"Filter" json:"Filter"`
|
||||
}
|
||||
|
||||
Filter struct {
|
||||
Key Key `xml:"S3Key" json:"S3Key"`
|
||||
}
|
||||
|
||||
Key struct {
|
||||
FilterRules []FilterRule `xml:"FilterRule" json:"FilterRules"`
|
||||
}
|
||||
|
||||
FilterRule struct {
|
||||
Name string `xml:"Name" json:"Name"`
|
||||
Value string `xml:"Value" json:"Value"`
|
||||
}
|
||||
|
||||
// TopicConfiguration and LambdaFunctionConfiguration -- we don't support these configurations,
|
||||
// but we need them to detect in notification configurations in requests.
|
||||
TopicConfiguration struct{}
|
||||
LambdaFunctionConfiguration struct{}
|
||||
)
|
||||
|
||||
func (n NotificationConfiguration) IsEmpty() bool {
|
||||
return len(n.QueueConfigurations) == 0 && len(n.TopicConfigurations) == 0 && len(n.LambdaFunctionConfigurations) == 0
|
||||
}
|
30
api/data/tagging.go
Normal file
30
api/data/tagging.go
Normal file
|
@ -0,0 +1,30 @@
|
|||
package data
|
||||
|
||||
import "encoding/xml"
|
||||
|
||||
// Tagging contains tag set.
|
||||
type Tagging struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Tagging"`
|
||||
TagSet []Tag `xml:"TagSet>Tag"`
|
||||
}
|
||||
|
||||
// Tag is an AWS key-value tag.
|
||||
type Tag struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
type GetObjectTaggingParams struct {
|
||||
ObjectVersion *ObjectVersion
|
||||
|
||||
// NodeVersion can be nil. If not nil we save one request to tree service.
|
||||
NodeVersion *NodeVersion // optional
|
||||
}
|
||||
|
||||
type PutObjectTaggingParams struct {
|
||||
ObjectVersion *ObjectVersion
|
||||
TagSet map[string]string
|
||||
|
||||
// NodeVersion can be nil. If not nil we save one request to tree service.
|
||||
NodeVersion *NodeVersion // optional
|
||||
}
|
|
@ -16,20 +16,31 @@ const (
|
|||
// NodeVersion represent node from tree service.
|
||||
type NodeVersion struct {
|
||||
BaseNodeVersion
|
||||
DeleteMarker *DeleteMarkerInfo
|
||||
IsUnversioned bool
|
||||
IsCombined bool
|
||||
}
|
||||
|
||||
func (v NodeVersion) IsDeleteMarker() bool {
|
||||
return v.DeleteMarker != nil
|
||||
// ExtendedNodeVersion contains additional node info to be able to sort versions by timestamp.
|
||||
type ExtendedNodeVersion struct {
|
||||
NodeVersion *NodeVersion
|
||||
IsLatest bool
|
||||
DirName string
|
||||
}
|
||||
|
||||
// DeleteMarkerInfo is used to save object info if node in the tree service is delete marker.
|
||||
// We need this information because the "delete marker" object is no longer stored in FrostFS.
|
||||
type DeleteMarkerInfo struct {
|
||||
Created time.Time
|
||||
Owner user.ID
|
||||
func (e ExtendedNodeVersion) Version() string {
|
||||
if e.NodeVersion.IsUnversioned {
|
||||
return UnversionedObjectVersionID
|
||||
}
|
||||
|
||||
return e.NodeVersion.OID.EncodeToString()
|
||||
}
|
||||
|
||||
func (e ExtendedNodeVersion) Name() string {
|
||||
if e.DirName != "" {
|
||||
return e.DirName
|
||||
}
|
||||
|
||||
return e.NodeVersion.FilePath
|
||||
}
|
||||
|
||||
// ExtendedObjectInfo contains additional node info to be able to sort versions by timestamp.
|
||||
|
@ -50,13 +61,36 @@ func (e ExtendedObjectInfo) Version() string {
|
|||
// BaseNodeVersion is minimal node info from tree service.
|
||||
// Basically used for "system" object.
|
||||
type BaseNodeVersion struct {
|
||||
ID uint64
|
||||
ParenID uint64
|
||||
OID oid.ID
|
||||
Timestamp uint64
|
||||
Size uint64
|
||||
ETag string
|
||||
FilePath string
|
||||
ID uint64
|
||||
ParentID uint64
|
||||
OID oid.ID
|
||||
Timestamp uint64
|
||||
Size uint64
|
||||
ETag string
|
||||
MD5 string
|
||||
FilePath string
|
||||
Created *time.Time
|
||||
Owner *user.ID
|
||||
IsDeleteMarker bool
|
||||
CreationEpoch uint64
|
||||
}
|
||||
|
||||
func (v *BaseNodeVersion) GetETag(md5Enabled bool) string {
|
||||
if md5Enabled && len(v.MD5) > 0 {
|
||||
return v.MD5
|
||||
}
|
||||
return v.ETag
|
||||
}
|
||||
|
||||
// IsFilledExtra returns true is node was created by version of gate v0.29.x and later.
|
||||
func (v BaseNodeVersion) IsFilledExtra() bool {
|
||||
return v.Created != nil && v.Owner != nil
|
||||
}
|
||||
|
||||
func (v *BaseNodeVersion) FillExtra(owner *user.ID, created *time.Time, realSize uint64) {
|
||||
v.Owner = owner
|
||||
v.Created = created
|
||||
v.Size = realSize
|
||||
}
|
||||
|
||||
type ObjectTaggingInfo struct {
|
||||
|
@ -75,7 +109,8 @@ type MultipartInfo struct {
|
|||
Owner user.ID
|
||||
Created time.Time
|
||||
Meta map[string]string
|
||||
CopiesNumbers []uint32
|
||||
Finished bool
|
||||
CreationEpoch uint64
|
||||
}
|
||||
|
||||
// PartInfo is upload information about part.
|
||||
|
@ -86,14 +121,31 @@ type PartInfo struct {
|
|||
OID oid.ID `json:"oid"`
|
||||
Size uint64 `json:"size"`
|
||||
ETag string `json:"etag"`
|
||||
MD5 string `json:"md5"`
|
||||
Created time.Time `json:"created"`
|
||||
}
|
||||
|
||||
type PartInfoExtended struct {
|
||||
PartInfo
|
||||
|
||||
// Timestamp is used to find the latest version of part info in case of tree split
|
||||
// when there are multiple nodes for the same part.
|
||||
Timestamp uint64
|
||||
}
|
||||
|
||||
// ToHeaderString form short part representation to use in S3-Completed-Parts header.
|
||||
func (p *PartInfo) ToHeaderString() string {
|
||||
// ETag value contains SHA256 checksum which is used while getting object parts attributes.
|
||||
return strconv.Itoa(p.Number) + "-" + strconv.FormatUint(p.Size, 10) + "-" + p.ETag
|
||||
}
|
||||
|
||||
func (p *PartInfo) GetETag(md5Enabled bool) string {
|
||||
if md5Enabled && len(p.MD5) > 0 {
|
||||
return p.MD5
|
||||
}
|
||||
return p.ETag
|
||||
}
|
||||
|
||||
// LockInfo is lock information to create appropriate tree node.
|
||||
type LockInfo struct {
|
||||
id uint64
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
package errors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
frosterrors "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree"
|
||||
frosterr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/frostfs/errors"
|
||||
)
|
||||
|
||||
type (
|
||||
|
@ -26,6 +29,7 @@ type (
|
|||
const (
|
||||
_ ErrorCode = iota
|
||||
ErrAccessDenied
|
||||
ErrAccessControlListNotSupported
|
||||
ErrBadDigest
|
||||
ErrEntityTooSmall
|
||||
ErrEntityTooLarge
|
||||
|
@ -53,9 +57,9 @@ const (
|
|||
ErrInvalidCopyDest
|
||||
ErrInvalidPolicyDocument
|
||||
ErrInvalidObjectState
|
||||
ErrMalformedACL
|
||||
ErrMalformedXML
|
||||
ErrMissingContentLength
|
||||
ErrMissingContentMD5
|
||||
ErrMissingRequestBodyError
|
||||
ErrMissingSecurityHeader
|
||||
ErrNoSuchBucket
|
||||
|
@ -73,6 +77,7 @@ const (
|
|||
ErrInvalidArgument
|
||||
ErrInvalidTagKey
|
||||
ErrInvalidTagValue
|
||||
ErrInvalidTagKeyUniqueness
|
||||
ErrInvalidTagsSizeExceed
|
||||
ErrNotImplemented
|
||||
ErrPreconditionFailed
|
||||
|
@ -89,6 +94,7 @@ const (
|
|||
ErrBucketNotEmpty
|
||||
ErrAllAccessDisabled
|
||||
ErrMalformedPolicy
|
||||
ErrMalformedPolicyNotPrincipal
|
||||
ErrMissingFields
|
||||
ErrMissingCredTag
|
||||
ErrCredMalformed
|
||||
|
@ -148,6 +154,7 @@ const (
|
|||
ErrInvalidEncryptionAlgorithm
|
||||
ErrInvalidSSECustomerKey
|
||||
ErrMissingSSECustomerKey
|
||||
ErrMissingSSECustomerAlgorithm
|
||||
ErrMissingSSECustomerKeyMD5
|
||||
ErrSSECustomerKeyMD5Mismatch
|
||||
ErrInvalidSSECustomerParameters
|
||||
|
@ -182,6 +189,10 @@ const (
|
|||
ErrInvalidRequest
|
||||
ErrInvalidRequestLargeCopy
|
||||
ErrInvalidStorageClass
|
||||
VersionIDMarkerWithoutKeyMarker
|
||||
ErrInvalidRangeLength
|
||||
ErrRangeOutOfBounds
|
||||
ErrMissingContentRange
|
||||
|
||||
ErrMalformedJSON
|
||||
ErrInsecureClientRequest
|
||||
|
@ -278,6 +289,9 @@ const (
|
|||
//CORS configuration errors.
|
||||
ErrCORSUnsupportedMethod
|
||||
ErrCORSWildcardExposeHeaders
|
||||
|
||||
// Limits errors.
|
||||
ErrLimitExceeded
|
||||
)
|
||||
|
||||
// error code to Error structure, these fields carry respective
|
||||
|
@ -313,6 +327,12 @@ var errorCodes = errorCodeMap{
|
|||
Description: "Invalid storage class.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
VersionIDMarkerWithoutKeyMarker: {
|
||||
ErrCode: VersionIDMarkerWithoutKeyMarker,
|
||||
Code: "VersionIDMarkerWithoutKeyMarker",
|
||||
Description: "A version-id marker cannot be specified without a key marker.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidRequestBody: {
|
||||
ErrCode: ErrInvalidRequestBody,
|
||||
Code: "InvalidArgument",
|
||||
|
@ -367,6 +387,12 @@ var errorCodes = errorCodeMap{
|
|||
Description: "Access Denied.",
|
||||
HTTPStatusCode: http.StatusForbidden,
|
||||
},
|
||||
ErrAccessControlListNotSupported: {
|
||||
ErrCode: ErrAccessControlListNotSupported,
|
||||
Code: "AccessControlListNotSupported",
|
||||
Description: "The bucket does not allow ACLs.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrBadDigest: {
|
||||
ErrCode: ErrBadDigest,
|
||||
Code: "BadDigest",
|
||||
|
@ -433,6 +459,12 @@ var errorCodes = errorCodeMap{
|
|||
Description: "The requested range is not satisfiable",
|
||||
HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
|
||||
},
|
||||
ErrMalformedACL: {
|
||||
ErrCode: ErrMalformedACL,
|
||||
Code: "MalformedACLError",
|
||||
Description: "The ACL that you provided was not well formed or did not validate against our published schema.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMalformedXML: {
|
||||
ErrCode: ErrMalformedXML,
|
||||
Code: "MalformedXML",
|
||||
|
@ -445,12 +477,6 @@ var errorCodes = errorCodeMap{
|
|||
Description: "You must provide the Content-Length HTTP header.",
|
||||
HTTPStatusCode: http.StatusLengthRequired,
|
||||
},
|
||||
ErrMissingContentMD5: {
|
||||
ErrCode: ErrMissingContentMD5,
|
||||
Code: "MissingContentMD5",
|
||||
Description: "Missing required header for this request: Content-Md5.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSecurityHeader: {
|
||||
ErrCode: ErrMissingSecurityHeader,
|
||||
Code: "MissingSecurityHeader",
|
||||
|
@ -526,13 +552,19 @@ var errorCodes = errorCodeMap{
|
|||
ErrInvalidTagKey: {
|
||||
ErrCode: ErrInvalidTagKey,
|
||||
Code: "InvalidTag",
|
||||
Description: "The TagValue you have provided is invalid",
|
||||
Description: "The TagKey you have provided is invalid",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidTagValue: {
|
||||
ErrCode: ErrInvalidTagValue,
|
||||
Code: "InvalidTag",
|
||||
Description: "The TagKey you have provided is invalid",
|
||||
Description: "The TagValue you have provided is invalid",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidTagKeyUniqueness: {
|
||||
ErrCode: ErrInvalidTagKeyUniqueness,
|
||||
Code: "InvalidTag",
|
||||
Description: "Cannot provide multiple Tags with the same key",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidTagsSizeExceed: {
|
||||
|
@ -598,7 +630,7 @@ var errorCodes = errorCodeMap{
|
|||
ErrAuthorizationHeaderMalformed: {
|
||||
ErrCode: ErrAuthorizationHeaderMalformed,
|
||||
Code: "AuthorizationHeaderMalformed",
|
||||
Description: "The authorization header is malformed; the region is wrong; expecting 'us-east-1'.",
|
||||
Description: "The authorization header that you provided is not valid.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMalformedPOSTRequest: {
|
||||
|
@ -643,6 +675,12 @@ var errorCodes = errorCodeMap{
|
|||
Description: "Policy has invalid resource.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMalformedPolicyNotPrincipal: {
|
||||
ErrCode: ErrMalformedPolicyNotPrincipal,
|
||||
Code: "MalformedPolicy",
|
||||
Description: "Allow with NotPrincipal is not allowed.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingFields: {
|
||||
ErrCode: ErrMissingFields,
|
||||
Code: "MissingFields",
|
||||
|
@ -1048,6 +1086,12 @@ var errorCodes = errorCodeMap{
|
|||
Description: "Requests specifying Server Side Encryption with Customer provided keys must provide an appropriate secret key.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSSECustomerAlgorithm: {
|
||||
ErrCode: ErrMissingSSECustomerAlgorithm,
|
||||
Code: "InvalidArgument",
|
||||
Description: "Requests specifying Server Side Encryption with Customer provided keys must provide a valid encryption algorithm.",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrMissingSSECustomerKeyMD5: {
|
||||
ErrCode: ErrMissingSSECustomerKeyMD5,
|
||||
Code: "InvalidArgument",
|
||||
|
@ -1704,12 +1748,38 @@ var errorCodes = errorCodeMap{
|
|||
Description: "Part number must be an integer between 1 and 10000, inclusive",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
ErrInvalidRangeLength: {
|
||||
ErrCode: ErrInvalidRangeLength,
|
||||
Code: "InvalidRange",
|
||||
Description: "Provided range length must be equal to content length",
|
||||
HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
|
||||
},
|
||||
ErrRangeOutOfBounds: {
|
||||
ErrCode: ErrRangeOutOfBounds,
|
||||
Code: "InvalidRange",
|
||||
Description: "Provided range is outside of object bounds",
|
||||
HTTPStatusCode: http.StatusRequestedRangeNotSatisfiable,
|
||||
},
|
||||
ErrMissingContentRange: {
|
||||
ErrCode: ErrMissingContentRange,
|
||||
Code: "MissingContentRange",
|
||||
Description: "Content-Range header is mandatory for this type of request",
|
||||
HTTPStatusCode: http.StatusBadRequest,
|
||||
},
|
||||
// The Conflict status is used because this error was made based on the LimitExceeded error
|
||||
// from aws iam error https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateUser.html#API_CreateUser_Errors.
|
||||
ErrLimitExceeded: {
|
||||
ErrCode: ErrLimitExceeded,
|
||||
Code: "LimitExceeded",
|
||||
Description: "You have reached the quota limit.",
|
||||
HTTPStatusCode: http.StatusConflict,
|
||||
},
|
||||
// Add your error structure here.
|
||||
}
|
||||
|
||||
// IsS3Error checks if the provided error is a specific s3 error.
|
||||
func IsS3Error(err error, code ErrorCode) bool {
|
||||
err = frosterrors.UnwrapErr(err)
|
||||
err = frosterr.UnwrapErr(err)
|
||||
e, ok := err.(Error)
|
||||
return ok && e.ErrCode == code
|
||||
}
|
||||
|
@ -1746,6 +1816,34 @@ func GetAPIErrorWithError(code ErrorCode, err error) Error {
|
|||
return errorCodes.toAPIErrWithErr(code, err)
|
||||
}
|
||||
|
||||
// TransformToS3Error converts FrostFS error to the corresponding S3 error type.
|
||||
func TransformToS3Error(err error) error {
|
||||
err = frosterr.UnwrapErr(err) // this wouldn't work with errors.Join
|
||||
var s3err Error
|
||||
if errors.As(err, &s3err) {
|
||||
return err
|
||||
}
|
||||
|
||||
if errors.Is(err, frostfs.ErrAccessDenied) ||
|
||||
errors.Is(err, tree.ErrNodeAccessDenied) {
|
||||
return GetAPIError(ErrAccessDenied)
|
||||
}
|
||||
|
||||
if errors.Is(err, frostfs.ErrGatewayTimeout) {
|
||||
return GetAPIError(ErrGatewayTimeout)
|
||||
}
|
||||
|
||||
if errors.Is(err, frostfs.ErrGlobalDomainIsAlreadyTaken) {
|
||||
return GetAPIError(ErrBucketAlreadyExists)
|
||||
}
|
||||
|
||||
if errors.Is(err, frostfs.ErrQuotaLimitReached) {
|
||||
return GetAPIError(ErrLimitExceeded)
|
||||
}
|
||||
|
||||
return GetAPIError(ErrInternalError)
|
||||
}
|
||||
|
||||
// ObjectError -- error that is linked to a specific object.
|
||||
type ObjectError struct {
|
||||
Err error
|
||||
|
|
|
@ -2,7 +2,12 @@ package errors
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/tree"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func BenchmarkErrCode(b *testing.B) {
|
||||
|
@ -24,3 +29,56 @@ func BenchmarkErrorsIs(b *testing.B) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransformS3Errors(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
err error
|
||||
expected ErrorCode
|
||||
}{
|
||||
{
|
||||
name: "simple std error to internal error",
|
||||
err: errors.New("some error"),
|
||||
expected: ErrInternalError,
|
||||
},
|
||||
{
|
||||
name: "layer access denied error to s3 access denied error",
|
||||
err: frostfs.ErrAccessDenied,
|
||||
expected: ErrAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "wrapped layer access denied error to s3 access denied error",
|
||||
err: fmt.Errorf("wrap: %w", frostfs.ErrAccessDenied),
|
||||
expected: ErrAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "layer node access denied error to s3 access denied error",
|
||||
err: tree.ErrNodeAccessDenied,
|
||||
expected: ErrAccessDenied,
|
||||
},
|
||||
{
|
||||
name: "layer gateway timeout error to s3 gateway timeout error",
|
||||
err: frostfs.ErrGatewayTimeout,
|
||||
expected: ErrGatewayTimeout,
|
||||
},
|
||||
{
|
||||
name: "s3 error to s3 error",
|
||||
err: GetAPIError(ErrInvalidPart),
|
||||
expected: ErrInvalidPart,
|
||||
},
|
||||
{
|
||||
name: "wrapped s3 error to s3 error",
|
||||
err: fmt.Errorf("wrap: %w", GetAPIError(ErrInvalidPart)),
|
||||
expected: ErrInvalidPart,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := TransformToS3Error(tc.err)
|
||||
s3err, ok := err.(Error)
|
||||
require.True(t, ok, "error must be s3 error")
|
||||
require.Equalf(t, tc.expected, s3err.ErrCode,
|
||||
"expected: '%s', got: '%s'",
|
||||
GetAPIError(tc.expected).Code, GetAPIError(s3err.ErrCode).Code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
1594
api/handler/acl.go
1594
api/handler/acl.go
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -11,74 +11,81 @@ import (
|
|||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/netmap"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type (
|
||||
handler struct {
|
||||
log *zap.Logger
|
||||
obj layer.Client
|
||||
notificator Notificator
|
||||
cfg *Config
|
||||
}
|
||||
|
||||
Notificator interface {
|
||||
SendNotifications(topics map[string]string, p *SendNotificationParams) error
|
||||
SendTestNotification(topic, bucketName, requestID, HostID string, now time.Time) error
|
||||
log *zap.Logger
|
||||
obj *layer.Layer
|
||||
cfg Config
|
||||
ape APE
|
||||
frostfsid FrostFSID
|
||||
}
|
||||
|
||||
// Config contains data which handler needs to keep.
|
||||
Config struct {
|
||||
Policy PlacementPolicy
|
||||
XMLDecoder XMLDecoderProvider
|
||||
DefaultMaxAge int
|
||||
NotificatorEnabled bool
|
||||
ResolveZoneList []string
|
||||
IsResolveListAllow bool // True if ResolveZoneList contains allowed zones
|
||||
CompleteMultipartKeepalive time.Duration
|
||||
Kludge KludgeSettings
|
||||
Config interface {
|
||||
DefaultPlacementPolicy(namespace string) netmap.PlacementPolicy
|
||||
PlacementPolicy(namespace, constraint string) (netmap.PlacementPolicy, bool)
|
||||
CopiesNumbers(namespace, constraint string) ([]uint32, bool)
|
||||
DefaultCopiesNumbers(namespace string) []uint32
|
||||
NewXMLDecoder(reader io.Reader, agent string) *xml.Decoder
|
||||
DefaultMaxAge() int
|
||||
ResolveZoneList() []string
|
||||
IsResolveListAllow() bool
|
||||
BypassContentEncodingInChunks(agent string) bool
|
||||
MD5Enabled() bool
|
||||
RetryMaxAttempts() int
|
||||
RetryMaxBackoff() time.Duration
|
||||
RetryStrategy() RetryStrategy
|
||||
TLSTerminationHeader() string
|
||||
}
|
||||
|
||||
PlacementPolicy interface {
|
||||
DefaultPlacementPolicy() netmap.PlacementPolicy
|
||||
PlacementPolicy(string) (netmap.PlacementPolicy, bool)
|
||||
CopiesNumbers(string) ([]uint32, bool)
|
||||
DefaultCopiesNumbers() []uint32
|
||||
FrostFSID interface {
|
||||
GetUserAddress(account, user string) (string, error)
|
||||
GetUserKey(account, name string) (string, error)
|
||||
}
|
||||
|
||||
XMLDecoderProvider interface {
|
||||
NewCompleteMultipartDecoder(io.Reader) *xml.Decoder
|
||||
// APE is Access Policy Engine that needs to save policy and acl info to different places.
|
||||
APE interface {
|
||||
PutBucketPolicy(ns string, cnrID cid.ID, policy []byte, chains []*chain.Chain) error
|
||||
DeleteBucketPolicy(ns string, cnrID cid.ID, chainIDs []chain.ID) error
|
||||
GetBucketPolicy(ns string, cnrID cid.ID) ([]byte, error)
|
||||
SaveACLChains(cid string, chains []*chain.Chain) error
|
||||
}
|
||||
)
|
||||
|
||||
KludgeSettings interface {
|
||||
BypassContentEncodingInChunks() bool
|
||||
}
|
||||
type RetryStrategy string
|
||||
|
||||
const (
|
||||
RetryStrategyExponential = "exponential"
|
||||
RetryStrategyConstant = "constant"
|
||||
)
|
||||
|
||||
var _ api.Handler = (*handler)(nil)
|
||||
|
||||
// New creates new api.Handler using given logger and client.
|
||||
func New(log *zap.Logger, obj layer.Client, notificator Notificator, cfg *Config) (api.Handler, error) {
|
||||
func New(log *zap.Logger, obj *layer.Layer, cfg Config, storage APE, ffsid FrostFSID) (api.Handler, error) {
|
||||
switch {
|
||||
case obj == nil:
|
||||
return nil, errors.New("empty FrostFS Object Layer")
|
||||
case log == nil:
|
||||
return nil, errors.New("empty logger")
|
||||
}
|
||||
|
||||
if !cfg.NotificatorEnabled {
|
||||
log.Warn(logs.NotificatorIsDisabledS3WontProduceNotificationEvents)
|
||||
} else if notificator == nil {
|
||||
return nil, errors.New("empty notificator")
|
||||
case storage == nil:
|
||||
return nil, errors.New("empty policy storage")
|
||||
case ffsid == nil:
|
||||
return nil, errors.New("empty frostfsid")
|
||||
}
|
||||
|
||||
return &handler{
|
||||
log: log,
|
||||
obj: obj,
|
||||
cfg: cfg,
|
||||
notificator: notificator,
|
||||
log: log,
|
||||
obj: obj,
|
||||
cfg: cfg,
|
||||
ape: storage,
|
||||
frostfsid: ffsid,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -86,7 +93,7 @@ func New(log *zap.Logger, obj layer.Client, notificator Notificator, cfg *Config
|
|||
// 1) array of copies numbers sent in request's header has the highest priority.
|
||||
// 2) array of copies numbers with corresponding location constraint provided in the config file.
|
||||
// 3) default copies number from the config file wrapped into array.
|
||||
func (h *handler) pickCopiesNumbers(metadata map[string]string, locationConstraint string) ([]uint32, error) {
|
||||
func (h *handler) pickCopiesNumbers(metadata map[string]string, namespace, locationConstraint string) ([]uint32, error) {
|
||||
copiesNumbersStr, ok := metadata[layer.AttributeFrostfsCopiesNumber]
|
||||
if ok {
|
||||
result, err := parseCopiesNumbers(copiesNumbersStr)
|
||||
|
@ -96,12 +103,12 @@ func (h *handler) pickCopiesNumbers(metadata map[string]string, locationConstrai
|
|||
return result, nil
|
||||
}
|
||||
|
||||
copiesNumbers, ok := h.cfg.Policy.CopiesNumbers(locationConstraint)
|
||||
copiesNumbers, ok := h.cfg.CopiesNumbers(namespace, locationConstraint)
|
||||
if ok {
|
||||
return copiesNumbers, nil
|
||||
}
|
||||
|
||||
return h.cfg.Policy.DefaultCopiesNumbers(), nil
|
||||
return h.cfg.DefaultCopiesNumbers(namespace), nil
|
||||
}
|
||||
|
||||
func parseCopiesNumbers(copiesNumbersStr string) ([]uint32, error) {
|
||||
|
|
|
@ -12,11 +12,9 @@ func TestCopiesNumberPicker(t *testing.T) {
|
|||
locationConstraint2 := "two"
|
||||
locationConstraints[locationConstraint1] = []uint32{2, 3, 4}
|
||||
|
||||
config := &Config{
|
||||
Policy: &placementPolicyMock{
|
||||
copiesNumbers: locationConstraints,
|
||||
defaultCopiesNumbers: []uint32{1},
|
||||
},
|
||||
config := &configMock{
|
||||
copiesNumbers: locationConstraints,
|
||||
defaultCopiesNumbers: []uint32{1},
|
||||
}
|
||||
h := handler{
|
||||
cfg: config,
|
||||
|
@ -28,7 +26,7 @@ func TestCopiesNumberPicker(t *testing.T) {
|
|||
metadata["somekey1"] = "5, 6, 7"
|
||||
expectedCopiesNumbers := []uint32{1}
|
||||
|
||||
actualCopiesNumbers, err := h.pickCopiesNumbers(metadata, locationConstraint2)
|
||||
actualCopiesNumbers, err := h.pickCopiesNumbers(metadata, "", locationConstraint2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCopiesNumbers, actualCopiesNumbers)
|
||||
})
|
||||
|
@ -37,7 +35,7 @@ func TestCopiesNumberPicker(t *testing.T) {
|
|||
metadata["somekey2"] = "6, 7, 8"
|
||||
expectedCopiesNumbers := []uint32{2, 3, 4}
|
||||
|
||||
actualCopiesNumbers, err := h.pickCopiesNumbers(metadata, locationConstraint1)
|
||||
actualCopiesNumbers, err := h.pickCopiesNumbers(metadata, "", locationConstraint1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCopiesNumbers, actualCopiesNumbers)
|
||||
})
|
||||
|
@ -46,7 +44,7 @@ func TestCopiesNumberPicker(t *testing.T) {
|
|||
metadata["frostfs-copies-number"] = "7, 8, 9"
|
||||
expectedCopiesNumbers := []uint32{7, 8, 9}
|
||||
|
||||
actualCopiesNumbers, err := h.pickCopiesNumbers(metadata, locationConstraint2)
|
||||
actualCopiesNumbers, err := h.pickCopiesNumbers(metadata, "", locationConstraint2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCopiesNumbers, actualCopiesNumbers)
|
||||
})
|
||||
|
@ -55,7 +53,7 @@ func TestCopiesNumberPicker(t *testing.T) {
|
|||
metadata["frostfs-copies-number"] = "7,8,9"
|
||||
expectedCopiesNumbers := []uint32{7, 8, 9}
|
||||
|
||||
actualCopiesNumbers, err := h.pickCopiesNumbers(metadata, locationConstraint2)
|
||||
actualCopiesNumbers, err := h.pickCopiesNumbers(metadata, "", locationConstraint2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCopiesNumbers, actualCopiesNumbers)
|
||||
})
|
||||
|
@ -64,7 +62,7 @@ func TestCopiesNumberPicker(t *testing.T) {
|
|||
metadata["frostfs-copies-number"] = "11, 12, 13, "
|
||||
expectedCopiesNumbers := []uint32{11, 12, 13}
|
||||
|
||||
actualCopiesNumbers, err := h.pickCopiesNumbers(metadata, locationConstraint2)
|
||||
actualCopiesNumbers, err := h.pickCopiesNumbers(metadata, "", locationConstraint2)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expectedCopiesNumbers, actualCopiesNumbers)
|
||||
})
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
@ -68,17 +70,18 @@ var validAttributes = map[string]struct{}{
|
|||
}
|
||||
|
||||
func (h *handler) GetObjectAttributesHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
ctx := r.Context()
|
||||
reqInfo := middleware.GetReqInfo(ctx)
|
||||
|
||||
params, err := parseGetObjectAttributeArgs(r)
|
||||
params, err := parseGetObjectAttributeArgs(r, h.reqLogger(ctx))
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "invalid request", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "invalid request", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -88,44 +91,44 @@ func (h *handler) GetObjectAttributesHandler(w http.ResponseWriter, r *http.Requ
|
|||
VersionID: params.VersionID,
|
||||
}
|
||||
|
||||
extendedInfo, err := h.obj.GetExtendedObjectInfo(r.Context(), p)
|
||||
extendedInfo, err := h.obj.GetExtendedObjectInfo(ctx, p)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not fetch object info", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not fetch object info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
info := extendedInfo.ObjectInfo
|
||||
|
||||
encryptionParams, err := formEncryptionParams(r)
|
||||
encryptionParams, err := h.formEncryptionParams(r)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "invalid sse headers", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(info.Headers)); err != nil {
|
||||
h.logAndSendError(w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err))
|
||||
h.logAndSendError(ctx, w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if err = checkPreconditions(info, params.Conditional); err != nil {
|
||||
h.logAndSendError(w, "precondition failed", reqInfo, err)
|
||||
if err = checkPreconditions(info, params.Conditional, h.cfg.MD5Enabled()); err != nil {
|
||||
h.logAndSendError(ctx, w, "precondition failed", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
bktSettings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
||||
bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := encodeToObjectAttributesResponse(info, params)
|
||||
response, err := encodeToObjectAttributesResponse(info, params, h.cfg.MD5Enabled())
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "couldn't encode object info to response", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "couldn't encode object info to response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
writeAttributesHeaders(w.Header(), extendedInfo, bktSettings.Unversioned())
|
||||
if err = middleware.EncodeToResponse(w, response); err != nil {
|
||||
h.logAndSendError(w, "something went wrong", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -135,14 +138,14 @@ func writeAttributesHeaders(h http.Header, info *data.ExtendedObjectInfo, isBuck
|
|||
h.Set(api.AmzVersionID, info.Version())
|
||||
}
|
||||
|
||||
if info.NodeVersion.IsDeleteMarker() {
|
||||
if info.NodeVersion.IsDeleteMarker {
|
||||
h.Set(api.AmzDeleteMarker, strconv.FormatBool(true))
|
||||
}
|
||||
|
||||
// x-amz-request-charged
|
||||
}
|
||||
|
||||
func parseGetObjectAttributeArgs(r *http.Request) (*GetObjectAttributesArgs, error) {
|
||||
func parseGetObjectAttributeArgs(r *http.Request, log *zap.Logger) (*GetObjectAttributesArgs, error) {
|
||||
res := &GetObjectAttributesArgs{
|
||||
VersionID: r.URL.Query().Get(api.QueryVersionID),
|
||||
}
|
||||
|
@ -175,23 +178,27 @@ func parseGetObjectAttributeArgs(r *http.Request) (*GetObjectAttributesArgs, err
|
|||
}
|
||||
}
|
||||
|
||||
res.Conditional, err = parseConditionalHeaders(r.Header)
|
||||
return res, err
|
||||
res.Conditional = parseConditionalHeaders(r.Header, log)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func encodeToObjectAttributesResponse(info *data.ObjectInfo, p *GetObjectAttributesArgs) (*GetObjectAttributesResponse, error) {
|
||||
func encodeToObjectAttributesResponse(info *data.ObjectInfo, p *GetObjectAttributesArgs, md5Enabled bool) (*GetObjectAttributesResponse, error) {
|
||||
resp := &GetObjectAttributesResponse{}
|
||||
|
||||
for _, attr := range p.Attributes {
|
||||
switch attr {
|
||||
case eTag:
|
||||
resp.ETag = info.HashSum
|
||||
resp.ETag = data.Quote(info.ETag(md5Enabled))
|
||||
case storageClass:
|
||||
resp.StorageClass = "STANDARD"
|
||||
resp.StorageClass = api.DefaultStorageClass
|
||||
case objectSize:
|
||||
resp.ObjectSize = info.Size
|
||||
case checksum:
|
||||
resp.Checksum = &Checksum{ChecksumSHA256: info.HashSum}
|
||||
checksumBytes, err := hex.DecodeString(info.HashSum)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("form upload attributes: %w", err)
|
||||
}
|
||||
resp.Checksum = &Checksum{ChecksumSHA256: base64.StdEncoding.EncodeToString(checksumBytes)}
|
||||
case objectParts:
|
||||
parts, err := formUploadAttributes(info, p.MaxParts, p.PartNumberMarker)
|
||||
if err != nil {
|
||||
|
@ -219,10 +226,15 @@ func formUploadAttributes(info *data.ObjectInfo, maxParts, marker int) (*ObjectP
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid completed part: %w", err)
|
||||
}
|
||||
// ETag value contains SHA256 checksum.
|
||||
checksumBytes, err := hex.DecodeString(part.ETag)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid sha256 checksum in completed part: %w", err)
|
||||
}
|
||||
parts[i] = Part{
|
||||
PartNumber: part.PartNumber,
|
||||
Size: int(part.Size),
|
||||
ChecksumSHA256: part.ETag,
|
||||
ChecksumSHA256: base64.StdEncoding.EncodeToString(checksumBytes),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
|
@ -24,11 +26,13 @@ func TestGetObjectPartsAttributes(t *testing.T) {
|
|||
multipartUpload := createMultipartUpload(hc, bktName, objMultipartName, map[string]string{})
|
||||
etag, _ := uploadPart(hc, bktName, objMultipartName, multipartUpload.UploadID, 1, partSize)
|
||||
completeMultipartUpload(hc, bktName, objMultipartName, multipartUpload.UploadID, []string{etag})
|
||||
etagBytes, err := hex.DecodeString(etag[1 : len(etag)-1])
|
||||
require.NoError(t, err)
|
||||
|
||||
result = getObjectAttributes(hc, bktName, objMultipartName, objectParts)
|
||||
require.NotNil(t, result.ObjectParts)
|
||||
require.Len(t, result.ObjectParts.Parts, 1)
|
||||
require.Equal(t, etag, result.ObjectParts.Parts[0].ChecksumSHA256)
|
||||
require.Equal(t, base64.StdEncoding.EncodeToString(etagBytes), result.ObjectParts.Parts[0].ChecksumSHA256)
|
||||
require.Equal(t, partSize, result.ObjectParts.Parts[0].Size)
|
||||
require.Equal(t, 1, result.ObjectParts.PartsCount)
|
||||
}
|
||||
|
|
70
api/handler/bucket_list.go
Normal file
70
api/handler/bucket_list.go
Normal file
|
@ -0,0 +1,70 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"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 := r.Context()
|
||||
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
|
||||
}
|
174
api/handler/bucket_list_test.go
Normal file
174
api/handler/bucket_list_test.go
Normal 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 := prepareHandlerContext(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
|
||||
}
|
|
@ -13,7 +13,6 @@ import (
|
|||
"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/internal/logs"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
@ -42,16 +41,15 @@ func path2BucketObject(path string) (string, string, error) {
|
|||
|
||||
func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var (
|
||||
err error
|
||||
versionID string
|
||||
metadata map[string]string
|
||||
tagSet map[string]string
|
||||
sessionTokenEACL *session.Container
|
||||
err error
|
||||
versionID string
|
||||
metadata map[string]string
|
||||
tagSet map[string]string
|
||||
|
||||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
|
||||
containsACL = containsACLHeaders(r)
|
||||
cannedACLStatus = aclHeadersStatus(r)
|
||||
)
|
||||
|
||||
src := r.Header.Get(api.AmzCopySource)
|
||||
|
@ -67,7 +65,7 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
srcBucket, srcObject, err := path2BucketObject(src)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "invalid source copy", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "invalid source copy", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -77,63 +75,74 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if srcObjPrm.BktInfo, err = h.getBucketAndCheckOwner(r, srcBucket, api.AmzSourceExpectedBucketOwner); err != nil {
|
||||
h.logAndSendError(w, "couldn't get source bucket", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "couldn't get source bucket", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
dstBktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "couldn't get target bucket", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "couldn't get target bucket", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
settings, err := h.obj.GetBucketSettings(ctx, dstBktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if containsACL {
|
||||
if sessionTokenEACL, err = getSessionTokenSetEACL(ctx); err != nil {
|
||||
h.logAndSendError(w, "could not get eacl session token from a box", reqInfo, err)
|
||||
return
|
||||
}
|
||||
if cannedACLStatus == aclStatusYes {
|
||||
h.logAndSendError(ctx, w, "acl not supported for this bucket", reqInfo, errors.GetAPIError(errors.ErrAccessControlListNotSupported))
|
||||
return
|
||||
}
|
||||
|
||||
extendedSrcObjInfo, err := h.obj.GetExtendedObjectInfo(ctx, srcObjPrm)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not find object", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not find object", reqInfo, err)
|
||||
return
|
||||
}
|
||||
srcObjInfo := extendedSrcObjInfo.ObjectInfo
|
||||
|
||||
encryptionParams, err := formEncryptionParams(r)
|
||||
srcEncryptionParams, err := h.formCopySourceEncryptionParams(r)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "invalid sse headers", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
|
||||
return
|
||||
}
|
||||
dstEncryptionParams, err := h.formEncryptionParams(r)
|
||||
if err != nil {
|
||||
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(srcObjInfo.Headers)); err != nil {
|
||||
h.logAndSendError(w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err))
|
||||
if err = srcEncryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(srcObjInfo.Headers)); err != nil {
|
||||
if errors.IsS3Error(err, errors.ErrInvalidEncryptionParameters) || errors.IsS3Error(err, errors.ErrSSEEncryptedObject) ||
|
||||
errors.IsS3Error(err, errors.ErrInvalidSSECustomerParameters) {
|
||||
h.logAndSendError(ctx, w, "encryption doesn't match object", reqInfo, err, zap.Error(err))
|
||||
return
|
||||
}
|
||||
h.logAndSendError(ctx, w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
if srcSize, err := layer.GetObjectSize(srcObjInfo); err != nil {
|
||||
h.logAndSendError(w, "failed to get source object size", reqInfo, err)
|
||||
var dstSize uint64
|
||||
srcSize, err := layer.GetObjectSize(srcObjInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(ctx, w, "failed to get source object size", reqInfo, err)
|
||||
return
|
||||
} else if srcSize > layer.UploadMaxSize { //https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html
|
||||
h.logAndSendError(w, "too bid object to copy with single copy operation, use multipart upload copy instead", reqInfo, errors.GetAPIError(errors.ErrInvalidRequestLargeCopy))
|
||||
} else if srcSize > layer.UploadMaxSize { // https://docs.aws.amazon.com/AmazonS3/latest/API/API_CopyObject.html
|
||||
h.logAndSendError(ctx, w, "too bid object to copy with single copy operation, use multipart upload copy instead", reqInfo, errors.GetAPIError(errors.ErrInvalidRequestLargeCopy))
|
||||
return
|
||||
}
|
||||
dstSize = srcSize
|
||||
|
||||
args, err := parseCopyObjectArgs(r.Header)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not parse request params", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not parse request params", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if isCopyingToItselfForbidden(reqInfo, srcBucket, srcObject, settings, args) {
|
||||
h.logAndSendError(w, "copying to itself without changing anything", reqInfo, errors.GetAPIError(errors.ErrInvalidCopyDest))
|
||||
h.logAndSendError(ctx, w, "copying to itself without changing anything", reqInfo, errors.GetAPIError(errors.ErrInvalidCopyDest))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -144,12 +153,12 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
if args.TaggingDirective == replaceDirective {
|
||||
tagSet, err = parseTaggingHeader(r.Header)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not parse tagging header", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not parse tagging header", reqInfo, err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
tagPrm := &layer.GetObjectTaggingParams{
|
||||
ObjectVersion: &layer.ObjectVersion{
|
||||
tagPrm := &data.GetObjectTaggingParams{
|
||||
ObjectVersion: &data.ObjectVersion{
|
||||
BktInfo: srcObjPrm.BktInfo,
|
||||
ObjectName: srcObject,
|
||||
VersionID: srcObjInfo.VersionID(),
|
||||
|
@ -159,13 +168,13 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
_, tagSet, err = h.obj.GetObjectTagging(ctx, tagPrm)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get object tagging", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get object tagging", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if err = checkPreconditions(srcObjInfo, args.Conditional); err != nil {
|
||||
h.logAndSendError(w, "precondition failed", reqInfo, errors.GetAPIError(errors.ErrPreconditionFailed))
|
||||
if err = checkPreconditions(srcObjInfo, args.Conditional, h.cfg.MD5Enabled()); err != nil {
|
||||
h.logAndSendError(ctx, w, "precondition failed", reqInfo, errors.GetAPIError(errors.ErrPreconditionFailed))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -174,69 +183,54 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
srcObjInfo.Headers[api.ContentType] = srcObjInfo.ContentType
|
||||
}
|
||||
metadata = makeCopyMap(srcObjInfo.Headers)
|
||||
delete(metadata, layer.MultipartObjectSize) // object payload will be real one rather than list of compound parts
|
||||
filterMetadataMap(metadata)
|
||||
} else if contentType := r.Header.Get(api.ContentType); len(contentType) > 0 {
|
||||
metadata[api.ContentType] = contentType
|
||||
}
|
||||
|
||||
params := &layer.CopyObjectParams{
|
||||
SrcVersioned: srcObjPrm.Versioned(),
|
||||
SrcObject: srcObjInfo,
|
||||
ScrBktInfo: srcObjPrm.BktInfo,
|
||||
DstBktInfo: dstBktInfo,
|
||||
DstObject: reqInfo.ObjectName,
|
||||
SrcSize: srcObjInfo.Size,
|
||||
Header: metadata,
|
||||
Encryption: encryptionParams,
|
||||
SrcVersioned: srcObjPrm.Versioned(),
|
||||
SrcObject: srcObjInfo,
|
||||
ScrBktInfo: srcObjPrm.BktInfo,
|
||||
DstBktInfo: dstBktInfo,
|
||||
DstObject: reqInfo.ObjectName,
|
||||
DstSize: dstSize,
|
||||
Header: metadata,
|
||||
SrcEncryption: srcEncryptionParams,
|
||||
DstEncryption: dstEncryptionParams,
|
||||
}
|
||||
|
||||
params.CopiesNumbers, err = h.pickCopiesNumbers(metadata, dstBktInfo.LocationConstraint)
|
||||
params.CopiesNumbers, err = h.pickCopiesNumbers(metadata, reqInfo.Namespace, dstBktInfo.LocationConstraint)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "invalid copies number", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "invalid copies number", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
params.Lock, err = formObjectLock(ctx, dstBktInfo, settings.LockConfiguration, r.Header)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not form object lock", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not form object lock", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
additional := []zap.Field{zap.String("src_bucket_name", srcBucket), zap.String("src_object_name", srcObject)}
|
||||
extendedDstObjInfo, err := h.obj.CopyObject(ctx, params)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "couldn't copy object", reqInfo, err, additional...)
|
||||
h.logAndSendError(ctx, w, "couldn't copy object", reqInfo, err, additional...)
|
||||
return
|
||||
}
|
||||
dstObjInfo := extendedDstObjInfo.ObjectInfo
|
||||
|
||||
if err = middleware.EncodeToResponse(w, &CopyObjectResponse{LastModified: dstObjInfo.Created.UTC().Format(time.RFC3339), ETag: dstObjInfo.HashSum}); err != nil {
|
||||
h.logAndSendError(w, "something went wrong", reqInfo, err, additional...)
|
||||
if err = middleware.EncodeToResponse(w, &CopyObjectResponse{
|
||||
LastModified: dstObjInfo.Created.UTC().Format(time.RFC3339),
|
||||
ETag: data.Quote(dstObjInfo.ETag(h.cfg.MD5Enabled())),
|
||||
}); err != nil {
|
||||
h.logAndSendError(ctx, w, "something went wrong", reqInfo, err, additional...)
|
||||
return
|
||||
}
|
||||
|
||||
if containsACL {
|
||||
newEaclTable, err := h.getNewEAclTable(r, dstBktInfo, dstObjInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get new eacl table", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
p := &layer.PutBucketACLParams{
|
||||
BktInfo: dstBktInfo,
|
||||
EACL: newEaclTable,
|
||||
SessionToken: sessionTokenEACL,
|
||||
}
|
||||
|
||||
if err = h.obj.PutBucketACL(ctx, p); err != nil {
|
||||
h.logAndSendError(w, "could not put bucket acl", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if tagSet != nil {
|
||||
tagPrm := &layer.PutObjectTaggingParams{
|
||||
ObjectVersion: &layer.ObjectVersion{
|
||||
tagPrm := &data.PutObjectTaggingParams{
|
||||
ObjectVersion: &data.ObjectVersion{
|
||||
BktInfo: dstBktInfo,
|
||||
ObjectName: reqInfo.ObjectName,
|
||||
VersionID: dstObjInfo.VersionID(),
|
||||
|
@ -244,25 +238,15 @@ func (h *handler) CopyObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
TagSet: tagSet,
|
||||
NodeVersion: extendedDstObjInfo.NodeVersion,
|
||||
}
|
||||
if _, err = h.obj.PutObjectTagging(ctx, tagPrm); err != nil {
|
||||
h.logAndSendError(w, "could not upload object tagging", reqInfo, err)
|
||||
if err = h.obj.PutObjectTagging(ctx, tagPrm); err != nil {
|
||||
h.logAndSendError(ctx, w, "could not upload object tagging", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
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))
|
||||
|
||||
s := &SendNotificationParams{
|
||||
Event: EventObjectCreatedCopy,
|
||||
NotificationInfo: data.NotificationInfoFromObject(dstObjInfo),
|
||||
BktInfo: dstBktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
if err = h.sendNotifications(ctx, s); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
|
||||
if encryptionParams.Enabled() {
|
||||
if dstEncryptionParams.Enabled() {
|
||||
addSSECHeaders(w.Header(), r.Header)
|
||||
}
|
||||
}
|
||||
|
@ -275,6 +259,13 @@ func makeCopyMap(headers map[string]string) map[string]string {
|
|||
return res
|
||||
}
|
||||
|
||||
func filterMetadataMap(metadata map[string]string) {
|
||||
delete(metadata, layer.MultipartObjectSize) // object payload will be real one rather than list of compound parts
|
||||
for key := range layer.EncryptionMetadata {
|
||||
delete(metadata, key)
|
||||
}
|
||||
}
|
||||
|
||||
func isCopyingToItselfForbidden(reqInfo *middleware.ReqInfo, srcBucket string, srcObject string, settings *data.BucketSettings, args *copyObjectArgs) bool {
|
||||
if reqInfo.BucketName != srcBucket || reqInfo.ObjectName != srcObject {
|
||||
return false
|
||||
|
@ -290,8 +281,8 @@ func isCopyingToItselfForbidden(reqInfo *middleware.ReqInfo, srcBucket string, s
|
|||
func parseCopyObjectArgs(headers http.Header) (*copyObjectArgs, error) {
|
||||
var err error
|
||||
args := &conditionalArgs{
|
||||
IfMatch: headers.Get(api.AmzCopyIfMatch),
|
||||
IfNoneMatch: headers.Get(api.AmzCopyIfNoneMatch),
|
||||
IfMatch: data.UnQuote(headers.Get(api.AmzCopyIfMatch)),
|
||||
IfNoneMatch: data.UnQuote(headers.Get(api.AmzCopyIfNoneMatch)),
|
||||
}
|
||||
|
||||
if args.IfModifiedSince, err = parseHTTPTime(headers.Get(api.AmzCopyIfModifiedSince)); err != nil {
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
package handler
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/tls"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"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/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/encryption"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -16,6 +24,7 @@ type CopyMeta struct {
|
|||
Tags map[string]string
|
||||
MetadataDirective string
|
||||
Metadata map[string]string
|
||||
Headers map[string]string
|
||||
}
|
||||
|
||||
func TestCopyWithTaggingDirective(t *testing.T) {
|
||||
|
@ -98,6 +107,165 @@ func TestCopyMultipart(t *testing.T) {
|
|||
equalDataSlices(t, data, copiedData)
|
||||
}
|
||||
|
||||
func TestCopyEncryptedToUnencrypted(t *testing.T) {
|
||||
tc := prepareHandlerContext(t)
|
||||
|
||||
bktName, srcObjName := "bucket-for-copy", "object-for-copy"
|
||||
key1 := []byte("firstencriptionkeyofsourceobject")
|
||||
key1Md5 := md5.Sum(key1)
|
||||
key2 := []byte("anotherencriptionkeysourceobject")
|
||||
key2Md5 := md5.Sum(key2)
|
||||
bktInfo := createTestBucket(tc, bktName)
|
||||
|
||||
srcEnc, err := encryption.NewParams(key1)
|
||||
require.NoError(t, err)
|
||||
srcObjInfo := createTestObject(tc, bktInfo, srcObjName, *srcEnc)
|
||||
require.True(t, containEncryptionMetadataHeaders(srcObjInfo.Headers))
|
||||
|
||||
dstObjName := "copy-object"
|
||||
|
||||
// empty copy-source-sse headers
|
||||
w, r := prepareTestRequest(tc, bktName, dstObjName, nil)
|
||||
r.TLS = &tls.ConnectionState{}
|
||||
r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
|
||||
tc.Handler().CopyObjectHandler(w, r)
|
||||
|
||||
assertStatus(t, w, http.StatusBadRequest)
|
||||
assertS3Error(t, w, errors.GetAPIError(errors.ErrSSEEncryptedObject))
|
||||
|
||||
// empty copy-source-sse-custom-key
|
||||
w, r = prepareTestRequest(tc, bktName, dstObjName, nil)
|
||||
r.TLS = &tls.ConnectionState{}
|
||||
r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
|
||||
tc.Handler().CopyObjectHandler(w, r)
|
||||
|
||||
assertStatus(t, w, http.StatusBadRequest)
|
||||
assertS3Error(t, w, errors.GetAPIError(errors.ErrMissingSSECustomerKey))
|
||||
|
||||
// empty copy-source-sse-custom-algorithm
|
||||
w, r = prepareTestRequest(tc, bktName, dstObjName, nil)
|
||||
r.TLS = &tls.ConnectionState{}
|
||||
r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key1))
|
||||
tc.Handler().CopyObjectHandler(w, r)
|
||||
|
||||
assertStatus(t, w, http.StatusBadRequest)
|
||||
assertS3Error(t, w, errors.GetAPIError(errors.ErrMissingSSECustomerAlgorithm))
|
||||
|
||||
// invalid copy-source-sse-custom-key
|
||||
w, r = prepareTestRequest(tc, bktName, dstObjName, nil)
|
||||
r.TLS = &tls.ConnectionState{}
|
||||
r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key2))
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(key2Md5[:]))
|
||||
tc.Handler().CopyObjectHandler(w, r)
|
||||
|
||||
assertStatus(t, w, http.StatusBadRequest)
|
||||
assertS3Error(t, w, errors.GetAPIError(errors.ErrInvalidSSECustomerParameters))
|
||||
|
||||
// success copy
|
||||
w, r = prepareTestRequest(tc, bktName, dstObjName, nil)
|
||||
r.TLS = &tls.ConnectionState{}
|
||||
r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key1))
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(key1Md5[:]))
|
||||
tc.Handler().CopyObjectHandler(w, r)
|
||||
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
dstObjInfo, err := tc.Layer().GetObjectInfo(tc.Context(), &layer.HeadObjectParams{BktInfo: bktInfo, Object: dstObjName})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, srcObjInfo.Headers[layer.AttributeDecryptedSize], strconv.Itoa(int(dstObjInfo.Size)))
|
||||
require.False(t, containEncryptionMetadataHeaders(dstObjInfo.Headers))
|
||||
}
|
||||
|
||||
func TestCopyUnencryptedToEncrypted(t *testing.T) {
|
||||
tc := prepareHandlerContext(t)
|
||||
|
||||
bktName, srcObjName := "bucket-for-copy", "object-for-copy"
|
||||
key := []byte("firstencriptionkeyofsourceobject")
|
||||
keyMd5 := md5.Sum(key)
|
||||
bktInfo := createTestBucket(tc, bktName)
|
||||
|
||||
srcObjInfo := createTestObject(tc, bktInfo, srcObjName, encryption.Params{})
|
||||
require.False(t, containEncryptionMetadataHeaders(srcObjInfo.Headers))
|
||||
|
||||
dstObjName := "copy-object"
|
||||
|
||||
// invalid copy-source-sse headers
|
||||
w, r := prepareTestRequest(tc, bktName, dstObjName, nil)
|
||||
r.TLS = &tls.ConnectionState{}
|
||||
r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key))
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(keyMd5[:]))
|
||||
tc.Handler().CopyObjectHandler(w, r)
|
||||
|
||||
assertStatus(t, w, http.StatusBadRequest)
|
||||
assertS3Error(t, w, errors.GetAPIError(errors.ErrInvalidEncryptionParameters))
|
||||
|
||||
// success copy
|
||||
w, r = prepareTestRequest(tc, bktName, dstObjName, nil)
|
||||
r.TLS = &tls.ConnectionState{}
|
||||
r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
|
||||
r.Header.Set(api.AmzServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
|
||||
r.Header.Set(api.AmzServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key))
|
||||
r.Header.Set(api.AmzServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(keyMd5[:]))
|
||||
tc.Handler().CopyObjectHandler(w, r)
|
||||
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
dstObjInfo, err := tc.Layer().GetObjectInfo(tc.Context(), &layer.HeadObjectParams{BktInfo: bktInfo, Object: dstObjName})
|
||||
require.NoError(t, err)
|
||||
require.True(t, containEncryptionMetadataHeaders(dstObjInfo.Headers))
|
||||
require.Equal(t, strconv.Itoa(int(srcObjInfo.Size)), dstObjInfo.Headers[layer.AttributeDecryptedSize])
|
||||
}
|
||||
|
||||
func TestCopyEncryptedToEncryptedWithAnotherKey(t *testing.T) {
|
||||
tc := prepareHandlerContext(t)
|
||||
|
||||
bktName, srcObjName := "bucket-for-copy", "object-for-copy"
|
||||
key1 := []byte("firstencriptionkeyofsourceobject")
|
||||
key1Md5 := md5.Sum(key1)
|
||||
key2 := []byte("anotherencriptionkeysourceobject")
|
||||
key2Md5 := md5.Sum(key2)
|
||||
bktInfo := createTestBucket(tc, bktName)
|
||||
|
||||
srcEnc, err := encryption.NewParams(key1)
|
||||
require.NoError(t, err)
|
||||
srcObjInfo := createTestObject(tc, bktInfo, srcObjName, *srcEnc)
|
||||
require.True(t, containEncryptionMetadataHeaders(srcObjInfo.Headers))
|
||||
|
||||
dstObjName := "copy-object"
|
||||
|
||||
w, r := prepareTestRequest(tc, bktName, dstObjName, nil)
|
||||
r.TLS = &tls.ConnectionState{}
|
||||
r.Header.Set(api.AmzCopySource, bktName+"/"+srcObjName)
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key1))
|
||||
r.Header.Set(api.AmzCopySourceServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(key1Md5[:]))
|
||||
r.Header.Set(api.AmzServerSideEncryptionCustomerAlgorithm, layer.AESEncryptionAlgorithm)
|
||||
r.Header.Set(api.AmzServerSideEncryptionCustomerKey, base64.StdEncoding.EncodeToString(key2))
|
||||
r.Header.Set(api.AmzServerSideEncryptionCustomerKeyMD5, base64.StdEncoding.EncodeToString(key2Md5[:]))
|
||||
tc.Handler().CopyObjectHandler(w, r)
|
||||
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
dstObjInfo, err := tc.Layer().GetObjectInfo(tc.Context(), &layer.HeadObjectParams{BktInfo: bktInfo, Object: dstObjName})
|
||||
require.NoError(t, err)
|
||||
require.True(t, containEncryptionMetadataHeaders(dstObjInfo.Headers))
|
||||
require.Equal(t, srcObjInfo.Headers[layer.AttributeDecryptedSize], dstObjInfo.Headers[layer.AttributeDecryptedSize])
|
||||
}
|
||||
|
||||
func containEncryptionMetadataHeaders(headers map[string]string) bool {
|
||||
for k := range headers {
|
||||
if _, ok := layer.EncryptionMetadata[k]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func copyObject(hc *handlerContext, bktName, fromObject, toObject string, copyMeta CopyMeta, statusCode int) {
|
||||
w, r := prepareTestRequest(hc, bktName, toObject, nil)
|
||||
r.Header.Set(api.AmzCopySource, bktName+"/"+fromObject)
|
||||
|
@ -114,28 +282,33 @@ func copyObject(hc *handlerContext, bktName, fromObject, toObject string, copyMe
|
|||
}
|
||||
r.Header.Set(api.AmzTagging, tagsQuery.Encode())
|
||||
|
||||
for key, val := range copyMeta.Headers {
|
||||
r.Header.Set(key, val)
|
||||
}
|
||||
|
||||
hc.Handler().CopyObjectHandler(w, r)
|
||||
assertStatus(hc.t, w, statusCode)
|
||||
}
|
||||
|
||||
func putObjectTagging(t *testing.T, tc *handlerContext, bktName, objName string, tags map[string]string) {
|
||||
body := &Tagging{
|
||||
TagSet: make([]Tag, 0, len(tags)),
|
||||
body := &data.Tagging{
|
||||
TagSet: make([]data.Tag, 0, len(tags)),
|
||||
}
|
||||
|
||||
for key, val := range tags {
|
||||
body.TagSet = append(body.TagSet, Tag{
|
||||
body.TagSet = append(body.TagSet, data.Tag{
|
||||
Key: key,
|
||||
Value: val,
|
||||
})
|
||||
}
|
||||
|
||||
w, r := prepareTestRequest(tc, bktName, objName, body)
|
||||
middleware.GetReqInfo(r.Context()).Tagging = body
|
||||
tc.Handler().PutObjectTaggingHandler(w, r)
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
}
|
||||
|
||||
func getObjectTagging(t *testing.T, tc *handlerContext, bktName, objName, version string) *Tagging {
|
||||
func getObjectTagging(t *testing.T, tc *handlerContext, bktName, objName, version string) *data.Tagging {
|
||||
query := make(url.Values)
|
||||
query.Add(api.QueryVersionID, version)
|
||||
|
||||
|
@ -143,7 +316,7 @@ func getObjectTagging(t *testing.T, tc *handlerContext, bktName, objName, versio
|
|||
tc.Handler().GetObjectTaggingHandler(w, r)
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
|
||||
tagging := &Tagging{}
|
||||
tagging := &data.Tagging{}
|
||||
err := xml.NewDecoder(w.Result().Body).Decode(tagging)
|
||||
require.NoError(t, err)
|
||||
return tagging
|
||||
|
|
|
@ -20,65 +20,73 @@ const (
|
|||
)
|
||||
|
||||
func (h *handler) GetBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
ctx := r.Context()
|
||||
reqInfo := middleware.GetReqInfo(ctx)
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
cors, err := h.obj.GetBucketCORS(r.Context(), bktInfo)
|
||||
cors, err := h.obj.GetBucketCORS(ctx, bktInfo, h.cfg.NewXMLDecoder)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get cors", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get cors", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = middleware.EncodeToResponse(w, cors); err != nil {
|
||||
h.logAndSendError(w, "could not encode cors to response", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not encode cors to response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) PutBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
ctx := r.Context()
|
||||
reqInfo := middleware.GetReqInfo(ctx)
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
p := &layer.PutCORSParams{
|
||||
BktInfo: bktInfo,
|
||||
Reader: r.Body,
|
||||
BktInfo: bktInfo,
|
||||
Reader: r.Body,
|
||||
NewDecoder: h.cfg.NewXMLDecoder,
|
||||
UserAgent: r.UserAgent(),
|
||||
}
|
||||
|
||||
p.CopiesNumbers, err = h.pickCopiesNumbers(parseMetadata(r), bktInfo.LocationConstraint)
|
||||
p.CopiesNumbers, err = h.pickCopiesNumbers(parseMetadata(r), reqInfo.Namespace, bktInfo.LocationConstraint)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "invalid copies number", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "invalid copies number", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.obj.PutBucketCORS(r.Context(), p); err != nil {
|
||||
h.logAndSendError(w, "could not put cors configuration", reqInfo, err)
|
||||
if err = h.obj.PutBucketCORS(ctx, p); err != nil {
|
||||
h.logAndSendError(ctx, w, "could not put cors configuration", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
middleware.WriteSuccessResponseHeadersOnly(w)
|
||||
if err = middleware.WriteSuccessResponseHeadersOnly(w); err != nil {
|
||||
h.logAndSendError(ctx, w, "write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) DeleteBucketCorsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
ctx := r.Context()
|
||||
reqInfo := middleware.GetReqInfo(ctx)
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = h.obj.DeleteBucketCORS(r.Context(), bktInfo); err != nil {
|
||||
h.logAndSendError(w, "could not delete cors", reqInfo, err)
|
||||
if err = h.obj.DeleteBucketCORS(ctx, bktInfo); err != nil {
|
||||
h.logAndSendError(ctx, w, "could not delete cors", reqInfo, err)
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
|
@ -98,15 +106,15 @@ func (h *handler) AppendCORSHeaders(w http.ResponseWriter, r *http.Request) {
|
|||
if reqInfo.BucketName == "" {
|
||||
return
|
||||
}
|
||||
bktInfo, err := h.obj.GetBucketInfo(ctx, reqInfo.BucketName)
|
||||
bktInfo, err := h.getBucketInfo(ctx, reqInfo.BucketName)
|
||||
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
|
||||
}
|
||||
|
||||
cors, err := h.obj.GetBucketCORS(ctx, bktInfo)
|
||||
cors, err := h.obj.GetBucketCORS(ctx, bktInfo, h.cfg.NewXMLDecoder)
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -145,21 +153,22 @@ func (h *handler) AppendCORSHeaders(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func (h *handler) Preflight(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
bktInfo, err := h.obj.GetBucketInfo(r.Context(), reqInfo.BucketName)
|
||||
ctx := r.Context()
|
||||
reqInfo := middleware.GetReqInfo(ctx)
|
||||
bktInfo, err := h.getBucketInfo(ctx, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
origin := r.Header.Get(api.Origin)
|
||||
if origin == "" {
|
||||
h.logAndSendError(w, "origin request header needed", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
|
||||
h.logAndSendError(ctx, w, "origin request header needed", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
|
||||
}
|
||||
|
||||
method := r.Header.Get(api.AccessControlRequestMethod)
|
||||
if method == "" {
|
||||
h.logAndSendError(w, "Access-Control-Request-Method request header needed", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
|
||||
h.logAndSendError(ctx, w, "Access-Control-Request-Method request header needed", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -169,9 +178,9 @@ func (h *handler) Preflight(w http.ResponseWriter, r *http.Request) {
|
|||
headers = strings.Split(requestHeaders, ", ")
|
||||
}
|
||||
|
||||
cors, err := h.obj.GetBucketCORS(r.Context(), bktInfo)
|
||||
cors, err := h.obj.GetBucketCORS(ctx, bktInfo, h.cfg.NewXMLDecoder)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get cors", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get cors", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -183,8 +192,8 @@ func (h *handler) Preflight(w http.ResponseWriter, r *http.Request) {
|
|||
if !checkSubslice(rule.AllowedHeaders, headers) {
|
||||
continue
|
||||
}
|
||||
w.Header().Set(api.AccessControlAllowOrigin, o)
|
||||
w.Header().Set(api.AccessControlAllowMethods, strings.Join(rule.AllowedMethods, ", "))
|
||||
w.Header().Set(api.AccessControlAllowOrigin, origin)
|
||||
w.Header().Set(api.AccessControlAllowMethods, method)
|
||||
if headers != nil {
|
||||
w.Header().Set(api.AccessControlAllowHeaders, requestHeaders)
|
||||
}
|
||||
|
@ -194,19 +203,22 @@ func (h *handler) Preflight(w http.ResponseWriter, r *http.Request) {
|
|||
if rule.MaxAgeSeconds > 0 || rule.MaxAgeSeconds == -1 {
|
||||
w.Header().Set(api.AccessControlMaxAge, strconv.Itoa(rule.MaxAgeSeconds))
|
||||
} else {
|
||||
w.Header().Set(api.AccessControlMaxAge, strconv.Itoa(h.cfg.DefaultMaxAge))
|
||||
w.Header().Set(api.AccessControlMaxAge, strconv.Itoa(h.cfg.DefaultMaxAge()))
|
||||
}
|
||||
if o != wildcard {
|
||||
w.Header().Set(api.AccessControlAllowCredentials, "true")
|
||||
}
|
||||
middleware.WriteSuccessResponseHeadersOnly(w)
|
||||
if err = middleware.WriteSuccessResponseHeadersOnly(w); err != nil {
|
||||
h.logAndSendError(ctx, w, "write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
h.logAndSendError(w, "Forbidden", reqInfo, errors.GetAPIError(errors.ErrAccessDenied))
|
||||
h.logAndSendError(ctx, w, "Forbidden", reqInfo, errors.GetAPIError(errors.ErrAccessDenied))
|
||||
}
|
||||
|
||||
func checkSubslice(slice []string, subSlice []string) bool {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/middleware"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCORSOriginWildcard(t *testing.T) {
|
||||
|
@ -18,19 +19,37 @@ func TestCORSOriginWildcard(t *testing.T) {
|
|||
</CORSRule>
|
||||
</CORSConfiguration>
|
||||
`
|
||||
hc := prepareHandlerContext(t)
|
||||
bodyNoXmlns := `
|
||||
<CORSConfiguration>
|
||||
<CORSRule>
|
||||
<AllowedMethod>GET</AllowedMethod>
|
||||
<AllowedOrigin>*</AllowedOrigin>
|
||||
</CORSRule>
|
||||
</CORSConfiguration>`
|
||||
hc := prepareHandlerContextWithMinCache(t)
|
||||
|
||||
bktName := "bucket-for-cors"
|
||||
box, _ := createAccessBox(t)
|
||||
w, r := prepareTestRequest(hc, bktName, "", nil)
|
||||
ctx := middleware.SetBoxData(r.Context(), box)
|
||||
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
|
||||
r = r.WithContext(ctx)
|
||||
r.Header.Add(api.AmzACL, "public-read")
|
||||
hc.Handler().CreateBucketHandler(w, r)
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
|
||||
w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(body))
|
||||
ctx = middleware.SetBoxData(r.Context(), box)
|
||||
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)
|
||||
hc.Handler().GetBucketCorsHandler(w, r)
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
|
||||
hc.config.useDefaultXMLNS = true
|
||||
w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(bodyNoXmlns))
|
||||
ctx = middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
|
||||
r = r.WithContext(ctx)
|
||||
hc.Handler().PutBucketCorsHandler(w, r)
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
|
@ -39,3 +58,181 @@ func TestCORSOriginWildcard(t *testing.T) {
|
|||
hc.Handler().GetBucketCorsHandler(w, r)
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
}
|
||||
|
||||
func TestPreflight(t *testing.T) {
|
||||
body := `
|
||||
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
|
||||
<CORSRule>
|
||||
<AllowedMethod>GET</AllowedMethod>
|
||||
<AllowedOrigin>http://www.example.com</AllowedOrigin>
|
||||
<AllowedHeader>Authorization</AllowedHeader>
|
||||
<ExposeHeader>x-amz-*</ExposeHeader>
|
||||
<ExposeHeader>X-Amz-*</ExposeHeader>
|
||||
<MaxAgeSeconds>600</MaxAgeSeconds>
|
||||
</CORSRule>
|
||||
</CORSConfiguration>
|
||||
`
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName := "bucket-preflight-test"
|
||||
box, _ := createAccessBox(t)
|
||||
w, r := prepareTestRequest(hc, bktName, "", nil)
|
||||
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
|
||||
r = r.WithContext(ctx)
|
||||
hc.Handler().CreateBucketHandler(w, r)
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
|
||||
w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(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 {
|
||||
name string
|
||||
origin string
|
||||
method string
|
||||
headers string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Valid",
|
||||
origin: "http://www.example.com",
|
||||
method: "GET",
|
||||
headers: "Authorization",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Empty origin",
|
||||
method: "GET",
|
||||
headers: "Authorization",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Empty request method",
|
||||
origin: "http://www.example.com",
|
||||
headers: "Authorization",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Not allowed method",
|
||||
origin: "http://www.example.com",
|
||||
method: "PUT",
|
||||
headers: "Authorization",
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
{
|
||||
name: "Not allowed headers",
|
||||
origin: "http://www.example.com",
|
||||
method: "GET",
|
||||
headers: "Authorization, Last-Modified",
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
w, r = prepareTestPayloadRequest(hc, bktName, "", nil)
|
||||
r.Header.Set(api.Origin, tc.origin)
|
||||
r.Header.Set(api.AccessControlRequestMethod, tc.method)
|
||||
r.Header.Set(api.AccessControlRequestHeaders, tc.headers)
|
||||
hc.Handler().Preflight(w, r)
|
||||
assertStatus(t, w, tc.expectedStatus)
|
||||
|
||||
if tc.expectedStatus == http.StatusOK {
|
||||
require.Equal(t, tc.origin, w.Header().Get(api.AccessControlAllowOrigin))
|
||||
require.Equal(t, tc.method, w.Header().Get(api.AccessControlAllowMethods))
|
||||
require.Equal(t, tc.headers, w.Header().Get(api.AccessControlAllowHeaders))
|
||||
require.Equal(t, "x-amz-*, X-Amz-*", w.Header().Get(api.AccessControlExposeHeaders))
|
||||
require.Equal(t, "true", w.Header().Get(api.AccessControlAllowCredentials))
|
||||
require.Equal(t, "600", w.Header().Get(api.AccessControlMaxAge))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreflightWildcardOrigin(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-preflight-wildcard-test"
|
||||
box, _ := createAccessBox(t)
|
||||
w, r := prepareTestRequest(hc, bktName, "", nil)
|
||||
ctx := middleware.SetBox(r.Context(), &middleware.Box{AccessBox: box})
|
||||
r = r.WithContext(ctx)
|
||||
hc.Handler().CreateBucketHandler(w, r)
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
|
||||
w, r = prepareTestPayloadRequest(hc, bktName, "", strings.NewReader(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 {
|
||||
name string
|
||||
origin string
|
||||
method string
|
||||
headers string
|
||||
expectedStatus int
|
||||
}{
|
||||
{
|
||||
name: "Valid get",
|
||||
origin: "http://www.example.com",
|
||||
method: "GET",
|
||||
headers: "Authorization, Last-Modified",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Valid put",
|
||||
origin: "http://example.com",
|
||||
method: "PUT",
|
||||
headers: "Authorization, Content-Type",
|
||||
expectedStatus: http.StatusOK,
|
||||
},
|
||||
{
|
||||
name: "Empty origin",
|
||||
method: "GET",
|
||||
headers: "Authorization, Last-Modified",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Empty request method",
|
||||
origin: "http://www.example.com",
|
||||
headers: "Authorization, Last-Modified",
|
||||
expectedStatus: http.StatusBadRequest,
|
||||
},
|
||||
{
|
||||
name: "Not allowed method",
|
||||
origin: "http://www.example.com",
|
||||
method: "DELETE",
|
||||
headers: "Authorization, Last-Modified",
|
||||
expectedStatus: http.StatusForbidden,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
w, r = prepareTestPayloadRequest(hc, bktName, "", nil)
|
||||
r.Header.Set(api.Origin, tc.origin)
|
||||
r.Header.Set(api.AccessControlRequestMethod, tc.method)
|
||||
r.Header.Set(api.AccessControlRequestHeaders, tc.headers)
|
||||
hc.Handler().Preflight(w, r)
|
||||
assertStatus(t, w, tc.expectedStatus)
|
||||
|
||||
if tc.expectedStatus == http.StatusOK {
|
||||
require.Equal(t, tc.origin, w.Header().Get(api.AccessControlAllowOrigin))
|
||||
require.Equal(t, tc.method, w.Header().Get(api.AccessControlAllowMethods))
|
||||
require.Equal(t, tc.headers, w.Header().Get(api.AccessControlAllowHeaders))
|
||||
require.Empty(t, w.Header().Get(api.AccessControlExposeHeaders))
|
||||
require.Empty(t, w.Header().Get(api.AccessControlAllowCredentials))
|
||||
require.Equal(t, "0", w.Header().Get(api.AccessControlMaxAge))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,21 +2,18 @@ package handler
|
|||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/errors"
|
||||
"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/internal/logs"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-sdk-go/session"
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
"git.frostfs.info/TrueCloudLab/policy-engine/pkg/chain"
|
||||
)
|
||||
|
||||
// limitation of AWS https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html
|
||||
|
@ -24,8 +21,9 @@ const maxObjectsToDelete = 1000
|
|||
|
||||
// DeleteObjectsRequest -- xml carrying the object key names which should be deleted.
|
||||
type DeleteObjectsRequest struct {
|
||||
XMLName xml.Name `xml:"http://s3.amazonaws.com/doc/2006-03-01/ Delete" json:"-"`
|
||||
// Element to enable quiet mode for the request
|
||||
Quiet bool
|
||||
Quiet bool `xml:"Quiet,omitempty"`
|
||||
// List of objects to be deleted
|
||||
Objects []ObjectIdentifier `xml:"Object"`
|
||||
}
|
||||
|
@ -45,10 +43,10 @@ type DeletedObject struct {
|
|||
|
||||
// DeleteError structure.
|
||||
type DeleteError struct {
|
||||
Code string
|
||||
Message string
|
||||
Key string
|
||||
VersionID string `xml:"versionId,omitempty"`
|
||||
Code string `xml:"Code,omitempty"`
|
||||
Message string `xml:"Message,omitempty"`
|
||||
Key string `xml:"Key,omitempty"`
|
||||
VersionID string `xml:"VersionId,omitempty"`
|
||||
}
|
||||
|
||||
// DeleteObjectsResponse container for multiple object deletes.
|
||||
|
@ -73,67 +71,39 @@ func (h *handler) DeleteObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
networkInfo, err := h.obj.GetNetworkInfo(ctx)
|
||||
if err != nil {
|
||||
h.logAndSendError(ctx, w, "could not get network info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
p := &layer.DeleteObjectParams{
|
||||
BktInfo: bktInfo,
|
||||
Objects: versionedObject,
|
||||
Settings: bktSettings,
|
||||
BktInfo: bktInfo,
|
||||
Objects: versionedObject,
|
||||
Settings: bktSettings,
|
||||
NetworkInfo: networkInfo,
|
||||
}
|
||||
deletedObjects := h.obj.DeleteObjects(ctx, p)
|
||||
deletedObject := deletedObjects[0]
|
||||
if deletedObject.Error != nil {
|
||||
if isErrObjectLocked(deletedObject.Error) {
|
||||
h.logAndSendError(w, "object is locked", reqInfo, errors.GetAPIError(errors.ErrAccessDenied))
|
||||
h.logAndSendError(ctx, w, "object is locked", reqInfo, errors.GetAPIError(errors.ErrAccessDenied))
|
||||
} else {
|
||||
h.logAndSendError(w, "could not delete object", reqInfo, deletedObject.Error)
|
||||
h.logAndSendError(ctx, w, "could not delete object", reqInfo, deletedObject.Error)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var m *SendNotificationParams
|
||||
|
||||
if bktSettings.VersioningEnabled() && len(versionID) == 0 {
|
||||
m = &SendNotificationParams{
|
||||
Event: EventObjectRemovedDeleteMarkerCreated,
|
||||
NotificationInfo: &data.NotificationInfo{
|
||||
Name: reqInfo.ObjectName,
|
||||
HashSum: deletedObject.DeleteMarkerEtag,
|
||||
},
|
||||
BktInfo: bktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
} else {
|
||||
var objID oid.ID
|
||||
if len(versionID) != 0 {
|
||||
if err = objID.DecodeString(versionID); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
m = &SendNotificationParams{
|
||||
Event: EventObjectRemovedDelete,
|
||||
NotificationInfo: &data.NotificationInfo{
|
||||
Name: reqInfo.ObjectName,
|
||||
Version: objID.EncodeToString(),
|
||||
},
|
||||
BktInfo: bktInfo,
|
||||
ReqInfo: reqInfo,
|
||||
}
|
||||
}
|
||||
|
||||
if err = h.sendNotifications(ctx, m); err != nil {
|
||||
h.reqLogger(ctx).Error(logs.CouldntSendNotification, zap.Error(err))
|
||||
}
|
||||
|
||||
if deletedObject.VersionID != "" {
|
||||
w.Header().Set(api.AmzVersionID, deletedObject.VersionID)
|
||||
}
|
||||
|
@ -161,41 +131,37 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
|
|||
ctx := r.Context()
|
||||
reqInfo := middleware.GetReqInfo(ctx)
|
||||
|
||||
// Content-Md5 is required and should be set
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if _, ok := r.Header[api.ContentMD5]; !ok {
|
||||
h.logAndSendError(w, "missing Content-MD5", reqInfo, errors.GetAPIError(errors.ErrMissingContentMD5))
|
||||
return
|
||||
}
|
||||
|
||||
// Content-Length is required and should be non-zero
|
||||
// http://docs.aws.amazon.com/AmazonS3/latest/API/multiobjectdeleteapi.html
|
||||
if r.ContentLength <= 0 {
|
||||
h.logAndSendError(w, "missing Content-Length", reqInfo, errors.GetAPIError(errors.ErrMissingContentLength))
|
||||
h.logAndSendError(ctx, w, "missing Content-Length", reqInfo, errors.GetAPIError(errors.ErrMissingContentLength))
|
||||
return
|
||||
}
|
||||
|
||||
// Unmarshal list of keys to be deleted.
|
||||
requested := &DeleteObjectsRequest{}
|
||||
if err := xml.NewDecoder(r.Body).Decode(requested); err != nil {
|
||||
h.logAndSendError(w, "couldn't decode body", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
|
||||
if err := h.cfg.NewXMLDecoder(r.Body, r.UserAgent()).Decode(requested); err != nil {
|
||||
h.logAndSendError(ctx, w, "couldn't decode body", reqInfo, fmt.Errorf("%w: %s", errors.GetAPIError(errors.ErrMalformedXML), err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if len(requested.Objects) == 0 || len(requested.Objects) > maxObjectsToDelete {
|
||||
h.logAndSendError(w, "number of objects to delete must be greater than 0 and less or equal to 1000", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
|
||||
h.logAndSendError(ctx, w, "number of objects to delete must be greater than 0 and less or equal to 1000", reqInfo, errors.GetAPIError(errors.ErrMalformedXML))
|
||||
return
|
||||
}
|
||||
|
||||
removed := make(map[string]*layer.VersionedObject)
|
||||
unique := make(map[string]struct{})
|
||||
toRemove := make([]*layer.VersionedObject, 0, len(requested.Objects))
|
||||
for _, obj := range requested.Objects {
|
||||
versionedObj := &layer.VersionedObject{
|
||||
Name: obj.ObjectName,
|
||||
VersionID: obj.VersionID,
|
||||
}
|
||||
toRemove = append(toRemove, versionedObj)
|
||||
removed[versionedObj.String()] = versionedObj
|
||||
key := versionedObj.String()
|
||||
if _, ok := unique[key]; !ok {
|
||||
toRemove = append(toRemove, versionedObj)
|
||||
unique[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
response := &DeleteObjectsResponse{
|
||||
|
@ -205,31 +171,31 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
|
|||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
marshaler := zapcore.ArrayMarshalerFunc(func(encoder zapcore.ArrayEncoder) error {
|
||||
for _, obj := range toRemove {
|
||||
encoder.AppendString(obj.String())
|
||||
}
|
||||
return nil
|
||||
})
|
||||
networkInfo, err := h.obj.GetNetworkInfo(ctx)
|
||||
if err != nil {
|
||||
h.logAndSendError(ctx, w, "could not get network info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
p := &layer.DeleteObjectParams{
|
||||
BktInfo: bktInfo,
|
||||
Objects: toRemove,
|
||||
Settings: bktSettings,
|
||||
BktInfo: bktInfo,
|
||||
Objects: toRemove,
|
||||
Settings: bktSettings,
|
||||
NetworkInfo: networkInfo,
|
||||
IsMultiple: true,
|
||||
}
|
||||
deletedObjects := h.obj.DeleteObjects(ctx, p)
|
||||
|
||||
var errs []error
|
||||
for _, obj := range deletedObjects {
|
||||
if obj.Error != nil {
|
||||
code := "BadRequest"
|
||||
|
@ -242,7 +208,6 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
|
|||
Key: obj.Name,
|
||||
VersionID: obj.VersionID,
|
||||
})
|
||||
errs = append(errs, obj.Error)
|
||||
} else if !requested.Quiet {
|
||||
deletedObj := DeletedObject{
|
||||
ObjectIdentifier: ObjectIdentifier{
|
||||
|
@ -257,40 +222,61 @@ func (h *handler) DeleteMultipleObjectsHandler(w http.ResponseWriter, r *http.Re
|
|||
response.DeletedObjects = append(response.DeletedObjects, deletedObj)
|
||||
}
|
||||
}
|
||||
if len(errs) != 0 {
|
||||
fields := []zap.Field{
|
||||
zap.Array("objects", marshaler),
|
||||
zap.Errors("errors", errs),
|
||||
}
|
||||
h.reqLogger(ctx).Error(logs.CouldntDeleteObjects, fields...)
|
||||
}
|
||||
|
||||
if err = middleware.EncodeToResponse(w, response); err != nil {
|
||||
h.logAndSendError(w, "could not write response", reqInfo, err, zap.Array("objects", marshaler))
|
||||
h.logAndSendError(ctx, w, "could not write response", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h *handler) DeleteBucketHandler(w http.ResponseWriter, r *http.Request) {
|
||||
reqInfo := middleware.GetReqInfo(r.Context())
|
||||
ctx := r.Context()
|
||||
reqInfo := middleware.GetReqInfo(ctx)
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = checkOwner(bktInfo, reqInfo.User); err != nil {
|
||||
h.logAndSendError(ctx, w, "request owner id does not match bucket owner id", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
var sessionToken *session.Container
|
||||
|
||||
boxData, err := middleware.GetBoxData(r.Context())
|
||||
boxData, err := middleware.GetBoxData(ctx)
|
||||
if err == nil {
|
||||
sessionToken = boxData.Gate.SessionTokenForDelete()
|
||||
}
|
||||
|
||||
if err = h.obj.DeleteBucket(r.Context(), &layer.DeleteBucketParams{
|
||||
skipObjCheck := false
|
||||
if value, ok := r.Header[api.AmzForceBucketDelete]; ok {
|
||||
s := value[0]
|
||||
if s == "true" {
|
||||
skipObjCheck = true
|
||||
}
|
||||
}
|
||||
|
||||
if err = h.obj.DeleteBucket(ctx, &layer.DeleteBucketParams{
|
||||
BktInfo: bktInfo,
|
||||
SessionToken: sessionToken,
|
||||
SkipCheck: skipObjCheck,
|
||||
}); err != nil {
|
||||
h.logAndSendError(w, "couldn't delete bucket", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "couldn't delete bucket", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
chainIDs := []chain.ID{
|
||||
getBucketChainID(chain.S3, bktInfo),
|
||||
getBucketChainID(chain.Ingress, bktInfo),
|
||||
getBucketCannedChainID(chain.S3, bktInfo.CID),
|
||||
getBucketCannedChainID(chain.Ingress, bktInfo.CID),
|
||||
}
|
||||
if err = h.ape.DeleteBucketPolicy(reqInfo.Namespace, bktInfo.CID, chainIDs); err != nil {
|
||||
h.logAndSendError(ctx, w, "failed to delete policy from storage", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,8 @@ package handler
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
|
@ -9,7 +11,8 @@ import (
|
|||
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/data"
|
||||
apiErrors "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/encryption"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -32,6 +35,7 @@ func TestDeleteBucketOnAlreadyRemovedError(t *testing.T) {
|
|||
|
||||
deleteObjects(t, hc, bktName, [][2]string{{objName, emptyVersion}})
|
||||
|
||||
hc.owner = bktInfo.Owner
|
||||
deleteBucket(t, hc, bktName, http.StatusNoContent)
|
||||
}
|
||||
|
||||
|
@ -48,11 +52,12 @@ func TestDeleteBucket(t *testing.T) {
|
|||
tc := prepareHandlerContext(t)
|
||||
|
||||
bktName, objName := "bucket-for-removal", "object-to-delete"
|
||||
_, objInfo := createVersionedBucketAndObject(t, tc, bktName, objName)
|
||||
bktInfo, objInfo := createVersionedBucketAndObject(t, tc, bktName, objName)
|
||||
|
||||
deleteMarkerVersion, isDeleteMarker := deleteObject(t, tc, bktName, objName, emptyVersion)
|
||||
require.True(t, isDeleteMarker)
|
||||
|
||||
tc.owner = bktInfo.Owner
|
||||
deleteBucket(t, tc, bktName, http.StatusConflict)
|
||||
deleteObject(t, tc, bktName, objName, objInfo.VersionID())
|
||||
deleteBucket(t, tc, bktName, http.StatusConflict)
|
||||
|
@ -77,9 +82,75 @@ func TestDeleteBucketOnNotFoundError(t *testing.T) {
|
|||
|
||||
deleteObjects(t, hc, bktName, [][2]string{{objName, emptyVersion}})
|
||||
|
||||
hc.owner = bktInfo.Owner
|
||||
deleteBucket(t, hc, bktName, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func TestForceDeleteBucket(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName, objName := "bucket-for-removal", "object-to-delete"
|
||||
bktInfo := createTestBucket(hc, bktName)
|
||||
|
||||
putObject(hc, bktName, objName)
|
||||
|
||||
nodeVersion, err := hc.tree.GetUnversioned(hc.context, bktInfo, objName)
|
||||
require.NoError(t, err)
|
||||
var addr oid.Address
|
||||
addr.SetContainer(bktInfo.CID)
|
||||
addr.SetObject(nodeVersion.OID)
|
||||
|
||||
hc.owner = bktInfo.Owner
|
||||
deleteBucketForce(t, hc, bktName, http.StatusConflict, "false")
|
||||
deleteBucketForce(t, hc, bktName, http.StatusNoContent, "true")
|
||||
}
|
||||
|
||||
func TestDeleteMultipleObjectCheckUniqueness(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName, objName := "bucket", "object"
|
||||
createTestBucket(hc, bktName)
|
||||
|
||||
putObject(hc, bktName, objName)
|
||||
|
||||
resp := deleteObjects(t, hc, bktName, [][2]string{{objName, emptyVersion}, {objName, emptyVersion}})
|
||||
require.Empty(t, resp.Errors)
|
||||
require.Len(t, resp.DeletedObjects, 1)
|
||||
}
|
||||
|
||||
func TestDeleteObjectsError(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName, objName := "bucket-for-removal", "object-to-delete"
|
||||
bktInfo := createTestBucket(hc, bktName)
|
||||
putBucketVersioning(t, hc, bktName, true)
|
||||
|
||||
putObject(hc, bktName, objName)
|
||||
|
||||
nodeVersion, err := hc.tree.GetLatestVersion(hc.context, bktInfo, objName)
|
||||
require.NoError(t, err)
|
||||
var addr oid.Address
|
||||
addr.SetContainer(bktInfo.CID)
|
||||
addr.SetObject(nodeVersion.OID)
|
||||
|
||||
expectedError := apierr.GetAPIError(apierr.ErrAccessDenied)
|
||||
hc.tp.SetObjectError(addr, expectedError)
|
||||
|
||||
w := deleteObjectsBase(hc, bktName, [][2]string{{objName, nodeVersion.OID.EncodeToString()}})
|
||||
var buf bytes.Buffer
|
||||
res := &DeleteObjectsResponse{}
|
||||
err = xml.NewDecoder(io.TeeReader(w.Result().Body, &buf)).Decode(res)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Contains(t, buf.String(), "VersionId")
|
||||
require.ElementsMatch(t, []DeleteError{{
|
||||
Code: expectedError.Code,
|
||||
Key: objName,
|
||||
Message: expectedError.Error(),
|
||||
VersionID: nodeVersion.OID.EncodeToString(),
|
||||
}}, res.Errors)
|
||||
}
|
||||
|
||||
func TestDeleteObject(t *testing.T) {
|
||||
tc := prepareHandlerContext(t)
|
||||
|
||||
|
@ -131,7 +202,7 @@ func TestDeleteDeletedObject(t *testing.T) {
|
|||
})
|
||||
|
||||
t.Run("versioned bucket not found obj", func(t *testing.T) {
|
||||
bktName, objName := "bucket-versioned-for-removal", "object-to-delete"
|
||||
bktName, objName := "bucket-versioned-for-removal-not-found", "object-to-delete"
|
||||
_, objInfo := createVersionedBucketAndObject(t, tc, bktName, objName)
|
||||
|
||||
versionID, isDeleteMarker := deleteObject(t, tc, bktName, objName, objInfo.VersionID())
|
||||
|
@ -206,6 +277,7 @@ func TestDeleteMarkerVersioned(t *testing.T) {
|
|||
deleteMarkerVersion, isDeleteMarker := deleteObject(t, tc, bktName, objName, emptyVersion)
|
||||
require.True(t, isDeleteMarker)
|
||||
versions := listVersions(t, tc, bktName)
|
||||
require.Len(t, versions.DeleteMarker, 1)
|
||||
require.Equal(t, deleteMarkerVersion, versions.DeleteMarker[0].VersionID)
|
||||
|
||||
_, isDeleteMarker = deleteObject(t, tc, bktName, objName, emptyVersion)
|
||||
|
@ -327,6 +399,27 @@ func TestDeleteMarkers(t *testing.T) {
|
|||
require.Len(t, listOIDsFromMockedFrostFS(t, tc, bktName), 0, "shouldn't be any object in frostfs")
|
||||
}
|
||||
|
||||
func TestGetHeadDeleteMarker(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName, objName := "bucket-for-removal", "object-to-delete"
|
||||
createTestBucket(hc, bktName)
|
||||
putBucketVersioning(t, hc, bktName, true)
|
||||
|
||||
putObject(hc, bktName, objName)
|
||||
|
||||
deleteMarkerVersionID, _ := deleteObject(t, hc, bktName, objName, emptyVersion)
|
||||
|
||||
w := headObjectBase(hc, bktName, objName, deleteMarkerVersionID)
|
||||
require.Equal(t, w.Code, http.StatusMethodNotAllowed)
|
||||
require.Equal(t, w.Result().Header.Get(api.AmzDeleteMarker), "true")
|
||||
|
||||
w, r := prepareTestRequest(hc, bktName, objName, nil)
|
||||
hc.Handler().GetObjectHandler(w, r)
|
||||
assertStatus(hc.t, w, http.StatusNotFound)
|
||||
require.Equal(t, w.Result().Header.Get(api.AmzDeleteMarker), "true")
|
||||
}
|
||||
|
||||
func TestDeleteObjectFromListCache(t *testing.T) {
|
||||
tc := prepareHandlerContext(t)
|
||||
|
||||
|
@ -367,25 +460,39 @@ func TestDeleteObjectCheckMarkerReturn(t *testing.T) {
|
|||
require.Equal(t, deleteMarkerVersion, deleteMarkerVersion2)
|
||||
}
|
||||
|
||||
func TestDeleteBucketByNotOwner(t *testing.T) {
|
||||
hc := prepareHandlerContext(t)
|
||||
|
||||
bktName := "bucket-name"
|
||||
bktInfo := createTestBucket(hc, bktName)
|
||||
deleteBucket(t, hc, bktName, http.StatusForbidden)
|
||||
|
||||
hc.owner = bktInfo.Owner
|
||||
deleteBucket(t, hc, bktName, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func createBucketAndObject(tc *handlerContext, bktName, objName string) (*data.BucketInfo, *data.ObjectInfo) {
|
||||
bktInfo := createTestBucket(tc, bktName)
|
||||
|
||||
objInfo := createTestObject(tc, bktInfo, objName)
|
||||
objInfo := createTestObject(tc, bktInfo, objName, encryption.Params{})
|
||||
|
||||
return bktInfo, objInfo
|
||||
}
|
||||
|
||||
func createVersionedBucketAndObject(t *testing.T, tc *handlerContext, bktName, objName string) (*data.BucketInfo, *data.ObjectInfo) {
|
||||
createTestBucket(tc, bktName)
|
||||
bktInfo, err := tc.Layer().GetBucketInfo(tc.Context(), bktName)
|
||||
require.NoError(t, err)
|
||||
putBucketVersioning(t, tc, bktName, true)
|
||||
|
||||
objInfo := createTestObject(tc, bktInfo, objName)
|
||||
func createVersionedBucketAndObject(_ *testing.T, tc *handlerContext, bktName, objName string) (*data.BucketInfo, *data.ObjectInfo) {
|
||||
bktInfo := createVersionedBucket(tc, bktName)
|
||||
objInfo := createTestObject(tc, bktInfo, objName, encryption.Params{})
|
||||
|
||||
return bktInfo, objInfo
|
||||
}
|
||||
|
||||
func createVersionedBucket(hc *handlerContext, bktName string) *data.BucketInfo {
|
||||
bktInfo := createTestBucket(hc, bktName)
|
||||
putBucketVersioning(hc.t, hc, bktName, true)
|
||||
|
||||
return bktInfo
|
||||
}
|
||||
|
||||
func putBucketVersioning(t *testing.T, tc *handlerContext, bktName string, enabled bool) {
|
||||
cfg := &VersioningConfiguration{Status: "Suspended"}
|
||||
if enabled {
|
||||
|
@ -396,6 +503,16 @@ func putBucketVersioning(t *testing.T, tc *handlerContext, bktName string, enabl
|
|||
assertStatus(t, w, http.StatusOK)
|
||||
}
|
||||
|
||||
func getBucketVersioning(hc *handlerContext, bktName string) *VersioningConfiguration {
|
||||
w, r := prepareTestRequest(hc, bktName, "", nil)
|
||||
hc.Handler().GetBucketVersioningHandler(w, r)
|
||||
assertStatus(hc.t, w, http.StatusOK)
|
||||
|
||||
res := &VersioningConfiguration{}
|
||||
parseTestResponse(hc.t, w, res)
|
||||
return res
|
||||
}
|
||||
|
||||
func deleteObject(t *testing.T, tc *handlerContext, bktName, objName, version string) (string, bool) {
|
||||
query := make(url.Values)
|
||||
query.Add(api.QueryVersionID, version)
|
||||
|
@ -408,6 +525,14 @@ func deleteObject(t *testing.T, tc *handlerContext, bktName, objName, version st
|
|||
}
|
||||
|
||||
func deleteObjects(t *testing.T, tc *handlerContext, bktName string, objVersions [][2]string) *DeleteObjectsResponse {
|
||||
w := deleteObjectsBase(tc, bktName, objVersions)
|
||||
|
||||
res := &DeleteObjectsResponse{}
|
||||
parseTestResponse(t, w, res)
|
||||
return res
|
||||
}
|
||||
|
||||
func deleteObjectsBase(hc *handlerContext, bktName string, objVersions [][2]string) *httptest.ResponseRecorder {
|
||||
req := &DeleteObjectsRequest{}
|
||||
for _, version := range objVersions {
|
||||
req.Objects = append(req.Objects, ObjectIdentifier{
|
||||
|
@ -416,14 +541,18 @@ func deleteObjects(t *testing.T, tc *handlerContext, bktName string, objVersions
|
|||
})
|
||||
}
|
||||
|
||||
w, r := prepareTestRequest(tc, bktName, "", req)
|
||||
r.Header.Set(api.ContentMD5, "")
|
||||
tc.Handler().DeleteMultipleObjectsHandler(w, r)
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
w, r := prepareTestRequest(hc, bktName, "", req)
|
||||
hc.Handler().DeleteMultipleObjectsHandler(w, r)
|
||||
assertStatus(hc.t, w, http.StatusOK)
|
||||
|
||||
res := &DeleteObjectsResponse{}
|
||||
parseTestResponse(t, w, res)
|
||||
return res
|
||||
return w
|
||||
}
|
||||
|
||||
func deleteBucketForce(t *testing.T, tc *handlerContext, bktName string, code int, value string) {
|
||||
w, r := prepareTestRequest(tc, bktName, "", nil)
|
||||
r.Header.Set(api.AmzForceBucketDelete, value)
|
||||
tc.Handler().DeleteBucketHandler(w, r)
|
||||
assertStatus(t, w, code)
|
||||
}
|
||||
|
||||
func deleteBucket(t *testing.T, tc *handlerContext, bktName string, code int) {
|
||||
|
@ -437,9 +566,9 @@ func checkNotFound(t *testing.T, hc *handlerContext, bktName, objName, version s
|
|||
assertStatus(t, w, http.StatusNotFound)
|
||||
}
|
||||
|
||||
func headObjectAssertS3Error(hc *handlerContext, bktName, objName, version string, code apiErrors.ErrorCode) {
|
||||
func headObjectAssertS3Error(hc *handlerContext, bktName, objName, version string, code apierr.ErrorCode) {
|
||||
w := headObjectBase(hc, bktName, objName, version)
|
||||
assertS3Error(hc.t, w, apiErrors.GetAPIError(code))
|
||||
assertS3Error(hc.t, w, apierr.GetAPIError(code))
|
||||
}
|
||||
|
||||
func checkFound(t *testing.T, hc *handlerContext, bktName, objName, version string) {
|
||||
|
@ -447,6 +576,18 @@ func checkFound(t *testing.T, hc *handlerContext, bktName, objName, version stri
|
|||
assertStatus(t, w, http.StatusOK)
|
||||
}
|
||||
|
||||
func headObjectWithHeaders(hc *handlerContext, bktName, objName, version string, headers map[string]string) *httptest.ResponseRecorder {
|
||||
query := make(url.Values)
|
||||
query.Add(api.QueryVersionID, version)
|
||||
|
||||
w, r := prepareTestFullRequest(hc, bktName, objName, query, nil)
|
||||
for k, v := range headers {
|
||||
r.Header.Set(k, v)
|
||||
}
|
||||
hc.Handler().HeadObjectHandler(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
func headObjectBase(hc *handlerContext, bktName, objName, version string) *httptest.ResponseRecorder {
|
||||
query := make(url.Values)
|
||||
query.Add(api.QueryVersionID, version)
|
||||
|
@ -456,13 +597,8 @@ func headObjectBase(hc *handlerContext, bktName, objName, version string) *httpt
|
|||
return w
|
||||
}
|
||||
|
||||
func listVersions(t *testing.T, tc *handlerContext, bktName string) *ListObjectsVersionsResponse {
|
||||
w, r := prepareTestRequest(tc, bktName, "", nil)
|
||||
tc.Handler().ListBucketObjectVersionsHandler(w, r)
|
||||
assertStatus(t, w, http.StatusOK)
|
||||
res := &ListObjectsVersionsResponse{}
|
||||
parseTestResponse(t, w, res)
|
||||
return res
|
||||
func listVersions(_ *testing.T, tc *handlerContext, bktName string) *ListObjectsVersionsResponse {
|
||||
return listObjectsVersions(tc, bktName, "", "", "", "", -1)
|
||||
}
|
||||
|
||||
func getVersion(resp *ListObjectsVersionsResponse, objName string) []*ObjectVersionResponse {
|
||||
|
|
|
@ -14,7 +14,9 @@ import (
|
|||
"testing"
|
||||
|
||||
"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/layer"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer/frostfs"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
|
@ -36,7 +38,7 @@ func TestSimpleGetEncrypted(t *testing.T) {
|
|||
|
||||
objInfo, err := tc.Layer().GetObjectInfo(tc.Context(), &layer.HeadObjectParams{BktInfo: bktInfo, Object: objName})
|
||||
require.NoError(t, err)
|
||||
obj, err := tc.MockedPool().ReadObject(tc.Context(), layer.PrmObjectRead{Container: bktInfo.CID, Object: objInfo.ID})
|
||||
obj, err := tc.MockedPool().GetObject(tc.Context(), frostfs.PrmObjectGet{Container: bktInfo.CID, Object: objInfo.ID})
|
||||
require.NoError(t, err)
|
||||
encryptedContent, err := io.ReadAll(obj.Payload)
|
||||
require.NoError(t, err)
|
||||
|
@ -44,6 +46,35 @@ func TestSimpleGetEncrypted(t *testing.T) {
|
|||
|
||||
response, _ := getEncryptedObject(tc, bktName, objName)
|
||||
require.Equal(t, content, string(response))
|
||||
|
||||
result := listVersions(t, tc, bktName)
|
||||
require.Len(t, result.Version, 1)
|
||||
require.Equal(t, uint64(len(content)), result.Version[0].Size)
|
||||
}
|
||||
|
||||
func TestMD5HeaderBadOrEmpty(t *testing.T) {
|
||||
tc := prepareHandlerContext(t)
|
||||
|
||||
bktName, objName := "bucket-for-sse-c", "object-to-encrypt"
|
||||
createTestBucket(tc, bktName)
|
||||
content := "content"
|
||||
|
||||
headers := map[string]string{
|
||||
api.ContentMD5: "",
|
||||
}
|
||||
putEncryptedObjectWithHeadersErr(t, tc, bktName, objName, content, headers, errors.ErrInvalidDigest)
|
||||
|
||||
headers = map[string]string{
|
||||
api.ContentMD5: "yZRvHQZYwL5V7+k2pcwHLg==",
|
||||
}
|
||||
|
||||
putEncryptedObjectWithHeadersErr(t, tc, bktName, objName, content, headers, errors.ErrBadDigest)
|
||||
|
||||
headers = map[string]string{
|
||||
api.ContentMD5: "dGhlIHF1aWNrIGJyb3dF",
|
||||
}
|
||||
|
||||
putEncryptedObjectWithHeadersErr(t, tc, bktName, objName, content, headers, errors.ErrInvalidDigest)
|
||||
}
|
||||
|
||||
func TestGetEncryptedRange(t *testing.T) {
|
||||
|
@ -234,24 +265,33 @@ func multipartUpload(hc *handlerContext, bktName, objName string, headers map[st
|
|||
}
|
||||
|
||||
func createMultipartUploadEncrypted(hc *handlerContext, bktName, objName string, headers map[string]string) *InitiateMultipartUploadResponse {
|
||||
return createMultipartUploadBase(hc, bktName, objName, true, headers)
|
||||
return createMultipartUploadOkBase(hc, bktName, objName, true, headers)
|
||||
}
|
||||
|
||||
func createMultipartUpload(hc *handlerContext, bktName, objName string, headers map[string]string) *InitiateMultipartUploadResponse {
|
||||
return createMultipartUploadBase(hc, bktName, objName, false, headers)
|
||||
return createMultipartUploadOkBase(hc, bktName, objName, false, headers)
|
||||
}
|
||||
|
||||
func createMultipartUploadBase(hc *handlerContext, bktName, objName string, encrypted bool, headers map[string]string) *InitiateMultipartUploadResponse {
|
||||
func createMultipartUploadOkBase(hc *handlerContext, bktName, objName string, encrypted bool, headers map[string]string) *InitiateMultipartUploadResponse {
|
||||
w := createMultipartUploadBase(hc, bktName, objName, encrypted, headers)
|
||||
multipartInitInfo := &InitiateMultipartUploadResponse{}
|
||||
readResponse(hc.t, w, http.StatusOK, multipartInitInfo)
|
||||
return multipartInitInfo
|
||||
}
|
||||
|
||||
func createMultipartUploadAssertS3Error(hc *handlerContext, bktName, objName string, headers map[string]string, code errors.ErrorCode) {
|
||||
w := createMultipartUploadBase(hc, bktName, objName, false, headers)
|
||||
assertS3Error(hc.t, w, errors.GetAPIError(code))
|
||||
}
|
||||
|
||||
func createMultipartUploadBase(hc *handlerContext, bktName, objName string, encrypted bool, headers map[string]string) *httptest.ResponseRecorder {
|
||||
w, r := prepareTestRequest(hc, bktName, objName, nil)
|
||||
if encrypted {
|
||||
setEncryptHeaders(r)
|
||||
}
|
||||
setHeaders(r, headers)
|
||||
hc.Handler().CreateMultipartUploadHandler(w, r)
|
||||
multipartInitInfo := &InitiateMultipartUploadResponse{}
|
||||
readResponse(hc.t, w, http.StatusOK, multipartInitInfo)
|
||||
|
||||
return multipartInitInfo
|
||||
return w
|
||||
}
|
||||
|
||||
func completeMultipartUpload(hc *handlerContext, bktName, objName, uploadID string, partsETags []string) {
|
||||
|
@ -278,6 +318,21 @@ func completeMultipartUploadBase(hc *handlerContext, bktName, objName, uploadID
|
|||
return w
|
||||
}
|
||||
|
||||
func abortMultipartUpload(hc *handlerContext, bktName, objName, uploadID string) {
|
||||
w := abortMultipartUploadBase(hc, bktName, objName, uploadID)
|
||||
assertStatus(hc.t, w, http.StatusNoContent)
|
||||
}
|
||||
|
||||
func abortMultipartUploadBase(hc *handlerContext, bktName, objName, uploadID string) *httptest.ResponseRecorder {
|
||||
query := make(url.Values)
|
||||
query.Set(uploadIDQuery, uploadID)
|
||||
|
||||
w, r := prepareTestFullRequest(hc, bktName, objName, query, nil)
|
||||
hc.Handler().AbortMultipartUploadHandler(w, r)
|
||||
|
||||
return w
|
||||
}
|
||||
|
||||
func uploadPartEncrypted(hc *handlerContext, bktName, objName, uploadID string, num, size int) (string, []byte) {
|
||||
return uploadPartBase(hc, bktName, objName, true, uploadID, num, size)
|
||||
}
|
||||
|
@ -324,6 +379,10 @@ func TestMultipartEncrypted(t *testing.T) {
|
|||
|
||||
part2Range := getEncryptedObjectRange(t, hc, bktName, objName, len(part1), len(part1)+len(part2)-1)
|
||||
require.Equal(t, part2[0:], part2Range)
|
||||
|
||||
result := listVersions(t, hc, bktName)
|
||||
require.Len(t, result.Version, 1)
|
||||
require.EqualValues(t, uint64(partSize+5), result.Version[0].Size)
|
||||
}
|
||||
|
||||
func putEncryptedObject(t *testing.T, tc *handlerContext, bktName, objName, content string) {
|
||||
|
@ -334,6 +393,15 @@ func putEncryptedObject(t *testing.T, tc *handlerContext, bktName, objName, cont
|
|||
assertStatus(t, w, http.StatusOK)
|
||||
}
|
||||
|
||||
func putEncryptedObjectWithHeadersErr(t *testing.T, tc *handlerContext, bktName, objName, content string, headers map[string]string, code errors.ErrorCode) {
|
||||
body := bytes.NewReader([]byte(content))
|
||||
w, r := prepareTestPayloadRequest(tc, bktName, objName, body)
|
||||
setHeaders(r, headers)
|
||||
|
||||
tc.Handler().PutObjectHandler(w, r)
|
||||
assertS3Error(t, w, errors.GetAPIError(code))
|
||||
}
|
||||
|
||||
func getEncryptedObject(hc *handlerContext, bktName, objName string) ([]byte, http.Header) {
|
||||
w, r := prepareTestRequest(hc, bktName, objName, nil)
|
||||
setEncryptHeaders(r)
|
||||
|
@ -345,6 +413,15 @@ func getObject(hc *handlerContext, bktName, objName string) ([]byte, http.Header
|
|||
return getObjectBase(hc, w, r)
|
||||
}
|
||||
|
||||
func getObjectWithHeaders(hc *handlerContext, bktName, objName string, headers map[string]string) *httptest.ResponseRecorder {
|
||||
w, r := prepareTestRequest(hc, bktName, objName, nil)
|
||||
for k, v := range headers {
|
||||
r.Header.Set(k, v)
|
||||
}
|
||||
hc.Handler().GetObjectHandler(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
func getObjectBase(hc *handlerContext, w *httptest.ResponseRecorder, r *http.Request) ([]byte, http.Header) {
|
||||
hc.Handler().GetObjectHandler(w, r)
|
||||
assertStatus(hc.t, w, http.StatusOK)
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"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"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/internal/logs"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
|
@ -78,7 +79,29 @@ func addSSECHeaders(responseHeader http.Header, requestHeader http.Header) {
|
|||
responseHeader.Set(api.AmzServerSideEncryptionCustomerKeyMD5, requestHeader.Get(api.AmzServerSideEncryptionCustomerKeyMD5))
|
||||
}
|
||||
|
||||
func writeHeaders(h http.Header, requestHeader http.Header, extendedInfo *data.ExtendedObjectInfo, tagSetLength int, isBucketUnversioned bool) {
|
||||
func writeNotModifiedHeaders(h http.Header, extendedInfo *data.ExtendedObjectInfo, tagSetLength int, isBucketUnversioned, md5Enabled bool) {
|
||||
h.Set(api.ETag, data.Quote(extendedInfo.ObjectInfo.ETag(md5Enabled)))
|
||||
h.Set(api.LastModified, extendedInfo.ObjectInfo.Created.UTC().Format(http.TimeFormat))
|
||||
h.Set(api.AmzTaggingCount, strconv.Itoa(tagSetLength))
|
||||
|
||||
if !isBucketUnversioned {
|
||||
h.Set(api.AmzVersionID, extendedInfo.Version())
|
||||
}
|
||||
|
||||
if cacheControl := extendedInfo.ObjectInfo.Headers[api.CacheControl]; cacheControl != "" {
|
||||
h.Set(api.CacheControl, cacheControl)
|
||||
}
|
||||
|
||||
for key, val := range extendedInfo.ObjectInfo.Headers {
|
||||
if layer.IsSystemHeader(key) {
|
||||
continue
|
||||
}
|
||||
h[api.MetadataPrefix+key] = []string{val}
|
||||
}
|
||||
}
|
||||
|
||||
func writeHeaders(h http.Header, requestHeader http.Header, extendedInfo *data.ExtendedObjectInfo, tagSetLength int,
|
||||
isBucketUnversioned, md5Enabled bool) {
|
||||
info := extendedInfo.ObjectInfo
|
||||
if len(info.ContentType) > 0 && h.Get(api.ContentType) == "" {
|
||||
h.Set(api.ContentType, info.ContentType)
|
||||
|
@ -94,8 +117,10 @@ func writeHeaders(h http.Header, requestHeader http.Header, extendedInfo *data.E
|
|||
h.Set(api.ContentLength, strconv.FormatUint(info.Size, 10))
|
||||
}
|
||||
|
||||
h.Set(api.ETag, info.HashSum)
|
||||
h.Set(api.ETag, data.Quote(info.ETag(md5Enabled)))
|
||||
|
||||
h.Set(api.AmzTaggingCount, strconv.Itoa(tagSetLength))
|
||||
h.Set(api.AmzStorageClass, api.DefaultStorageClass)
|
||||
|
||||
if !isBucketUnversioned {
|
||||
h.Set(api.AmzVersionID, extendedInfo.Version())
|
||||
|
@ -110,6 +135,9 @@ func writeHeaders(h http.Header, requestHeader http.Header, extendedInfo *data.E
|
|||
if encodings := info.Headers[api.ContentEncoding]; encodings != "" {
|
||||
h.Set(api.ContentEncoding, encodings)
|
||||
}
|
||||
if contentLanguage := info.Headers[api.ContentLanguage]; contentLanguage != "" {
|
||||
h.Set(api.ContentLanguage, contentLanguage)
|
||||
}
|
||||
|
||||
for key, val := range info.Headers {
|
||||
if layer.IsSystemHeader(key) {
|
||||
|
@ -123,18 +151,15 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
var (
|
||||
params *layer.RangeParams
|
||||
|
||||
reqInfo = middleware.GetReqInfo(r.Context())
|
||||
ctx = r.Context()
|
||||
reqInfo = middleware.GetReqInfo(ctx)
|
||||
)
|
||||
|
||||
conditional, err := parseConditionalHeaders(r.Header)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not parse request params", reqInfo, err)
|
||||
return
|
||||
}
|
||||
conditional := parseConditionalHeaders(r.Header, h.reqLogger(ctx))
|
||||
|
||||
bktInfo, err := h.getBucketAndCheckOwner(r, reqInfo.BucketName)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket info", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -144,64 +169,67 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
VersionID: reqInfo.URL.Query().Get(api.QueryVersionID),
|
||||
}
|
||||
|
||||
extendedInfo, err := h.obj.GetExtendedObjectInfo(r.Context(), p)
|
||||
extendedInfo, err := h.obj.GetExtendedObjectInfo(ctx, p)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not find object", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not find object", reqInfo, err)
|
||||
return
|
||||
}
|
||||
info := extendedInfo.ObjectInfo
|
||||
|
||||
if err = checkPreconditions(info, conditional); err != nil {
|
||||
h.logAndSendError(w, "precondition failed", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
encryptionParams, err := formEncryptionParams(r)
|
||||
bktSettings, err := h.obj.GetBucketSettings(ctx, bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "invalid sse headers", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get bucket settings", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(info.Headers)); err != nil {
|
||||
h.logAndSendError(w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
fullSize, err := layer.GetObjectSize(info)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "invalid size header", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
if params, err = fetchRangeHeader(r.Header, fullSize); err != nil {
|
||||
h.logAndSendError(w, "could not parse range header", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
t := &layer.ObjectVersion{
|
||||
t := &data.ObjectVersion{
|
||||
BktInfo: bktInfo,
|
||||
ObjectName: info.Name,
|
||||
VersionID: info.VersionID(),
|
||||
}
|
||||
|
||||
tagSet, lockInfo, err := h.obj.GetObjectTaggingAndLock(r.Context(), t, extendedInfo.NodeVersion)
|
||||
tagSet, lockInfo, err := h.obj.GetObjectTaggingAndLock(ctx, t, extendedInfo.NodeVersion)
|
||||
if err != nil && !errors.IsS3Error(err, errors.ErrNoSuchKey) {
|
||||
h.logAndSendError(w, "could not get object meta data", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get object meta data", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if layer.IsAuthenticatedRequest(r.Context()) {
|
||||
if err = checkPreconditions(info, conditional, h.cfg.MD5Enabled()); err != nil {
|
||||
if errors.IsS3Error(err, errors.ErrNotModified) {
|
||||
writeNotModifiedHeaders(w.Header(), extendedInfo, len(tagSet), bktSettings.Unversioned(), h.cfg.MD5Enabled())
|
||||
}
|
||||
h.logAndSendError(ctx, w, "precondition failed", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
encryptionParams, err := h.formEncryptionParams(r)
|
||||
if err != nil {
|
||||
h.logAndSendError(ctx, w, "invalid sse headers", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = encryptionParams.MatchObjectEncryption(layer.FormEncryptionInfo(info.Headers)); err != nil {
|
||||
h.logAndSendError(ctx, w, "encryption doesn't match object", reqInfo, errors.GetAPIError(errors.ErrBadRequest), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
fullSize, err := layer.GetObjectSize(info)
|
||||
if err != nil {
|
||||
h.logAndSendError(ctx, w, "invalid size header", reqInfo, errors.GetAPIError(errors.ErrBadRequest))
|
||||
return
|
||||
}
|
||||
|
||||
if params, err = fetchRangeHeader(r.Header, fullSize); err != nil {
|
||||
h.logAndSendError(ctx, w, "could not parse range header", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
if layer.IsAuthenticatedRequest(ctx) {
|
||||
overrideResponseHeaders(w.Header(), reqInfo.URL.Query())
|
||||
}
|
||||
|
||||
if err = h.setLockingHeaders(bktInfo, lockInfo, w.Header()); err != nil {
|
||||
h.logAndSendError(w, "could not get locking info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
bktSettings, err := h.obj.GetBucketSettings(r.Context(), bktInfo)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get bucket settings", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get locking info", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -213,13 +241,13 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
Encryption: encryptionParams,
|
||||
}
|
||||
|
||||
objPayload, err := h.obj.GetObject(r.Context(), getPayloadParams)
|
||||
objPayload, err := h.obj.GetObject(ctx, getPayloadParams)
|
||||
if err != nil {
|
||||
h.logAndSendError(w, "could not get object payload", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not get object payload", reqInfo, err)
|
||||
return
|
||||
}
|
||||
|
||||
writeHeaders(w.Header(), r.Header, extendedInfo, len(tagSet), bktSettings.Unversioned())
|
||||
writeHeaders(w.Header(), r.Header, extendedInfo, len(tagSet), bktSettings.Unversioned(), h.cfg.MD5Enabled())
|
||||
if params != nil {
|
||||
writeRangeHeaders(w, params, fullSize)
|
||||
} else {
|
||||
|
@ -227,17 +255,18 @@ func (h *handler) GetObjectHandler(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
if err = objPayload.StreamTo(w); err != nil {
|
||||
h.logAndSendError(w, "could not stream object payload", reqInfo, err)
|
||||
h.logAndSendError(ctx, w, "could not stream object payload", reqInfo, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func checkPreconditions(info *data.ObjectInfo, args *conditionalArgs) error {
|
||||
if len(args.IfMatch) > 0 && args.IfMatch != info.HashSum {
|
||||
return fmt.Errorf("%w: etag mismatched: '%s', '%s'", errors.GetAPIError(errors.ErrPreconditionFailed), args.IfMatch, info.HashSum)
|
||||
func checkPreconditions(info *data.ObjectInfo, args *conditionalArgs, md5Enabled bool) error {
|
||||
etag := info.ETag(md5Enabled)
|
||||
if len(args.IfMatch) > 0 && args.IfMatch != etag {
|
||||
return fmt.Errorf("%w: etag mismatched: '%s', '%s'", errors.GetAPIError(errors.ErrPreconditionFailed), args.IfMatch, etag)
|
||||
}
|
||||
if len(args.IfNoneMatch) > 0 && args.IfNoneMatch == info.HashSum {
|
||||
return fmt.Errorf("%w: etag matched: '%s', '%s'", errors.GetAPIError(errors.ErrNotModified), args.IfNoneMatch, info.HashSum)
|
||||
if len(args.IfNoneMatch) > 0 && args.IfNoneMatch == etag {
|
||||
return fmt.Errorf("%w: etag matched: '%s', '%s'", errors.GetAPIError(errors.ErrNotModified), args.IfNoneMatch, etag)
|
||||
}
|
||||
if args.IfModifiedSince != nil && info.Created.Before(*args.IfModifiedSince) {
|
||||
return fmt.Errorf("%w: not modified since '%s', last modified '%s'", errors.GetAPIError(errors.ErrNotModified),
|
||||
|
@ -258,21 +287,24 @@ func checkPreconditions(info *data.ObjectInfo, args *conditionalArgs) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func parseConditionalHeaders(headers http.Header) (*conditionalArgs, error) {
|
||||
var err error
|
||||
func parseConditionalHeaders(headers http.Header, log *zap.Logger) *conditionalArgs {
|
||||
args := &conditionalArgs{
|
||||
IfMatch: strings.Trim(headers.Get(api.IfMatch), "\""),
|
||||
IfNoneMatch: strings.Trim(headers.Get(api.IfNoneMatch), "\""),
|
||||
IfMatch: data.UnQuote(headers.Get(api.IfMatch)),
|
||||
IfNoneMatch: data.UnQuote(headers.Get(api.IfNoneMatch)),
|
||||
}
|
||||
|
||||
if args.IfModifiedSince, err = parseHTTPTime(headers.Get(api.IfModifiedSince)); err != nil {
|
||||
return nil, err
|
||||
if httpTime, err := parseHTTPTime(headers.Get(api.IfModifiedSince)); err == nil {
|
||||
args.IfModifiedSince = httpTime
|
||||
} else {
|
||||
log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfModifiedSince, headers.Get(api.IfModifiedSince)), zap.Error(err), logs.TagField(logs.TagDatapath))
|
||||
}
|
||||
if args.IfUnmodifiedSince, err = parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err != nil {
|
||||
return nil, err
|
||||
if httpTime, err := parseHTTPTime(headers.Get(api.IfUnmodifiedSince)); err == nil {
|
||||
args.IfUnmodifiedSince = httpTime
|
||||
} else {
|
||||
log.Warn(logs.FailedToParseHTTPTime, zap.String(api.IfUnmodifiedSince, headers.Get(api.IfUnmodifiedSince)), zap.Error(err), logs.TagField(logs.TagDatapath))
|
||||
}
|
||||
|
||||
return args, nil
|
||||
return args
|
||||
}
|
||||
|
||||
func parseHTTPTime(data string) (*time.Time, error) {
|
||||
|
|
|
@ -2,7 +2,7 @@ package handler
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
stderrors "errors"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
@ -13,7 +13,7 @@ import (
|
|||
|
||||
"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/errors"
|
||||
apierr "git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/errors"
|
||||
"git.frostfs.info/TrueCloudLab/frostfs-s3-gw/api/layer"
|
||||
apistatus "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/client/status"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
@ -89,7 +89,7 @@ func TestPreconditions(t *testing.T) {
|
|||
name: "IfMatch false",
|
||||
info: newInfo(etag, today),
|
||||
args: &conditionalArgs{IfMatch: etag2},
|
||||
expected: errors.GetAPIError(errors.ErrPreconditionFailed)},
|
||||
expected: apierr.GetAPIError(apierr.ErrPreconditionFailed)},
|
||||
{
|
||||
name: "IfNoneMatch true",
|
||||
info: newInfo(etag, today),
|
||||
|
@ -99,7 +99,7 @@ func TestPreconditions(t *testing.T) {
|
|||
name: "IfNoneMatch false",
|
||||
info: newInfo(etag, today),
|
||||
args: &conditionalArgs{IfNoneMatch: etag},
|
||||
expected: errors.GetAPIError(errors.ErrNotModified)},
|
||||
expected: apierr.GetAPIError(apierr.ErrNotModified)},
|
||||
{
|
||||
name: "IfModifiedSince true",
|
||||
info: newInfo(etag, today),
|
||||
|
@ -109,7 +109,7 @@ func TestPreconditions(t *testing.T) {
|
|||
name: "IfModifiedSince false",
|
||||
info: newInfo(etag, yesterday),
|
||||
args: &conditionalArgs{IfModifiedSince: &today},
|
||||
expected: errors.GetAPIError(errors.ErrNotModified)},
|
||||
expected: apierr.GetAPIError(apierr.ErrNotModified)},
|
||||
{
|
||||
name: "IfUnmodifiedSince true",
|
||||
info: newInfo(etag, yesterday),
|
||||
|
@ -119,7 +119,7 @@ func TestPreconditions(t *testing.T) {
|
|||
name: "IfUnmodifiedSince false",
|
||||
info: newInfo(etag, today),
|
||||
args: &conditionalArgs{IfUnmodifiedSince: &yesterday},
|
||||
expected: errors.GetAPIError(errors.ErrPreconditionFailed)},
|
||||
expected: apierr.GetAPIError(apierr.ErrPreconditionFailed)},
|
||||
|
||||
{
|
||||
name: "IfMatch true, IfUnmodifiedSince false",
|
||||
|
@ -131,27 +131,27 @@ func TestPreconditions(t *testing.T) {
|
|||
name: "IfMatch false, IfUnmodifiedSince true",
|
||||
info: newInfo(etag, yesterday),
|
||||
args: &conditionalArgs{IfMatch: etag2, IfUnmodifiedSince: &today},
|
||||
expected: errors.GetAPIError(errors.ErrPreconditionFailed),
|
||||
expected: apierr.GetAPIError(apierr.ErrPreconditionFailed),
|
||||
},
|
||||
{
|
||||
name: "IfNoneMatch false, IfModifiedSince true",
|
||||
info: newInfo(etag, today),
|
||||
args: &conditionalArgs{IfNoneMatch: etag, IfModifiedSince: &yesterday},
|
||||
expected: errors.GetAPIError(errors.ErrNotModified),
|
||||
expected: apierr.GetAPIError(apierr.ErrNotModified),
|
||||
},
|
||||
{
|
||||
name: "IfNoneMatch true, IfModifiedSince false",
|
||||
info: newInfo(etag, yesterday),
|
||||
args: &conditionalArgs{IfNoneMatch: etag2, IfModifiedSince: &today},
|
||||
expected: errors.GetAPIError(errors.ErrNotModified),
|
||||
expected: apierr.GetAPIError(apierr.ErrNotModified),
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := checkPreconditions(tc.info, tc.args)
|
||||
actual := checkPreconditions(tc.info, tc.args, false)
|
||||
if tc.expected == nil {
|
||||
require.NoError(t, actual)
|
||||
} else {
|
||||
require.True(t, stderrors.Is(actual, tc.expected), tc.expected, actual)
|
||||
require.True(t, errors.Is(actual, tc.expected), tc.expected, actual)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -193,15 +193,77 @@ func TestGetObject(t *testing.T) {
|
|||
hc.tp.SetObjectError(addr, &apistatus.ObjectNotFound{})
|
||||
hc.tp.SetObjectError(objInfo.Address(), &apistatus.ObjectNotFound{})
|
||||
|
||||
getObjectAssertS3Error(hc, bktName, objName, objInfo.VersionID(), errors.ErrNoSuchVersion)
|
||||
getObjectAssertS3Error(hc, bktName, objName, emptyVersion, errors.ErrNoSuchKey)
|
||||
getObjectAssertS3Error(hc, bktName, objName, objInfo.VersionID(), apierr.ErrNoSuchVersion)
|
||||
getObjectAssertS3Error(hc, bktName, objName, emptyVersion, apierr.ErrNoSuchKey)
|
||||
}
|
||||
|
||||
func putObjectContent(hc *handlerContext, bktName, objName, content string) {
|
||||
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) {
|
||||
hc := prepareHandlerContext(t)
|
||||
bktName, objName := "bucket", "obj"
|
||||
_, objInfo := createBucketAndObject(hc, bktName, objName)
|
||||
|
||||
_, headers := getObject(hc, bktName, objName)
|
||||
require.Equal(t, data.Quote(objInfo.HashSum), headers.Get(api.ETag))
|
||||
|
||||
hc.config.md5Enabled = true
|
||||
_, headers = getObject(hc, bktName, objName)
|
||||
require.Equal(t, data.Quote(objInfo.MD5Sum), headers.Get(api.ETag))
|
||||
}
|
||||
|
||||
func TestGetObjectNotModifiedHeaders(t *testing.T) {
|
||||
hc := prepareHandlerContextWithMinCache(t)
|
||||
bktName, objName, metadataHeader := "bucket", "obj", api.MetadataPrefix+"header"
|
||||
createVersionedBucket(hc, bktName)
|
||||
header := putObjectWithHeaders(hc, bktName, objName, map[string]string{api.CacheControl: "value", metadataHeader: "value"})
|
||||
etag, versionID := header.Get(api.ETag), header.Get(api.AmzVersionID)
|
||||
require.NotEmpty(t, etag)
|
||||
require.NotEmpty(t, versionID)
|
||||
|
||||
putObjectTagging(t, hc, bktName, objName, map[string]string{"key": "value"})
|
||||
|
||||
w := getObjectWithHeaders(hc, bktName, objName, map[string]string{api.IfNoneMatch: etag})
|
||||
require.Equal(t, http.StatusNotModified, w.Code)
|
||||
require.Equal(t, "1", w.Header().Get(api.AmzTaggingCount))
|
||||
require.Equal(t, etag, w.Header().Get(api.ETag))
|
||||
require.NotEmpty(t, w.Header().Get(api.LastModified))
|
||||
require.Equal(t, versionID, w.Header().Get(api.AmzVersionID))
|
||||
require.Equal(t, "value", w.Header().Get(api.CacheControl))
|
||||
require.Equal(t, []string{"value"}, w.Header()[metadataHeader])
|
||||
}
|
||||
|
||||
func putObjectContent(hc *handlerContext, bktName, objName, content string) http.Header {
|
||||
body := bytes.NewReader([]byte(content))
|
||||
w, r := prepareTestPayloadRequest(hc, bktName, objName, body)
|
||||
hc.Handler().PutObjectHandler(w, r)
|
||||
assertStatus(hc.t, w, http.StatusOK)
|
||||
return w.Result().Header
|
||||
}
|
||||
|
||||
func getObjectRange(t *testing.T, tc *handlerContext, bktName, objName string, start, end int) []byte {
|
||||
|
@ -214,9 +276,17 @@ func getObjectRange(t *testing.T, tc *handlerContext, bktName, objName string, s
|
|||
return content
|
||||
}
|
||||
|
||||
func getObjectAssertS3Error(hc *handlerContext, bktName, objName, version string, code errors.ErrorCode) {
|
||||
func getObjectVersion(tc *handlerContext, bktName, objName, version string) []byte {
|
||||
w := getObjectBaseResponse(tc, bktName, objName, version)
|
||||
assertStatus(tc.t, w, http.StatusOK)
|
||||
content, err := io.ReadAll(w.Result().Body)
|
||||
require.NoError(tc.t, err)
|
||||
return content
|
||||
}
|
||||
|
||||
func getObjectAssertS3Error(hc *handlerContext, bktName, objName, version string, code apierr.ErrorCode) {
|
||||
w := getObjectBaseResponse(hc, bktName, objName, version)
|
||||
assertS3Error(hc.t, w, errors.GetAPIError(code))
|
||||
assertS3Error(hc.t, w, apierr.GetAPIError(code))
|
||||
}
|
||||
|
||||
func getObjectBaseResponse(hc *handlerContext, bktName, objName, version string) *httptest.ResponseRecorder {
|
||||
|
|
1025
api/handler/handler_fuzz_test.go
Normal file
1025
api/handler/handler_fuzz_test.go
Normal file
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue