-
Notifications
You must be signed in to change notification settings - Fork 1.2k
/
Copy pathpresigner.rb
261 lines (241 loc) · 9.91 KB
/
presigner.rb
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
# frozen_string_literal: true
module Aws
module S3
class Presigner
# @api private
ONE_WEEK = 60 * 60 * 24 * 7
# @api private
FIFTEEN_MINUTES = 60 * 15
# @api private
BLACKLISTED_HEADERS = [
'accept',
'amz-sdk-request',
'cache-control',
'content-length', # due to a ELB bug
'expect',
'from',
'if-match',
'if-none-match',
'if-modified-since',
'if-unmodified-since',
'if-range',
'max-forwards',
'pragma',
'proxy-authorization',
'referer',
'te',
'user-agent'
].freeze
# @option options [Client] :client Optionally provide an existing
# S3 client
def initialize(options = {})
@client = options[:client] || Aws::S3::Client.new
end
# Create presigned URLs for S3 operations.
#
# @example
# signer = Aws::S3::Presigner.new
# url = signer.presigned_url(:get_object, bucket: "bucket", key: "key")
#
# @param [Symbol] method Symbolized method name of the operation you want
# to presign.
#
# @option params [Integer] :expires_in (900) The number of seconds
# before the presigned URL expires. Defaults to 15 minutes. As signature
# version 4 has a maximum expiry time of one week for presigned URLs,
# attempts to set this value to greater than one week (604800) will
# raise an exception. The min value of this option and the credentials
# expiration time is used in the presigned URL.
#
# @option params [Time] :time (Time.now) The starting time for when the
# presigned url becomes active.
#
# @option params [Boolean] :secure (true) When `false`, a HTTP URL
# is returned instead of the default HTTPS URL.
#
# @option params [Boolean] :virtual_host (false) When `true`, the
# bucket name will be used as the hostname.
#
# @option params [Boolean] :use_accelerate_endpoint (false) When `true`,
# Presigner will attempt to use accelerated endpoint.
#
# @option params [Array<String>] :whitelist_headers ([]) Additional
# headers to be included for the signed request. Certain headers beyond
# the authorization header could, in theory, be changed for various
# reasons (including but not limited to proxies) while in transit and
# after signing. This would lead to signature errors being returned,
# despite no actual problems with signing. (see BLACKLISTED_HEADERS)
#
# @raise [ArgumentError] Raises an ArgumentError if `:expires_in`
# exceeds one week.
#
# @return [String] a presigned url
def presigned_url(method, params = {})
url, _headers = _presigned_request(method, params)
url
end
# Allows you to create presigned URL requests for S3 operations. This
# method returns a tuple containing the URL and the signed X-amz-* headers
# to be used with the presigned url.
#
# @example
# signer = Aws::S3::Presigner.new
# url, headers = signer.presigned_request(
# :get_object, bucket: "bucket", key: "key"
# )
#
# @param [Symbol] method Symbolized method name of the operation you want
# to presign.
#
# @option params [Integer] :expires_in (900) The number of seconds
# before the presigned URL expires. Defaults to 15 minutes. As signature
# version 4 has a maximum expiry time of one week for presigned URLs,
# attempts to set this value to greater than one week (604800) will
# raise an exception. The min value of this option and the credentials
# expiration time is used in the presigned URL.
#
# @option params [Time] :time (Time.now) The starting time for when the
# presigned url becomes active.
#
# @option params [Boolean] :secure (true) When `false`, a HTTP URL
# is returned instead of the default HTTPS URL.
#
# @option params [Boolean] :virtual_host (false) When `true`, the
# bucket name will be used as the hostname. This will cause
# the returned URL to be 'http' and not 'https'.
#
# @option params [Boolean] :use_accelerate_endpoint (false) When `true`,
# Presigner will attempt to use accelerated endpoint.
#
# @option params [Array<String>] :whitelist_headers ([]) Additional
# headers to be included for the signed request. Certain headers beyond
# the authorization header could, in theory, be changed for various
# reasons (including but not limited to proxies) while in transit and
# after signing. This would lead to signature errors being returned,
# despite no actual problems with signing. (see BLACKLISTED_HEADERS)
#
# @raise [ArgumentError] Raises an ArgumentError if `:expires_in`
# exceeds one week.
#
# @return [String, Hash] A tuple with a presigned URL and headers that
# should be included with the request.
def presigned_request(method, params = {})
_presigned_request(method, params, false)
end
private
def _presigned_request(method, params, hoist = true)
virtual_host = params.delete(:virtual_host)
time = params.delete(:time)
unsigned_headers = unsigned_headers(params)
secure = params.delete(:secure) != false
expires_in = expires_in(params)
req = @client.build_request(method, params)
use_bucket_as_hostname(req) if virtual_host
handle_presigned_url_context(req)
x_amz_headers = sign_but_dont_send(
req, expires_in, secure, time, unsigned_headers, hoist
)
[req.send_request.data, x_amz_headers]
end
def unsigned_headers(params)
whitelist_headers = params.delete(:whitelist_headers) || []
BLACKLISTED_HEADERS - whitelist_headers
end
def expires_in(params)
if (expires_in = params.delete(:expires_in))
if expires_in > ONE_WEEK
raise ArgumentError,
"expires_in value of #{expires_in} exceeds one-week maximum."
elsif expires_in <= 0
raise ArgumentError,
"expires_in value of #{expires_in} cannot be 0 or less."
end
expires_in
else
FIFTEEN_MINUTES
end
end
def use_bucket_as_hostname(req)
req.handle(priority: 35) do |context|
uri = context.http_request.endpoint
uri.host = context.params[:bucket]
uri.path.sub!("/#{context.params[:bucket]}", '')
@handler.call(context)
end
end
# Used for excluding presigned_urls from API request count.
#
# Store context information as early as possible, to allow
# handlers to perform decisions based on this flag if need.
def handle_presigned_url_context(req)
req.handle(step: :initialize, priority: 98) do |context|
context[:presigned_url] = true
@handler.call(context)
end
end
# @param [Seahorse::Client::Request] req
def sign_but_dont_send(
req, expires_in, secure, time, unsigned_headers, hoist = true
)
x_amz_headers = {}
http_req = req.context.http_request
req.handlers.remove(Seahorse::Client::Plugins::ContentLength::Handler)
req.handlers.remove(Aws::Rest::ContentTypeHandler)
req.handlers.remove(Aws::Plugins::ChecksumAlgorithm::OptionHandler)
req.handlers.remove(Aws::Plugins::ChecksumAlgorithm::ChecksumHandler)
req.handlers.remove(Aws::Plugins::InvocationId::Handler)
req.handlers.remove(Aws::Plugins::Sign::Handler)
req.handlers.remove(Aws::S3::Plugins::S3Signer::LegacyHandler)
req.handle(step: :send) do |context|
# if an endpoint was not provided, force secure or insecure
if context.config.regional_endpoint
http_req.endpoint.scheme = secure ? 'https' : 'http'
http_req.endpoint.port = secure ? 443 : 80
end
query = http_req.endpoint.query ? http_req.endpoint.query.split('&') : []
http_req.headers.each do |key, value|
next unless key =~ /^x-amz/i
if hoist
value = Aws::Sigv4::Signer.uri_escape(value)
key = Aws::Sigv4::Signer.uri_escape(key)
# hoist x-amz-* headers to the querystring
http_req.headers.delete(key)
query << "#{key}=#{value}"
else
x_amz_headers[key] = value
end
end
http_req.endpoint.query = query.join('&') unless query.empty?
auth_scheme = context[:auth_scheme]
scheme_name = auth_scheme['name']
region = if scheme_name == 'sigv4a'
auth_scheme['signingRegionSet'].first
else
auth_scheme['signingRegion']
end
signer = Aws::Sigv4::Signer.new(
service: auth_scheme['signingName'] || 's3',
region: context[:sigv4_region] || region || context.config.region,
credentials_provider: context[:sigv4_credentials] || context.config.credentials,
signing_algorithm: scheme_name.to_sym,
uri_escape_path: !!!auth_scheme['disableDoubleEncoding'],
normalize_path: !!!auth_scheme['disableNormalizePath'],
unsigned_headers: unsigned_headers,
apply_checksum_header: false
)
url = signer.presign_url(
http_method: http_req.http_method,
url: http_req.endpoint,
headers: http_req.headers,
body_digest: 'UNSIGNED-PAYLOAD',
expires_in: expires_in,
time: time
).to_s
Seahorse::Client::Response.new(context: context, data: url)
end
# Return the headers
x_amz_headers
end
end
end
end