Skip to content

Commit 7fd1750

Browse files
committed
feat: Add HEAD request support
Close #474
1 parent 5968733 commit 7fd1750

File tree

24 files changed

+2748
-82
lines changed

24 files changed

+2748
-82
lines changed

conf/config-example.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,14 @@ targets:
348348
# url: http://localhost:8181/v1/data/example/authz/allowed
349349
# ## Actions
350350
# actions:
351+
# # Action for HEAD requests on target
352+
# HEAD:
353+
# # Will allow HEAD requests
354+
# enabled: true
355+
# # Configuration for HEAD requests
356+
# config:
357+
# # Webhooks
358+
# webhooks: []
351359
# # Action for GET requests on target
352360
# GET:
353361
# # Will allow GET requests

docs/configuration/example.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,14 @@ targets:
358358
# url: http://localhost:8181/v1/data/example/authz/allowed
359359
# ## Actions
360360
# actions:
361+
# # Action for HEAD requests on target
362+
# HEAD:
363+
# # Will allow HEAD requests
364+
# enabled: true
365+
# # Configuration for HEAD requests
366+
# config:
367+
# # Webhooks
368+
# webhooks: []
361369
# # Action for GET requests on target
362370
# GET:
363371
# # Will allow GET requests

docs/configuration/structure.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,10 +231,24 @@ See more information [here](../feature-guide/key-rewrite.md).
231231

