File: update_endpoint.go

package info (click to toggle)
golang-github-aws-aws-sdk-go-v2 1.24.1-2~bpo12%2B1
  • links: PTS, VCS
  • area: main
  • in suites: bookworm-backports
  • size: 554,032 kB
  • sloc: java: 15,941; makefile: 419; sh: 175
file content (310 lines) | stat: -rw-r--r-- 9,436 bytes parent folder | download | duplicates (6)
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
}