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
}
|