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