/
update_endpoint.go
310 lines (258 loc) · 9.21 KB
/
update_endpoint.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
package customizations
import (
"context"
"fmt"
"log"
"net/url"
"strings"
"github.com/aws/aws-sdk-go-v2/aws"
awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
"github.com/aws/aws-sdk-go-v2/service/internal/s3shared"
internalendpoints "github.com/aws/aws-sdk-go-v2/service/s3/internal/endpoints"
"github.com/aws/smithy-go/encoding/httpbinding"
"github.com/aws/smithy-go/middleware"
smithyhttp "github.com/aws/smithy-go/transport/http"
)
// EndpointResolver interface for resolving service endpoints.
type EndpointResolver interface {
ResolveEndpoint(region string, options EndpointResolverOptions) (aws.Endpoint, error)
}
// EndpointResolverOptions is the service endpoint resolver options
type EndpointResolverOptions = internalendpoints.Options
// UpdateEndpointParameterAccessor represents accessor functions used by the middleware
type UpdateEndpointParameterAccessor struct {
// functional pointer to fetch bucket name from provided input.
// The function is intended to take an input value, and
// return a string pointer to value of string, and bool if
// input has no bucket member.
GetBucketFromInput func(interface{}) (*string, bool)
}
// UpdateEndpointOptions provides the options for the UpdateEndpoint middleware setup.
type UpdateEndpointOptions struct {
// Accessor are parameter accessors used by the middleware
Accessor UpdateEndpointParameterAccessor
// use path style
UsePathStyle bool
// use transfer acceleration
UseAccelerate bool
// indicates if an operation supports s3 transfer acceleration.
SupportsAccelerate bool
// use ARN region
UseARNRegion bool
// Indicates that the operation should target the s3-object-lambda endpoint.
// Used to direct operations that do not route based on an input ARN.
TargetS3ObjectLambda bool
// EndpointResolver used to resolve endpoints. This may be a custom endpoint resolver
EndpointResolver EndpointResolver
// EndpointResolverOptions used by endpoint resolver
EndpointResolverOptions EndpointResolverOptions
// DisableMultiRegionAccessPoints indicates multi-region access point support is disabled
DisableMultiRegionAccessPoints bool
}
// UpdateEndpoint adds the middleware to the middleware stack based on the UpdateEndpointOptions.
func UpdateEndpoint(stack *middleware.Stack, options UpdateEndpointOptions) (err error) {
const serializerID = "OperationSerializer"
// initial arn look up middleware
err = stack.Initialize.Insert(&s3shared.ARNLookup{
GetARNValue: options.Accessor.GetBucketFromInput,
}, "legacyEndpointContextSetter", middleware.After)
if err != nil {
return err
}
// process arn
err = stack.Serialize.Insert(&processARNResource{
UseARNRegion: options.UseARNRegion,
UseAccelerate: options.UseAccelerate,
EndpointResolver: options.EndpointResolver,
EndpointResolverOptions: options.EndpointResolverOptions,
DisableMultiRegionAccessPoints: options.DisableMultiRegionAccessPoints,
}, serializerID, middleware.Before)
if err != nil {
return err
}
// process whether the operation requires the s3-object-lambda endpoint
// Occurs before operation serializer so that hostPrefix mutations
// can be handled correctly.
err = stack.Serialize.Insert(&s3ObjectLambdaEndpoint{
UseEndpoint: options.TargetS3ObjectLambda,
UseAccelerate: options.UseAccelerate,
EndpointResolver: options.EndpointResolver,
EndpointResolverOptions: options.EndpointResolverOptions,
}, serializerID, middleware.Before)
if err != nil {
return err
}
// remove bucket arn middleware
err = stack.Serialize.Insert(&removeBucketFromPathMiddleware{}, serializerID, middleware.After)
if err != nil {
return err
}
// update endpoint to use options for path style and accelerate
err = stack.Serialize.Insert(&updateEndpoint{
usePathStyle: options.UsePathStyle,
getBucketFromInput: options.Accessor.GetBucketFromInput,
useAccelerate: options.UseAccelerate,
supportsAccelerate: options.SupportsAccelerate,
}, serializerID, middleware.After)
if err != nil {
return err
}
return err
}
type updateEndpoint struct {
// path style options
usePathStyle bool
getBucketFromInput func(interface{}) (*string, bool)
// accelerate options
useAccelerate bool
supportsAccelerate bool
}
// ID returns the middleware ID.
func (*updateEndpoint) ID() string {
return "S3:UpdateEndpoint"
}
func (u *updateEndpoint) HandleSerialize(
ctx context.Context, in middleware.SerializeInput, next middleware.SerializeHandler,
) (
out middleware.SerializeOutput, metadata middleware.Metadata, err error,
) {
if !awsmiddleware.GetRequiresLegacyEndpoints(ctx) {
return next.HandleSerialize(ctx, in)
}
// if arn was processed, skip this middleware
if _, ok := s3shared.GetARNResourceFromContext(ctx); ok {
return next.HandleSerialize(ctx, in)
}
// skip this customization if host name is set as immutable
if smithyhttp.GetHostnameImmutable(ctx) {
return next.HandleSerialize(ctx, in)
}
req, ok := in.Request.(*smithyhttp.Request)
if !ok {
return out, metadata, fmt.Errorf("unknown request type %T", req)
}
// check if accelerate is supported
if u.useAccelerate && !u.supportsAccelerate {
// accelerate is not supported, thus will be ignored
log.Println("Transfer acceleration is not supported for the operation, ignoring UseAccelerate.")
u.useAccelerate = false
}
// transfer acceleration is not supported with path style urls
if u.useAccelerate && u.usePathStyle {
log.Println("UseAccelerate is not compatible with UsePathStyle, ignoring UsePathStyle.")
u.usePathStyle = false
}
if u.getBucketFromInput != nil {
// Below customization only apply if bucket name is provided
bucket, ok := u.getBucketFromInput(in.Parameters)
if ok && bucket != nil {
region := awsmiddleware.GetRegion(ctx)
if err := u.updateEndpointFromConfig(req, *bucket, region); err != nil {
return out, metadata, err
}
}
}
return next.HandleSerialize(ctx, in)
}
func (u updateEndpoint) updateEndpointFromConfig(req *smithyhttp.Request, bucket string, region string) error {
// do nothing if path style is enforced
if u.usePathStyle {
return nil
}
if !hostCompatibleBucketName(req.URL, bucket) {
// bucket name must be valid to put into the host for accelerate operations.
// For non-accelerate operations the bucket name can stay in the path if
// not valid hostname.
var err error
if u.useAccelerate {
err = fmt.Errorf("bucket name %s is not compatible with S3", bucket)
}
// No-Op if not using accelerate.
return err
}
// accelerate is only supported if use path style is disabled
if u.useAccelerate {
parts := strings.Split(req.URL.Host, ".")
if len(parts) < 3 {
return fmt.Errorf("unable to update endpoint host for S3 accelerate, hostname invalid, %s", req.URL.Host)
}
if parts[0] == "s3" || strings.HasPrefix(parts[0], "s3-") {
parts[0] = "s3-accelerate"
}
for i := 1; i+1 < len(parts); i++ {
if strings.EqualFold(parts[i], region) {
parts = append(parts[:i], parts[i+1:]...)
break
}
}
// construct the url host
req.URL.Host = strings.Join(parts, ".")
}
// move bucket to follow virtual host style
moveBucketNameToHost(req.URL, bucket)
return nil
}
// updates endpoint to use virtual host styling
func moveBucketNameToHost(u *url.URL, bucket string) {
u.Host = bucket + "." + u.Host
removeBucketFromPath(u, bucket)
}
// remove bucket from url
func removeBucketFromPath(u *url.URL, bucket string) {
if strings.HasPrefix(u.Path, "/"+bucket) {
// modify url path
u.Path = strings.Replace(u.Path, "/"+bucket, "", 1)
// modify url raw path
u.RawPath = strings.Replace(u.RawPath, "/"+httpbinding.EscapePath(bucket, true), "", 1)
}
if u.Path == "" {
u.Path = "/"
}
if u.RawPath == "" {
u.RawPath = "/"
}
}
// hostCompatibleBucketName returns true if the request should
// put the bucket in the host. This is false if the bucket is not
// DNS compatible or the EndpointResolver resolves an aws.Endpoint with
// HostnameImmutable member set to true.
//
// https://pkg.go.dev/github.com/aws/aws-sdk-go-v2/aws#Endpoint.HostnameImmutable
func hostCompatibleBucketName(u *url.URL, bucket string) bool {
// Bucket might be DNS compatible but dots in the hostname will fail
// certificate validation, so do not use host-style.
if u.Scheme == "https" && strings.Contains(bucket, ".") {
return false
}
// if the bucket is DNS compatible
return dnsCompatibleBucketName(bucket)
}
// dnsCompatibleBucketName returns true if the bucket name is DNS compatible.
// Buckets created outside of the classic region MUST be DNS compatible.
func dnsCompatibleBucketName(bucket string) bool {
if strings.Contains(bucket, "..") {
return false
}
// checks for `^[a-z0-9][a-z0-9\.\-]{1,61}[a-z0-9]$` domain mapping
if !((bucket[0] > 96 && bucket[0] < 123) || (bucket[0] > 47 && bucket[0] < 58)) {
return false
}
for _, c := range bucket[1:] {
if !((c > 96 && c < 123) || (c > 47 && c < 58) || c == 46 || c == 45) {
return false
}
}
// checks for `^(\d+\.){3}\d+$` IPaddressing
v := strings.SplitN(bucket, ".", -1)
if len(v) == 4 {
for _, c := range bucket {
if !((c > 47 && c < 58) || c == 46) {
// we confirm that this is not a IP address
return true
}
}
// this is a IP address
return false
}
return true
}