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