232232
| Key | Type | Required | Default | Description |
233233
| ------ | ------------------------------------------------------- | -------- | ------- | -------------------------------------------------- |
234+
| HEAD | [HeadActionConfiguration](#headactionconfiguration) | No | None | Action configuration for HEAD requests on target |
234235
| GET | [GetActionConfiguration](#getactionconfiguration) | No | None | Action configuration for GET requests on target |
235236
| PUT | [PutActionConfiguration](#putactionconfiguration) | No | None | Action configuration for PUT requests on target |
236237
| DELETE | [DeleteActionConfiguration](#deleteactionconfiguration) | No | None | Action configuration for DELETE requests on target |
237238

239+
## HeadActionConfiguration
240+
241+
| Key | Type | Required | Default | Description |
242+
| ------- | ----------------------------------------------------------------- | -------- | ------- | ------------------------------- |
243+
| enabled | Boolean | No | `false` | Will allow HEAD requests |
244+
| config | [HeadActionConfigConfiguration](#deleteactionconfigconfiguration) | No | None | Configuration for HEAD requests |
245+
246+
## HeadActionConfigConfiguration
247+
248+
| Key | Type | Required | Default | Description |
249+
| -------- | ----------------------------------------------- | -------- | ------- | -------------------------------------------------------------------- |
250+
| webhooks | [[WebhookConfiguration](#webhookconfiguration)] | No | `nil` | Webhooks configuration list to call when a HEAD request is performed |
251+
238252
## GetActionConfiguration
239253

240254
| Key | Type | Required | Default | Description |

docs/feature-guide/api.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ There is 2 different management cases:
1111

1212
- If path doesn't end with a slash, the backend will consider this as a file request. Example: `GET /file.pdf`
1313

14+
## HEAD
15+
16+
Those kind of requests is similar to `GET` ones but won't provide any result body.
17+
18+
There are working the same way for management cases for directories (eg: `HEAD /dir1/`) or files (eg: `HEAD /file.pdf`).
19+
1420
## PUT
1521

1622
This kind of requests will allow to send file in directory (so to upload a file in S3).

docs/feature-guide/webhooks.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ The main body is called the [HookBody](#hookbody).
2727
Here are all cased for input metadata:
2828

2929
- GET: [GetInputMetadataHookBody](#getinputmetadatahookbody)
30+
- HEAD: [HeadInputMetadataHookBody](#headinputmetadatahookbody)
3031
- PUT: [PutInputMetadataHookBody](#putinputmetadatahookbody)
3132
- DELETE: [DeleteInputMetadataHookBody](#deleteinputmetadatahookbody)
3233

@@ -55,6 +56,15 @@ Here are all cased for input metadata:
5556
| ----- | ------ | -------------------------------- |
5657
| name | String | Target name matching the request |
5758

59+
### HeadInputMetadataHookBody
60+
61+
| Field | Type | Description |
62+
| ----------------- | ------ | ---------------------------------- |
63+
| ifModifiedSince | String | `If-Modified-Since` header value |
64+
| ifMatch | String | `If-Match` header value |
65+
| ifNoneMatch | String | `If-None-Match` header value |
66+
| ifUnmodifiedSince | String | `If-Unmodified-Since` header value |
67+
5868
### GetInputMetadataHookBody
5969

6070
| Field | Type | Description |

pkg/s3-proxy/bucket/bucket-req-impl.go

Lines changed: 116 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,17 @@ func (bri *bucketReqImpl) manageKeyRewrite(ctx context.Context, key string) (str
139139
return key, nil
140140
}
141141

142-
// Get proxy GET requests.
142+
// Proxy GET requests.
143143
func (bri *bucketReqImpl) Get(ctx context.Context, input *GetInput) {
144+
bri.internalGetOrHead(ctx, input, false)
145+
}
146+
147+
// Proxy HEAD requests.
148+
func (bri *bucketReqImpl) Head(ctx context.Context, input *GetInput) {
149+
bri.internalGetOrHead(ctx, input, true)
150+
}
151+
152+
func (bri *bucketReqImpl) internalGetOrHead(ctx context.Context, input *GetInput, isHeadReq bool) {
144153
// Get response handler
145154
resHan := responsehandler.GetResponseHandlerFromContext(ctx)
146155

@@ -157,31 +166,36 @@ func (bri *bucketReqImpl) Get(ctx context.Context, input *GetInput) {
157166

158167
// Check that the path ends with a / for a directory listing or the main path special case (empty path)
159168
if strings.HasSuffix(input.RequestPath, "/") || input.RequestPath == "" {
160-
bri.manageGetFolder(ctx, key, input)
169+
bri.manageGetFolder(ctx, key, input, isHeadReq)
161170
// Stop
162171
return
163172
}
164173

165-
// Get object case
174+
// Get or Head object case
166175

167-
// Check if it is asked to redirect to signed url
168-
if bri.targetCfg.Actions != nil &&
176+
// Check if it is a HEAD request or if it is asked to redirect to signed url
177+
if isHeadReq || bri.targetCfg.Actions != nil &&
169178
bri.targetCfg.Actions.GET != nil &&
170179
bri.targetCfg.Actions.GET.Config != nil &&
171180
bri.targetCfg.Actions.GET.Config.RedirectToSignedURL {
172181
// Get S3 client
173182
s3cl := bri.s3ClientManager.
174183
GetClientForTarget(bri.targetCfg.Name)
175184
// Head file in bucket
176-
headOutput, err2 := s3cl.HeadObject(ctx, key)
185+
headOutput, hInfo, err2 := s3cl.HeadObject(ctx, key)
177186
// Check if there is an error
178187
if err2 != nil {
179188
// Save error
180189
err = err2
181190
} else if headOutput != nil {
182191
// File found
183-
// Redirect to signed url
184-
err = bri.redirectToSignedURL(ctx, key, input)
192+
// Check head request
193+
if isHeadReq {
194+
err = bri.answerHead(ctx, input, headOutput, hInfo)
195+
} else {
196+
// Redirect to signed url
197+
err = bri.redirectToSignedURL(ctx, key, input)
198+
}
185199
}
186200
} else {
187201
// Stream object
@@ -225,7 +239,7 @@ func (bri *bucketReqImpl) Get(ctx context.Context, input *GetInput) {
225239
}
226240
}
227241

228-
func (bri *bucketReqImpl) manageGetFolder(ctx context.Context, key string, input *GetInput) {
242+
func (bri *bucketReqImpl) manageGetFolder(ctx context.Context, key string, input *GetInput, isHeadReq bool) {
229243
// Get response handler
230244
resHan := responsehandler.GetResponseHandlerFromContext(ctx)
231245

@@ -236,7 +250,7 @@ func (bri *bucketReqImpl) manageGetFolder(ctx context.Context, key string, input
236250
// Create index key path
237251
indexKey := path.Join(key, bri.targetCfg.Actions.GET.Config.IndexDocument)
238252
// Head index file in bucket
239-
headOutput, err := bri.s3ClientManager.
253+
headOutput, hInfo, err := bri.s3ClientManager.
240254
GetClientForTarget(bri.targetCfg.Name).
241255
HeadObject(ctx, indexKey)
242256
// Check if error exists and not a not found error
@@ -248,8 +262,11 @@ func (bri *bucketReqImpl) manageGetFolder(ctx context.Context, key string, input
248262
}
249263
// Check that we found the file
250264
if headOutput != nil {
251-
// Check if it is asked to redirect to signed url
252-
if bri.targetCfg.Actions.GET.Config.RedirectToSignedURL {
265+
// Check if it is head request
266+
if isHeadReq { //nolint:gocritic // Ignore this
267+
// Answer with head
268+
err = bri.answerHead(ctx, input, headOutput, hInfo)
269+
} else if bri.targetCfg.Actions.GET.Config.RedirectToSignedURL { // Check if it is asked to redirect to signed url
253270
// Redirect to signed url
254271
err = bri.redirectToSignedURL(ctx, indexKey, input)
255272
} else {
@@ -311,25 +328,46 @@ func (bri *bucketReqImpl) manageGetFolder(ctx context.Context, key string, input
311328
return
312329
}
313330

314-
// Send hook
315-
bri.webhookManager.ManageGETHooks(
316-
ctx,
317-
bri.targetCfg.Name,
318-
input.RequestPath,
319-
&webhook.GetInputMetadata{
320-
IfModifiedSince: input.IfModifiedSince,
321-
IfMatch: input.IfMatch,
322-
IfNoneMatch: input.IfNoneMatch,
323-
IfUnmodifiedSince: input.IfUnmodifiedSince,
324-
Range: input.Range,
325-
},
326-
&webhook.S3Metadata{
327-
Bucket: info.Bucket,
328-
Region: info.Region,
329-
S3Endpoint: info.S3Endpoint,
330-
Key: info.Key,
331-
},
332-
)
331+
if isHeadReq {
332+
// Send hook
333+
bri.webhookManager.ManageHEADHooks(
334+
ctx,
335+
bri.targetCfg.Name,
336+
input.RequestPath,
337+
&webhook.HeadInputMetadata{
338+
IfModifiedSince: input.IfModifiedSince,
339+
IfMatch: input.IfMatch,
340+
IfNoneMatch: input.IfNoneMatch,
341+
IfUnmodifiedSince: input.IfUnmodifiedSince,
342+
},
343+
&webhook.S3Metadata{
344+
Bucket: info.Bucket,
345+
Region: info.Region,
346+
S3Endpoint: info.S3Endpoint,
347+
Key: info.Key,
348+
},
349+
)
350+
} else {
351+
// Send hook
352+
bri.webhookManager.ManageGETHooks(
353+
ctx,
354+
bri.targetCfg.Name,
355+
input.RequestPath,
356+
&webhook.GetInputMetadata{
357+
IfModifiedSince: input.IfModifiedSince,
358+
IfMatch: input.IfMatch,
359+
IfNoneMatch: input.IfNoneMatch,
360+
IfUnmodifiedSince: input.IfUnmodifiedSince,
361+
Range: input.Range,
362+
},
363+
&webhook.S3Metadata{
364+
Bucket: info.Bucket,
365+
Region: info.Region,
366+
S3Endpoint: info.S3Endpoint,
367+
Key: info.Key,
368+
},
369+
)
370+
}
333371

334372
// Transform entries in entry with path objects
335373
bucketRootPrefixKey := bri.targetCfg.Bucket.GetRootPrefix()
@@ -513,7 +551,7 @@ func (bri *bucketReqImpl) Put(ctx context.Context, inp *PutInput) {
513551
// Check if allow override is enabled
514552
if !bri.targetCfg.Actions.PUT.Config.AllowOverride {
515553
// Need to check if file already exists
516-
headOutput, err2 := bri.s3ClientManager.
554+
headOutput, _, err2 := bri.s3ClientManager.
517555
GetClientForTarget(bri.targetCfg.Name).
518556
HeadObject(ctx, key)
519557
// Check if error is not found if exists
@@ -740,6 +778,52 @@ func (bri *bucketReqImpl) redirectToSignedURL(ctx context.Context, key string, i
740778
return nil
741779
}
742780

781+
func (bri *bucketReqImpl) answerHead(
782+
ctx context.Context,
783+
input *GetInput,
784+
hOutput *s3client.HeadOutput,
785+
info *s3client.ResultInfo,
786+
) error {
787+
// Get response handler from context
788+
resHan := responsehandler.GetResponseHandlerFromContext(ctx)
789+
790+
// Send hook
791+
bri.webhookManager.ManageHEADHooks(
792+
ctx,
793+
bri.targetCfg.Name,
794+
input.RequestPath,
795+
&webhook.HeadInputMetadata{
796+
IfModifiedSince: input.IfModifiedSince,
797+
IfMatch: input.IfMatch,
798+
IfNoneMatch: input.IfNoneMatch,
799+
IfUnmodifiedSince: input.IfUnmodifiedSince,
800+
},
801+
&webhook.S3Metadata{
802+
Bucket: info.Bucket,
803+
Region: info.Region,
804+
S3Endpoint: info.S3Endpoint,
805+
Key: info.Key,
806+
},
807+
)
808+
809+
// Transform input
810+
inp := &responsehandler.StreamInput{
811+
CacheControl: hOutput.CacheControl,
812+
Expires: hOutput.Expires,
813+
ContentDisposition: hOutput.ContentDisposition,
814+
ContentEncoding: hOutput.ContentEncoding,
815+
ContentLanguage: hOutput.ContentLanguage,
816+
ContentLength: hOutput.ContentLength,
817+
ContentType: hOutput.ContentType,
818+
ETag: hOutput.ETag,
819+
LastModified: hOutput.LastModified,
820+
Metadata: hOutput.Metadata,
821+
}
822+
823+
// Stream
824+
return resHan.StreamFile(bri.LoadFileContent, inp)
825+
}
826+
743827
func (bri *bucketReqImpl) streamFileForResponse(ctx context.Context, key string, input *GetInput) error {
744828
// Get response handler from context
745829
resHan := responsehandler.GetResponseHandlerFromContext(ctx)

pkg/s3-proxy/bucket/bucket-req-impl_test.go

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,6 +1389,7 @@ func Test_requestContext_Put(t *testing.T) {
13891389
HeadObject(ctx, tt.s3ClientHeadObjectMockResult.input2).
13901390
Return(
13911391
tt.s3ClientHeadObjectMockResult.res,
1392+
nil,
13921393
tt.s3ClientHeadObjectMockResult.err,
13931394
).
13941395
Times(tt.s3ClientHeadObjectMockResult.times)
@@ -1630,8 +1631,10 @@ func Test_requestContext_Get(t *testing.T) {
16301631
Key: "/folder/index.html",
16311632
},
16321633
res: &s3client.GetOutput{
1633-
Body: body,
1634-
ContentType: "text/html; charset=utf-8",
1634+
Body: body,
1635+
BaseFileOutput: &s3client.BaseFileOutput{
1636+
ContentType: "text/html; charset=utf-8",
1637+
},
16351638
},
16361639
res2: &s3client.ResultInfo{
16371640
Bucket: "bucket",
@@ -1943,9 +1946,11 @@ func Test_requestContext_Get(t *testing.T) {
19431946
Key: "/folder/index.html",
19441947
},
19451948
res: &s3client.GetOutput{
1946-
Body: body,
1947-
ContentDisposition: "disposition",
1948-
ContentType: "type",
1949+
Body: body,
1950+
BaseFileOutput: &s3client.BaseFileOutput{
1951+
ContentDisposition: "disposition",
1952+
ContentType: "type",
1953+
},
19491954
},
19501955
res2: &s3client.ResultInfo{
19511956
Bucket: "bucket",
@@ -2111,9 +2116,11 @@ func Test_requestContext_Get(t *testing.T) {
21112116
Key: "/fake/fake.html",
21122117
},
21132118
res: &s3client.GetOutput{
2114-
Body: body,
2115-
ContentType: "type",
2116-
ContentEncoding: "encoding",
2119+
Body: body,
2120+
BaseFileOutput: &s3client.BaseFileOutput{
2121+
ContentType: "type",
2122+
ContentEncoding: "encoding",
2123+
},
21172124
},
21182125
res2: &s3client.ResultInfo{
21192126
Bucket: "bucket",
@@ -2225,6 +2232,7 @@ func Test_requestContext_Get(t *testing.T) {
22252232
HeadObject(ctx, tt.s3ClientHeadObjectMockResult.input2).
22262233
Return(
22272234
tt.s3ClientHeadObjectMockResult.res,
2235+
nil,
22282236
tt.s3ClientHeadObjectMockResult.err,
22292237
).
22302238
Times(tt.s3ClientHeadObjectMockResult.times)

pkg/s3-proxy/bucket/client.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ var ErrRemovalFolder = errors.New("can't remove folder")
2222
type Client interface {
2323
// Get allow to GET what's inside a request path
2424
Get(ctx context.Context, input *GetInput)
25+
// Head allow to HEAD what's inside a request path
26+
Head(ctx context.Context, input *GetInput)
2527
// Put will put a file following input
2628
Put(ctx context.Context, inp *PutInput)
2729
// Delete will delete file on request path

0 commit comments

Comments
 (0)