@@ -37,87 +37,173 @@ internal import SotoXML
37
37
/// - Creates error body for notFound responses to HEAD requests
38
38
public struct S3Middleware : AWSMiddlewareProtocol {
39
39
public func handle( _ request: AWSHTTPRequest , context: AWSMiddlewareContext , next: AWSMiddlewareNextHandler ) async throws -> AWSHTTPResponse {
40
- var request = request
41
40
42
- self . virtualAddressFixup ( request: & request, context: context)
43
- self . createBucketFixup ( request: & request, context: context)
44
- if !context. serviceConfig. options. contains ( . s3Disable100Continue) {
45
- self . expect100Continue ( request: & request)
46
- }
41
+ try await self . handleVirtualAddressFixup ( request, context: context) { request, context in
42
+ var request = request
43
+ self . createBucketFixup ( request: & request, context: context)
44
+ if !context. serviceConfig. options. contains ( . s3Disable100Continue) {
45
+ self . expect100Continue ( request: & request)
46
+ }
47
47
48
- do {
49
- var response = try await next ( request, context)
50
- if context. operation == " GetBucketLocation " {
51
- self . getBucketLocationResponseFixup ( response: & response)
48
+ do {
49
+ var response = try await next ( request, context)
50
+ if context. operation == " GetBucketLocation " {
51
+ self . getBucketLocationResponseFixup ( response: & response)
52
+ }
53
+ return response
54
+ } catch let error as AWSRawError {
55
+ let newError = self . fixupRawError ( error: error, context: context)
56
+ throw newError
52
57
}
53
- return response
54
- } catch let error as AWSRawError {
55
- let newError = self . fixupRawError ( error: error, context: context)
56
- throw newError
57
58
}
58
59
}
59
60
60
61
public init ( ) { }
61
62
62
- func virtualAddressFixup( request: inout AWSHTTPRequest , context: AWSMiddlewareContext ) {
63
+ func handleVirtualAddressFixup(
64
+ _ request: AWSHTTPRequest ,
65
+ context: AWSMiddlewareContext ,
66
+ next: AWSMiddlewareNextHandler
67
+ ) async throws -> AWSHTTPResponse {
68
+ if request. url. path. hasPrefix ( " /arn: " ) {
69
+ return try await handleARNBucket ( request, context: context, next: next)
70
+ }
63
71
/// process URL into form ${bucket}.s3.amazon.com
64
- let paths = request. url. path. split ( separator: " / " , omittingEmptySubsequences: true )
65
- if paths. count > 0 {
66
- guard var host = request. url. host else { return }
67
- if let port = request. url. port {
68
- host = " \( host) : \( port) "
72
+ let paths = request. url. path. split ( separator: " / " , maxSplits: 2 , omittingEmptySubsequences: false ) . dropFirst ( )
73
+ guard let bucket = paths. first, var host = request. url. host else { return try await next ( request, context) }
74
+
75
+ if let port = request. url. port {
76
+ host = " \( host) : \( port) "
77
+ }
78
+ var urlPath : String
79
+ var urlHost : String
80
+ let isAmazonUrl = host. hasSuffix ( " amazonaws.com " )
81
+
82
+ var hostComponents = host. split ( separator: " . " )
83
+ if isAmazonUrl, context. serviceConfig. options. contains ( . s3UseTransferAcceleratedEndpoint) {
84
+ if let s3Index = hostComponents. firstIndex ( where: { $0 == " s3 " } ) {
85
+ var s3 = " s3 "
86
+ s3 += " -accelerate "
87
+ // assume next host component is region
88
+ let regionIndex = s3Index + 1
89
+ hostComponents. remove ( at: regionIndex)
90
+ hostComponents [ s3Index] = Substring ( s3)
91
+ host = hostComponents. joined ( separator: " . " )
69
92
}
70
- let bucket = paths [ 0 ]
71
- var urlPath : String
72
- var urlHost : String
73
- let isAmazonUrl = host. hasSuffix ( " amazonaws.com " )
74
-
75
- var hostComponents = host. split ( separator: " . " )
76
- if isAmazonUrl, context. serviceConfig. options. contains ( . s3UseTransferAcceleratedEndpoint) {
77
- if let s3Index = hostComponents. firstIndex ( where: { $0 == " s3 " } ) {
78
- var s3 = " s3 "
79
- s3 += " -accelerate "
80
- // assume next host component is region
81
- let regionIndex = s3Index + 1
82
- hostComponents. remove ( at: regionIndex)
83
- hostComponents [ s3Index] = Substring ( s3)
84
- host = hostComponents. joined ( separator: " . " )
85
- }
93
+ }
94
+
95
+ // Is bucket an ARN
96
+ if bucket. hasPrefix ( " arn: " ) {
97
+ guard let arn = ARN ( string: bucket) ,
98
+ let resourceType = arn. resourceType,
99
+ let region = arn. region,
100
+ let accountId = arn. accountId
101
+ else {
102
+ throw AWSClient . ClientError. invalidARN
103
+ }
104
+ guard resourceType == " accesspoint " , arn. service == " s3-object-lambda " || arn. service == " s3-outposts " else {
105
+ throw AWSClient . ClientError. invalidARN
86
106
}
107
+ urlPath = " / "
108
+ // https://tutorial-object-lambda-accesspoint-123456789012.s3-object-lambda.us-west-2.amazonaws.com:443
109
+ urlHost = " \( arn. resourceId) - \( resourceType) - \( accountId) . \( arn. service) . \( region) .amazonaws.com "
87
110
88
111
// if host name contains amazonaws.com and bucket name doesn't contain a period do virtual address look up
89
- if isAmazonUrl || context. serviceConfig. options. contains ( . s3ForceVirtualHost) , !bucket. contains ( " . " ) {
90
- let pathsWithoutBucket = paths. dropFirst ( ) // bucket
91
- urlPath = pathsWithoutBucket. joined ( separator: " / " )
92
-
93
- if hostComponents. first == bucket {
94
- // Bucket name is part of host. No need to append bucket
95
- urlHost = host
96
- } else {
97
- urlHost = " \( bucket) . \( host) "
98
- }
99
- } else {
100
- urlPath = paths. joined ( separator: " / " )
112
+ } else if isAmazonUrl || context. serviceConfig. options. contains ( . s3ForceVirtualHost) , !bucket. contains ( " . " ) {
113
+ let pathsWithoutBucket = paths. dropFirst ( ) // bucket
114
+ urlPath = pathsWithoutBucket. first. flatMap { String ( $0) } ?? " "
115
+
116
+ if hostComponents. first == bucket {
117
+ // Bucket name is part of host. No need to append bucket
101
118
urlHost = host
119
+ } else {
120
+ urlHost = " \( bucket) . \( host) "
102
121
}
103
- // add trailing "/" back if it was present, no need to check for single slash path here
104
- if request. url. pathWithSlash. hasSuffix ( " / " ) {
105
- urlPath += " / "
106
- }
107
- // add percent encoding back into path as converting from URL to String has removed it
108
- let percentEncodedUrlPath = Self . urlEncodePath ( urlPath)
109
- var urlString = " \( request. url. scheme ?? " https " ) :// \( urlHost) / \( percentEncodedUrlPath) "
110
- if let query = request. url. query {
111
- urlString += " ? \( query) "
112
- }
113
- request. url = URL ( string: urlString) !
122
+ } else {
123
+ urlPath = paths. joined ( separator: " / " )
124
+ urlHost = host
125
+ }
126
+ let request = Self . updateRequestURL ( request, host: urlHost, path: urlPath)
127
+ return try await next ( request, context)
128
+ }
129
+
130
+ /// Handle bucket names in the form of an ARN
131
+ /// - Parameters:
132
+ /// - request: request
133
+ /// - context: request context
134
+ /// - next: next handler
135
+ /// - Returns: returns response from next handler
136
+ func handleARNBucket(
137
+ _ request: AWSHTTPRequest ,
138
+ context: AWSMiddlewareContext ,
139
+ next: AWSMiddlewareNextHandler
140
+ ) async throws -> AWSHTTPResponse {
141
+ guard let arn = ARN ( string: request. url. path. dropFirst ( ) ) ,
142
+ let resourceType = arn. resourceType,
143
+ let accountId = arn. accountId
144
+ else {
145
+ throw AWSClient . ClientError. invalidARN
146
+ }
147
+ let region = arn. region ?? context. serviceConfig. region
148
+ guard resourceType == " accesspoint " , arn. service == " s3-object-lambda " || arn. service == " s3-outposts " || arn. service == " s3 " else {
149
+ throw AWSClient . ClientError. invalidARN
114
150
}
151
+
152
+ // extract bucket and path from ARN
153
+ let resourceIDSplit = arn. resourceId. split ( separator: " / " , maxSplits: 1 , omittingEmptySubsequences: false )
154
+ guard let bucket = resourceIDSplit. first else { throw AWSClient . ClientError. invalidARN }
155
+ let path = String ( resourceIDSplit. dropFirst ( ) . first ?? " " )
156
+ let service = String ( arn. service)
157
+ let serviceIdentifier = service != " s3 " ? service : " s3-accesspoint "
158
+ let urlHost = " \( bucket) - \( accountId) . \( serviceIdentifier) . \( region) .amazonaws.com "
159
+ let request = Self . updateRequestURL ( request, host: urlHost, path: path)
160
+
161
+ var context = context
162
+ context. serviceConfig = AWSServiceConfig (
163
+ region: region,
164
+ partition: region. partition,
165
+ serviceName: " S3 " ,
166
+ serviceIdentifier: serviceIdentifier,
167
+ signingName: service,
168
+ serviceProtocol: context. serviceConfig. serviceProtocol,
169
+ apiVersion: context. serviceConfig. apiVersion,
170
+ errorType: context. serviceConfig. errorType,
171
+ xmlNamespace: context. serviceConfig. xmlNamespace,
172
+ middleware: context. serviceConfig. middleware,
173
+ timeout: context. serviceConfig. timeout,
174
+ byteBufferAllocator: context. serviceConfig. byteBufferAllocator,
175
+ options: context. serviceConfig. options
176
+ )
177
+ return try await next ( request, context)
178
+ }
179
+
180
+ /// Update request with new host and path
181
+ /// - Parameters:
182
+ /// - request: request
183
+ /// - host: new host name
184
+ /// - path: new path
185
+ /// - Returns: new request
186
+ static func updateRequestURL( _ request: AWSHTTPRequest , host: some StringProtocol , path: String ) -> AWSHTTPRequest {
187
+ var path = path
188
+ // add trailing "/" back if it was present, no need to check for single slash path here
189
+ if request. url. pathWithSlash. hasSuffix ( " / " ) {
190
+ path += " / "
191
+ }
192
+ // add percent encoding back into path as converting from URL to String has removed it
193
+ let percentEncodedUrlPath = Self . urlEncodePath ( path)
194
+ var urlString = " \( request. url. scheme ?? " https " ) :// \( host) / \( percentEncodedUrlPath) "
195
+ if let query = request. url. query {
196
+ urlString += " ? \( query) "
197
+ }
198
+ var request = request
199
+ request. url = URL ( string: urlString) !
200
+ return request
115
201
}
116
202
117
203
static let s3PathAllowedCharacters = CharacterSet . urlPathAllowed. subtracting ( . init( charactersIn: " +@()&$=:,'!* " ) )
118
204
/// percent encode path value.
119
- private static func urlEncodePath( _ value: String ) -> String {
120
- value. addingPercentEncoding ( withAllowedCharacters: Self . s3PathAllowedCharacters) ?? value
205
+ private static func urlEncodePath( _ value: some StringProtocol ) -> String {
206
+ value. addingPercentEncoding ( withAllowedCharacters: Self . s3PathAllowedCharacters) ?? String ( value)
121
207
}
122
208
123
209
func createBucketFixup( request: inout AWSHTTPRequest , context: AWSMiddlewareContext ) {
0 commit comments