mss-sdk 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.yardopts +9 -0
- data/LICENSE.txt +0 -0
- data/README.md +192 -0
- data/bin/mss-rb +178 -0
- data/ca-bundle.crt +3554 -0
- data/lib/mss/core/async_handle.rb +89 -0
- data/lib/mss/core/cacheable.rb +76 -0
- data/lib/mss/core/client.rb +786 -0
- data/lib/mss/core/collection/simple.rb +81 -0
- data/lib/mss/core/collection/with_limit_and_next_token.rb +70 -0
- data/lib/mss/core/collection/with_next_token.rb +96 -0
- data/lib/mss/core/collection.rb +262 -0
- data/lib/mss/core/configuration.rb +527 -0
- data/lib/mss/core/credential_providers.rb +653 -0
- data/lib/mss/core/data.rb +251 -0
- data/lib/mss/core/deprecations.rb +83 -0
- data/lib/mss/core/endpoints.rb +36 -0
- data/lib/mss/core/http/connection_pool.rb +374 -0
- data/lib/mss/core/http/curb_handler.rb +150 -0
- data/lib/mss/core/http/handler.rb +88 -0
- data/lib/mss/core/http/net_http_handler.rb +144 -0
- data/lib/mss/core/http/patch.rb +98 -0
- data/lib/mss/core/http/request.rb +258 -0
- data/lib/mss/core/http/response.rb +80 -0
- data/lib/mss/core/indifferent_hash.rb +87 -0
- data/lib/mss/core/inflection.rb +55 -0
- data/lib/mss/core/ini_parser.rb +41 -0
- data/lib/mss/core/json_client.rb +46 -0
- data/lib/mss/core/json_parser.rb +75 -0
- data/lib/mss/core/json_request_builder.rb +34 -0
- data/lib/mss/core/json_response_parser.rb +78 -0
- data/lib/mss/core/lazy_error_classes.rb +107 -0
- data/lib/mss/core/log_formatter.rb +426 -0
- data/lib/mss/core/managed_file.rb +31 -0
- data/lib/mss/core/meta_utils.rb +44 -0
- data/lib/mss/core/model.rb +61 -0
- data/lib/mss/core/naming.rb +29 -0
- data/lib/mss/core/option_grammar.rb +737 -0
- data/lib/mss/core/options/json_serializer.rb +81 -0
- data/lib/mss/core/options/validator.rb +154 -0
- data/lib/mss/core/options/xml_serializer.rb +117 -0
- data/lib/mss/core/page_result.rb +74 -0
- data/lib/mss/core/policy.rb +938 -0
- data/lib/mss/core/query_client.rb +40 -0
- data/lib/mss/core/query_error_parser.rb +23 -0
- data/lib/mss/core/query_request_builder.rb +46 -0
- data/lib/mss/core/query_response_parser.rb +34 -0
- data/lib/mss/core/region.rb +84 -0
- data/lib/mss/core/region_collection.rb +79 -0
- data/lib/mss/core/resource.rb +412 -0
- data/lib/mss/core/resource_cache.rb +39 -0
- data/lib/mss/core/response.rb +214 -0
- data/lib/mss/core/response_cache.rb +49 -0
- data/lib/mss/core/rest_error_parser.rb +23 -0
- data/lib/mss/core/rest_json_client.rb +39 -0
- data/lib/mss/core/rest_request_builder.rb +153 -0
- data/lib/mss/core/rest_response_parser.rb +65 -0
- data/lib/mss/core/rest_xml_client.rb +46 -0
- data/lib/mss/core/service_interface.rb +82 -0
- data/lib/mss/core/signers/base.rb +45 -0
- data/lib/mss/core/signers/cloud_front.rb +55 -0
- data/lib/mss/core/signers/s3.rb +158 -0
- data/lib/mss/core/signers/version_2.rb +71 -0
- data/lib/mss/core/signers/version_3.rb +85 -0
- data/lib/mss/core/signers/version_3_https.rb +60 -0
- data/lib/mss/core/signers/version_4/chunk_signed_stream.rb +190 -0
- data/lib/mss/core/signers/version_4.rb +227 -0
- data/lib/mss/core/uri_escape.rb +43 -0
- data/lib/mss/core/xml/frame.rb +245 -0
- data/lib/mss/core/xml/frame_stack.rb +84 -0
- data/lib/mss/core/xml/grammar.rb +306 -0
- data/lib/mss/core/xml/parser.rb +69 -0
- data/lib/mss/core/xml/root_frame.rb +64 -0
- data/lib/mss/core/xml/sax_handlers/libxml.rb +46 -0
- data/lib/mss/core/xml/sax_handlers/nokogiri.rb +55 -0
- data/lib/mss/core/xml/sax_handlers/ox.rb +40 -0
- data/lib/mss/core/xml/sax_handlers/rexml.rb +46 -0
- data/lib/mss/core/xml/stub.rb +122 -0
- data/lib/mss/core.rb +602 -0
- data/lib/mss/errors.rb +161 -0
- data/lib/mss/rails.rb +194 -0
- data/lib/mss/s3/access_control_list.rb +262 -0
- data/lib/mss/s3/acl_object.rb +263 -0
- data/lib/mss/s3/acl_options.rb +200 -0
- data/lib/mss/s3/bucket.rb +757 -0
- data/lib/mss/s3/bucket_collection.rb +161 -0
- data/lib/mss/s3/bucket_lifecycle_configuration.rb +472 -0
- data/lib/mss/s3/bucket_region_cache.rb +51 -0
- data/lib/mss/s3/bucket_tag_collection.rb +110 -0
- data/lib/mss/s3/bucket_version_collection.rb +78 -0
- data/lib/mss/s3/cipher_io.rb +119 -0
- data/lib/mss/s3/client/xml.rb +265 -0
- data/lib/mss/s3/client.rb +2076 -0
- data/lib/mss/s3/config.rb +60 -0
- data/lib/mss/s3/cors_rule.rb +107 -0
- data/lib/mss/s3/cors_rule_collection.rb +193 -0
- data/lib/mss/s3/data_options.rb +190 -0
- data/lib/mss/s3/encryption_utils.rb +145 -0
- data/lib/mss/s3/errors.rb +93 -0
- data/lib/mss/s3/multipart_upload.rb +353 -0
- data/lib/mss/s3/multipart_upload_collection.rb +75 -0
- data/lib/mss/s3/object_collection.rb +355 -0
- data/lib/mss/s3/object_metadata.rb +102 -0
- data/lib/mss/s3/object_upload_collection.rb +76 -0
- data/lib/mss/s3/object_version.rb +153 -0
- data/lib/mss/s3/object_version_collection.rb +88 -0
- data/lib/mss/s3/paginated_collection.rb +74 -0
- data/lib/mss/s3/policy.rb +73 -0
- data/lib/mss/s3/prefix_and_delimiter_collection.rb +46 -0
- data/lib/mss/s3/prefixed_collection.rb +84 -0
- data/lib/mss/s3/presign_v4.rb +135 -0
- data/lib/mss/s3/presigned_post.rb +574 -0
- data/lib/mss/s3/region_detection.rb +75 -0
- data/lib/mss/s3/request.rb +61 -0
- data/lib/mss/s3/s3_object.rb +1795 -0
- data/lib/mss/s3/tree/branch_node.rb +67 -0
- data/lib/mss/s3/tree/child_collection.rb +103 -0
- data/lib/mss/s3/tree/leaf_node.rb +93 -0
- data/lib/mss/s3/tree/node.rb +21 -0
- data/lib/mss/s3/tree/parent.rb +86 -0
- data/lib/mss/s3/tree.rb +115 -0
- data/lib/mss/s3/uploaded_part.rb +81 -0
- data/lib/mss/s3/uploaded_part_collection.rb +83 -0
- data/lib/mss/s3/website_configuration.rb +101 -0
- data/lib/mss/s3.rb +161 -0
- data/lib/mss/version.rb +16 -0
- data/lib/mss-sdk.rb +2 -0
- data/lib/mss.rb +14 -0
- data/rails/init.rb +14 -0
- metadata +201 -0
@@ -0,0 +1,574 @@
|
|
1
|
+
# Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
5
|
+
# the License is located at
|
6
|
+
#
|
7
|
+
#
|
8
|
+
# or in the "license" file accompanying this file. This file is
|
9
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
10
|
+
# ANY KIND, either express or implied. See the License for the specific
|
11
|
+
# language governing permissions and limitations under the License.
|
12
|
+
|
13
|
+
require 'uri'
|
14
|
+
require 'time'
|
15
|
+
|
16
|
+
module MSS
|
17
|
+
class S3
|
18
|
+
|
19
|
+
# Helper to generate form fields for presigned POST requests to
|
20
|
+
# a bucket. You can use this to create a form that can be used
|
21
|
+
# from a web browser to upload objects to S3 while specifying
|
22
|
+
# conditions on what can be uploaded and how it is processed and
|
23
|
+
# stored.
|
24
|
+
#
|
25
|
+
# @example Form fields for uploading by file name
|
26
|
+
#
|
27
|
+
# form = bucket.presigned_post(:key => "photos/${filename}")
|
28
|
+
# form.url.to_s # => "https://mybucket.s3.amazonmss.com/"
|
29
|
+
# form.fields # => { "AWSAccessKeyId" => "...", ... }
|
30
|
+
#
|
31
|
+
# @example Generating a minimal HTML form
|
32
|
+
#
|
33
|
+
# form = bucket.objects.myobj.presigned_post
|
34
|
+
# hidden_inputs = form.fields.map do |(name, value)|
|
35
|
+
# %(<input type="hidden" name="#{name}" value="#{value}" />)
|
36
|
+
# end
|
37
|
+
# <<-END
|
38
|
+
# <form action="#{form.url}" method="post" enctype="multipart/form-data">
|
39
|
+
# #{hidden_inputs}
|
40
|
+
# <input type="file" name="file" />
|
41
|
+
# </form>
|
42
|
+
# END
|
43
|
+
#
|
44
|
+
# @example Restricting the size of the uploaded object
|
45
|
+
#
|
46
|
+
# bucket.presigned_post(:content_length => 1..(10*1024))
|
47
|
+
#
|
48
|
+
# @example Restricting the key prefix
|
49
|
+
#
|
50
|
+
# bucket.presigned_post.where(:key).starts_with("photos/")
|
51
|
+
#
|
52
|
+
class PresignedPost
|
53
|
+
|
54
|
+
include Core::Model
|
55
|
+
|
56
|
+
# @return [Bucket] The bucket to which data can be uploaded
|
57
|
+
# using the form fields
|
58
|
+
attr_reader :bucket
|
59
|
+
|
60
|
+
# @return [String] The key of the object that will be
|
61
|
+
# uploaded. If this is nil, then the object can be uploaded
|
62
|
+
# with any key that satisfies the conditions specified for
|
63
|
+
# the upload (see {#where}).
|
64
|
+
attr_reader :key
|
65
|
+
|
66
|
+
# @return [Hash] A hash of the metadata fields included in the
|
67
|
+
# signed fields. Additional metadata fields may be provided
|
68
|
+
# with the upload as long as they satisfy the conditions
|
69
|
+
# specified for the upload (see {#where}).
|
70
|
+
attr_reader :metadata
|
71
|
+
|
72
|
+
# @return [Range] The range of acceptable object sizes for the
|
73
|
+
# upload. By default any size object may be uploaded.
|
74
|
+
attr_reader :content_length
|
75
|
+
|
76
|
+
# @api private
|
77
|
+
attr_reader :conditions
|
78
|
+
|
79
|
+
# @return [Array<String>] Additional fields which may be sent
|
80
|
+
# with the upload. These will be included in the policy so
|
81
|
+
# that they can be sent with any value. S3 will ignore
|
82
|
+
# them.
|
83
|
+
attr_reader :ignored_fields
|
84
|
+
|
85
|
+
# @return The expiration time for the signature. By default
|
86
|
+
# the signature will expire an hour after it is generated.
|
87
|
+
attr_reader :expires
|
88
|
+
|
89
|
+
attr_reader :callback_url
|
90
|
+
attr_reader :callback_body
|
91
|
+
attr_reader :callback_body_type
|
92
|
+
attr_reader :callback_host
|
93
|
+
|
94
|
+
# @api private
|
95
|
+
SPECIAL_FIELDS = [:key,
|
96
|
+
:policy,
|
97
|
+
:signature,
|
98
|
+
:expires,
|
99
|
+
:metadata,
|
100
|
+
:content_length,
|
101
|
+
:conditions,
|
102
|
+
:ignore,
|
103
|
+
:secure,
|
104
|
+
:callback_url,
|
105
|
+
:callback_body,
|
106
|
+
:callback_body_type,
|
107
|
+
:callback_host]
|
108
|
+
|
109
|
+
# Creates a new presigned post object.
|
110
|
+
#
|
111
|
+
# @param [Bucket] bucket The bucket to which data can be uploaded
|
112
|
+
# using the form fields.
|
113
|
+
#
|
114
|
+
# @param [Hash] opts Additional options for the upload. Aside
|
115
|
+
# from `:secure`, `:expires`, `:content_length` and `:ignore`
|
116
|
+
# the values provided here will be stored in the hash returned
|
117
|
+
# from the {#fields} method, and the policy in that hash will
|
118
|
+
# restrict their values to the values provided. If you
|
119
|
+
# instead want to only restrict the values and not provide
|
120
|
+
# them -- for example, if your application generates separate
|
121
|
+
# form fields for those values -- you should use the {#where}
|
122
|
+
# method on the returned object instead of providing the
|
123
|
+
# values here.
|
124
|
+
#
|
125
|
+
# @option opts [String] :key The key of the object that will
|
126
|
+
# be uploaded. If this is nil, then the object can be
|
127
|
+
# uploaded with any key that satisfies the conditions
|
128
|
+
# specified for the upload (see {#where}).
|
129
|
+
#
|
130
|
+
# @option opts [Boolean] :secure By setting this to false, you
|
131
|
+
# can cause {#url} to return an HTTP URL. By default it
|
132
|
+
# returns an HTTPS URL.
|
133
|
+
#
|
134
|
+
# @option opts [Time, DateTime, Integer, String] :expires The
|
135
|
+
# time at which the signature will expire. By default the
|
136
|
+
# signature will expire one hour after it is generated
|
137
|
+
# (e.g. when {#fields} is called).
|
138
|
+
#
|
139
|
+
# When the value is a Time or DateTime, the signature
|
140
|
+
# expires at the specified time. When it is an integer, the
|
141
|
+
# signature expires the specified number of seconds after it
|
142
|
+
# is generated. When it is a string, the string is parsed
|
143
|
+
# as a time (using Time.parse) and the signature expires at
|
144
|
+
# that time.
|
145
|
+
#
|
146
|
+
# @option opts [String] :cache_control Sets the Cache-Control
|
147
|
+
# header stored with the object.
|
148
|
+
#
|
149
|
+
# @option opts [String] :content_type Sets the Content-Type
|
150
|
+
# header stored with the object.
|
151
|
+
#
|
152
|
+
# @option opts [String] :content_disposition Sets the
|
153
|
+
# Content-Disposition header stored with the object.
|
154
|
+
#
|
155
|
+
# @option opts [String] :expires_header Sets the Expires
|
156
|
+
# header stored with the object.
|
157
|
+
#
|
158
|
+
# @option options [Symbol] :acl A canned access control
|
159
|
+
# policy. Valid values are:
|
160
|
+
# * `:private`
|
161
|
+
# * `:public_read`
|
162
|
+
# * `:public_read_write`
|
163
|
+
# * `:authenticated_read`
|
164
|
+
# * `:bucket_owner_read`
|
165
|
+
# * `:bucket_owner_full_control`
|
166
|
+
#
|
167
|
+
# @option options [Symbol] :server_side_encryption (nil) If this
|
168
|
+
# option is set, the object will be stored using server side
|
169
|
+
# encryption. The only valid value is `:aes256`, which
|
170
|
+
# specifies that the object should be stored using the AES
|
171
|
+
# encryption algorithm with 256 bit keys. By default, this
|
172
|
+
# option uses the value of the `:s3_server_side_encryption`
|
173
|
+
# option in the current configuration; for more information,
|
174
|
+
# see {MSS.config}.
|
175
|
+
#
|
176
|
+
# @option opts [String] :success_action_redirect The URL to
|
177
|
+
# which the client is redirected upon successful upload.
|
178
|
+
#
|
179
|
+
# @option opts [Integer] :success_action_status The status
|
180
|
+
# code returned to the client upon successful upload if
|
181
|
+
# `:success_action_redirect` is not specified. Accepts the
|
182
|
+
# values 200, 201, or 204 (default).
|
183
|
+
#
|
184
|
+
# If the value is set to 200 or 204, Amazon S3 returns an
|
185
|
+
# empty document with a 200 or 204 status code.
|
186
|
+
#
|
187
|
+
# If the value is set to 201, Amazon S3 returns an XML
|
188
|
+
# document with a 201 status code. For information on the
|
189
|
+
# content of the XML document, see
|
190
|
+
#
|
191
|
+
# @option opts [Hash] :metadata A hash of the metadata fields
|
192
|
+
# included in the signed fields. Additional metadata fields
|
193
|
+
# may be provided with the upload as long as they satisfy
|
194
|
+
# the conditions specified for the upload (see {#where}).
|
195
|
+
#
|
196
|
+
# @option opts [Integer, Range] :content_length The range of
|
197
|
+
# acceptable object sizes for the upload. By default any
|
198
|
+
# size object may be uploaded.
|
199
|
+
#
|
200
|
+
# @option opts [Array<String>] :ignore Additional fields which
|
201
|
+
# may be sent with the upload. These will be included in
|
202
|
+
# the policy so that they can be sent with any value. S3
|
203
|
+
# will ignore them.
|
204
|
+
def initialize(bucket, opts = {})
|
205
|
+
@bucket = bucket
|
206
|
+
@key = opts[:key]
|
207
|
+
@secure = (opts[:secure] != false)
|
208
|
+
@fields = {}
|
209
|
+
# TODO normalize all values to @fields
|
210
|
+
opts.each do |opt_key, opt_val|
|
211
|
+
@fields[opt_key] = opt_val unless SPECIAL_FIELDS.include? opt_key
|
212
|
+
end
|
213
|
+
@metadata = opts[:metadata] || {}
|
214
|
+
@content_length = range_value(opts[:content_length])
|
215
|
+
@conditions = opts[:conditions] || {}
|
216
|
+
@ignored_fields = [opts[:ignore]].flatten.compact
|
217
|
+
@expires = opts[:expires]
|
218
|
+
@callback_url = opts[:callback_url]
|
219
|
+
@callback_body = opts[:callback_body]
|
220
|
+
@callback_body_type = opts[:callback_body_type]
|
221
|
+
@callback_host = opts[:callback_host]
|
222
|
+
|
223
|
+
super
|
224
|
+
|
225
|
+
@fields[:server_side_encryption] =
|
226
|
+
config.s3_server_side_encryption unless
|
227
|
+
@fields.key?(:server_side_encryption)
|
228
|
+
@fields.delete(:server_side_encryption) if
|
229
|
+
@fields[:server_side_encryption].nil?
|
230
|
+
end
|
231
|
+
|
232
|
+
# @return [Boolean] True if {#url} generates an HTTPS url.
|
233
|
+
def secure?
|
234
|
+
@secure
|
235
|
+
end
|
236
|
+
|
237
|
+
# @return [URI::HTTP, URI::HTTPS] The URL to which the form
|
238
|
+
# fields should be POSTed. If you are using the fields in
|
239
|
+
# an HTML form, this is the URL to put in the `action`
|
240
|
+
# attribute of the form tag.
|
241
|
+
def url
|
242
|
+
req = Request.new
|
243
|
+
req.bucket = bucket.name
|
244
|
+
req.host = config.s3_endpoint
|
245
|
+
req.force_path_style = config.s3_force_path_style
|
246
|
+
build_uri(req)
|
247
|
+
end
|
248
|
+
|
249
|
+
# Lets you specify conditions on a field. See
|
250
|
+
# {PresignedPost#where} for usage examples.
|
251
|
+
class ConditionBuilder
|
252
|
+
|
253
|
+
# @api private
|
254
|
+
def initialize(post, field)
|
255
|
+
@post = post
|
256
|
+
@field = field
|
257
|
+
end
|
258
|
+
|
259
|
+
# Specifies that the value of the field must equal the
|
260
|
+
# provided value.
|
261
|
+
def is(value)
|
262
|
+
if @field == :content_length
|
263
|
+
self.in(value)
|
264
|
+
else
|
265
|
+
@post.with_equality_condition(@field, value)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
|
269
|
+
# Specifies that the value of the field must begin with the
|
270
|
+
# provided value. If you are specifying a condition on the
|
271
|
+
# "key" field, note that this check takes place after the
|
272
|
+
# `${filename}` variable is expanded. This is only valid
|
273
|
+
# for the following fields:
|
274
|
+
#
|
275
|
+
# * `:key`
|
276
|
+
# * `:cache_control`
|
277
|
+
# * `:content_type`
|
278
|
+
# * `:content_disposition`
|
279
|
+
# * `:content_encoding`
|
280
|
+
# * `:expires_header`
|
281
|
+
# * `:acl`
|
282
|
+
# * `:success_action_redirect`
|
283
|
+
# * metadata fields (see {#where_metadata})
|
284
|
+
def starts_with(prefix)
|
285
|
+
@post.with_prefix_condition(@field, prefix)
|
286
|
+
end
|
287
|
+
|
288
|
+
# Specifies that the value of the field must be in the given
|
289
|
+
# range. This may only be used to constrain the
|
290
|
+
# `:content_length` field,
|
291
|
+
# e.g. `presigned_post.with(:conent_length).in(1..4)`.
|
292
|
+
def in(range)
|
293
|
+
@post.refine(:content_length => range)
|
294
|
+
end
|
295
|
+
|
296
|
+
end
|
297
|
+
|
298
|
+
# Adds a condition to the policy for the POST. Use
|
299
|
+
# {#where_metadata} to add metadata conditions.
|
300
|
+
#
|
301
|
+
# @example Restricting the ACL to "bucket-owner" ACLs
|
302
|
+
# presigned_post.where(:acl).starts_with("bucket-owner")
|
303
|
+
#
|
304
|
+
# @param [Symbol] field The field for which a condition should
|
305
|
+
# be added. In addition to any arbitrary values you have set,
|
306
|
+
# the following values are also permitted:
|
307
|
+
#
|
308
|
+
# * `:key`
|
309
|
+
# * `:content_length`
|
310
|
+
# * `:cache_control`
|
311
|
+
# * `:content_type`
|
312
|
+
# * `:content_disposition`
|
313
|
+
# * `:content_encoding`
|
314
|
+
# * `:expires_header`
|
315
|
+
# * `:acl`
|
316
|
+
# * `:success_action_redirect`
|
317
|
+
# * `:success_action_status`
|
318
|
+
#
|
319
|
+
# @return [ConditionBuilder] An object that allows you to
|
320
|
+
# specify a condition on the field.
|
321
|
+
def where(field)
|
322
|
+
ConditionBuilder.new(self, field)
|
323
|
+
end
|
324
|
+
|
325
|
+
# Adds a condition to the policy for the POST to constrain the
|
326
|
+
# values of metadata fields uploaded with the object. If a
|
327
|
+
# metadata field does not have a condition associated with it
|
328
|
+
# and is not specified in the constructor (see {#metadata})
|
329
|
+
# then S3 will reject it.
|
330
|
+
#
|
331
|
+
# @param [Symbol, String] field The name of the metadata
|
332
|
+
# attribute. For example, `:color` corresponds to the
|
333
|
+
# "x-amz-meta-color" field in the POST body.
|
334
|
+
#
|
335
|
+
# @return [ConditionBuilder] An object that allows you to
|
336
|
+
# specify a condition on the metadata attribute.
|
337
|
+
# def where_metadata(field)
|
338
|
+
# where("x-amz-meta-#{field}")
|
339
|
+
# end
|
340
|
+
|
341
|
+
# @return [String] The Base64-encoded JSON policy document.
|
342
|
+
def policy
|
343
|
+
json = {
|
344
|
+
"expiration" => format_expiration,
|
345
|
+
"conditions" => generate_conditions,
|
346
|
+
"callbackUrl" => callback_url,
|
347
|
+
"callbackBody" => callback_body,
|
348
|
+
"callbackBodyType" => callback_body_type,
|
349
|
+
"callbackHost" => callback_host
|
350
|
+
}.to_json
|
351
|
+
Base64.encode64(json).tr("\n","")
|
352
|
+
end
|
353
|
+
|
354
|
+
# @return [Hash] A collection of form fields (including a
|
355
|
+
# signature and a policy) that can be used to POST data to
|
356
|
+
# S3. Additional form fields may be added after the fact as
|
357
|
+
# long as they are described by a policy condition (see
|
358
|
+
# {#where}).
|
359
|
+
def fields
|
360
|
+
|
361
|
+
secret = config.credential_provider.secret_access_key
|
362
|
+
signature = Core::Signers::Base.sign(secret, policy, 'sha1')
|
363
|
+
|
364
|
+
fields = {
|
365
|
+
"AWSAccessKeyId" => config.credential_provider.access_key_id,
|
366
|
+
"key" => key,
|
367
|
+
"bucket" => bucket.name,
|
368
|
+
"policy" => policy,
|
369
|
+
"signature" => signature
|
370
|
+
}.merge(optional_fields)
|
371
|
+
|
372
|
+
if token = config.credential_provider.session_token
|
373
|
+
fields["x-amz-security-token"] = token
|
374
|
+
end
|
375
|
+
|
376
|
+
fields.merge(optional_fields)
|
377
|
+
|
378
|
+
end
|
379
|
+
|
380
|
+
# @api private
|
381
|
+
def with_equality_condition(option_name, value)
|
382
|
+
field_name = field_name(option_name)
|
383
|
+
with_condition(option_name, Hash[[[field_name, value]]])
|
384
|
+
end
|
385
|
+
|
386
|
+
# @api private
|
387
|
+
def with_prefix_condition(option_name, prefix)
|
388
|
+
field_name = field_name(option_name)
|
389
|
+
with_condition(option_name,
|
390
|
+
["starts-with", "$#{field_name}", prefix])
|
391
|
+
end
|
392
|
+
|
393
|
+
# @api private
|
394
|
+
def refine(opts)
|
395
|
+
self.class.new(bucket, {
|
396
|
+
:conditions => conditions,
|
397
|
+
:key => key,
|
398
|
+
:metadata => metadata,
|
399
|
+
:secure => secure?,
|
400
|
+
:content_length => content_length,
|
401
|
+
:expires => expires,
|
402
|
+
:ignore => ignored_fields
|
403
|
+
}.merge(@fields).
|
404
|
+
merge(opts))
|
405
|
+
end
|
406
|
+
|
407
|
+
# @api private
|
408
|
+
private
|
409
|
+
def with_condition(field, condition)
|
410
|
+
conditions = self.conditions.dup
|
411
|
+
(conditions[field] ||= []) << condition
|
412
|
+
refine(:conditions => conditions)
|
413
|
+
end
|
414
|
+
|
415
|
+
# @api private
|
416
|
+
private
|
417
|
+
def format_expiration
|
418
|
+
time = expires || Time.now.utc + 60*60
|
419
|
+
time =
|
420
|
+
case time
|
421
|
+
when Time
|
422
|
+
time
|
423
|
+
when DateTime
|
424
|
+
Time.parse(time.to_s)
|
425
|
+
when Integer
|
426
|
+
(Time.now + time)
|
427
|
+
when String
|
428
|
+
Time.parse(time)
|
429
|
+
end
|
430
|
+
time.utc.iso8601
|
431
|
+
end
|
432
|
+
|
433
|
+
# @api private
|
434
|
+
private
|
435
|
+
def range_value(range)
|
436
|
+
case range
|
437
|
+
when Integer
|
438
|
+
range..range
|
439
|
+
when Range
|
440
|
+
range
|
441
|
+
end
|
442
|
+
end
|
443
|
+
|
444
|
+
# @api private
|
445
|
+
private
|
446
|
+
def split_range(range)
|
447
|
+
range = range_value(range)
|
448
|
+
[range.begin,
|
449
|
+
(range.exclude_end? ?
|
450
|
+
range.end-1 :
|
451
|
+
range.end)]
|
452
|
+
end
|
453
|
+
|
454
|
+
# @api private
|
455
|
+
private
|
456
|
+
def optional_fields
|
457
|
+
fields = @fields.keys.inject({}) do |fields, option_name|
|
458
|
+
fields[field_name(option_name)] =
|
459
|
+
field_value(option_name)
|
460
|
+
fields
|
461
|
+
end
|
462
|
+
|
463
|
+
@metadata.each do |key, value|
|
464
|
+
# fields["x-amz-meta-#{key}"] = value.to_s
|
465
|
+
fields["#{key}"] = value.to_s
|
466
|
+
end
|
467
|
+
|
468
|
+
fields
|
469
|
+
end
|
470
|
+
|
471
|
+
# @api private
|
472
|
+
private
|
473
|
+
def field_name(option_name)
|
474
|
+
case option_name
|
475
|
+
when :expires_header
|
476
|
+
"Expires"
|
477
|
+
when :server_side_encryption
|
478
|
+
"x-amz-server-side-encryption"
|
479
|
+
when :key, "Key", :policy, "Policy", :bucket, "Bucket"
|
480
|
+
option_name.to_s.downcase
|
481
|
+
when :acl, :success_action_redirect, :success_action_status
|
482
|
+
option_name.to_s
|
483
|
+
else
|
484
|
+
# e.g. Cache-Control from cache_control
|
485
|
+
field_name = option_name.to_s.tr("_", "-").
|
486
|
+
gsub(/-(.)/) { |m| m.upcase }
|
487
|
+
field_name[0,1] = field_name[0,1].upcase
|
488
|
+
field_name
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
# @api private
|
493
|
+
private
|
494
|
+
def field_value(option_name)
|
495
|
+
case option_name
|
496
|
+
when :acl
|
497
|
+
@fields[:acl].to_s.tr("_", "-")
|
498
|
+
when :server_side_encryption
|
499
|
+
value = @fields[:server_side_encryption]
|
500
|
+
if value.kind_of?(Symbol)
|
501
|
+
value.to_s.upcase
|
502
|
+
else
|
503
|
+
value.to_s
|
504
|
+
end
|
505
|
+
else
|
506
|
+
@fields[option_name].to_s
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
# @api private
|
511
|
+
private
|
512
|
+
def generate_conditions
|
513
|
+
|
514
|
+
conditions = self.conditions.inject([]) do |list, (field, field_conds)|
|
515
|
+
list + field_conds
|
516
|
+
end
|
517
|
+
|
518
|
+
conditions << { "bucket" => bucket.name }
|
519
|
+
conditions += key_conditions
|
520
|
+
conditions += optional_fields.map { |(n, v)| Hash[[[n, v]]] }
|
521
|
+
conditions += range_conditions
|
522
|
+
conditions += ignored_conditions
|
523
|
+
|
524
|
+
if token = config.credential_provider.session_token
|
525
|
+
conditions << { "x-amz-security-token" => token }
|
526
|
+
end
|
527
|
+
|
528
|
+
conditions
|
529
|
+
|
530
|
+
end
|
531
|
+
|
532
|
+
# @api private
|
533
|
+
private
|
534
|
+
def ignored_conditions
|
535
|
+
ignored_fields.map do |field|
|
536
|
+
["starts-with", "$#{field}", ""]
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
# @api private
|
541
|
+
private
|
542
|
+
def range_conditions
|
543
|
+
if content_length
|
544
|
+
[["content-length-range", *split_range(content_length)]]
|
545
|
+
else
|
546
|
+
[]
|
547
|
+
end
|
548
|
+
end
|
549
|
+
|
550
|
+
# @api private
|
551
|
+
private
|
552
|
+
def key_conditions
|
553
|
+
[if key && key.include?("${filename}")
|
554
|
+
["starts-with", "$key", key[/^(.*)\$\{filename\}/, 1]]
|
555
|
+
elsif key
|
556
|
+
{ "key" => key }
|
557
|
+
else
|
558
|
+
["starts-with", "$key", ""]
|
559
|
+
end]
|
560
|
+
end
|
561
|
+
|
562
|
+
# @api private
|
563
|
+
private
|
564
|
+
def build_uri(request)
|
565
|
+
uri_class = secure? ? URI::HTTPS : URI::HTTP
|
566
|
+
uri_class.build(:host => request.host,
|
567
|
+
:path => request.path,
|
568
|
+
:query => request.querystring)
|
569
|
+
end
|
570
|
+
|
571
|
+
end
|
572
|
+
|
573
|
+
end
|
574
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
|
3
|
+
module MSS
|
4
|
+
class S3
|
5
|
+
# @api private
|
6
|
+
module RegionDetection
|
7
|
+
|
8
|
+
private
|
9
|
+
|
10
|
+
def retry_server_errors(&block)
|
11
|
+
response = super
|
12
|
+
if requires_sigv4?(response)
|
13
|
+
detect_region_and_retry(response, &block)
|
14
|
+
else
|
15
|
+
response
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def requires_sigv4?(resp)
|
20
|
+
resp.http_response.status == 400 &&
|
21
|
+
resp.http_response.body &&
|
22
|
+
resp.http_response.body.include?('Please use MSS4-HMAC-SHA256')
|
23
|
+
end
|
24
|
+
|
25
|
+
def detect_region_and_retry(response, &retry_block)
|
26
|
+
updgrade_to_v4(response, 'us-east-1')
|
27
|
+
yield
|
28
|
+
return if response.http_response.status == 200
|
29
|
+
actual_region = region_from_location_header(response)
|
30
|
+
updgrade_to_v4(response, actual_region)
|
31
|
+
log_region_warning(response, actual_region)
|
32
|
+
yield
|
33
|
+
end
|
34
|
+
|
35
|
+
def updgrade_to_v4(response, region)
|
36
|
+
bucket = response.request_options[:bucket_name]
|
37
|
+
if response.http_request.body_stream.respond_to?(:rewind)
|
38
|
+
response.http_request.body_stream.rewind
|
39
|
+
end
|
40
|
+
response.http_request.headers.delete('authorization')
|
41
|
+
response.http_request.headers.delete('x-amz-security-token')
|
42
|
+
response.http_request.host = new_hostname(response, region)
|
43
|
+
new_v4_signer(region).sign_request(response.http_request)
|
44
|
+
end
|
45
|
+
|
46
|
+
def region_from_location_header(response)
|
47
|
+
location = response.http_response.headers['location'].first
|
48
|
+
location.match(/s3\.(.+?)\.amazonmss\.com/)[1]
|
49
|
+
end
|
50
|
+
|
51
|
+
def new_v4_signer(region)
|
52
|
+
Core::Signers::Version4.new(credential_provider, 's3', region)
|
53
|
+
end
|
54
|
+
|
55
|
+
def new_hostname(response, region)
|
56
|
+
bucket = response.request_options[:bucket_name]
|
57
|
+
if region == 'us-east-1'
|
58
|
+
's3-external-1.amazonmss.com'
|
59
|
+
else
|
60
|
+
"s3.#{region}.amazonmss.com"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def log_region_warning(response, actual_region)
|
65
|
+
bucket_name = response.request_options[:bucket_name]
|
66
|
+
S3::BUCKET_REGIONS[bucket_name] = actual_region
|
67
|
+
log_warning("S3 client configured for #{@region.inspect} " +
|
68
|
+
"but the bucket #{bucket_name.inspect} is in" +
|
69
|
+
"#{actual_region.inspect}; Please configure the proper region " +
|
70
|
+
"to avoid multiple unecessary redirects and signing attempts\n")
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# Copyright 2011-2013 Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License"). You
|
4
|
+
# may not use this file except in compliance with the License. A copy of
|
5
|
+
# the License is located at
|
6
|
+
#
|
7
|
+
#
|
8
|
+
# or in the "license" file accompanying this file. This file is
|
9
|
+
# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF
|
10
|
+
# ANY KIND, either express or implied. See the License for the specific
|
11
|
+
# language governing permissions and limitations under the License.
|
12
|
+
|
13
|
+
require 'uri'
|
14
|
+
require 'time'
|
15
|
+
|
16
|
+
module MSS
|
17
|
+
class S3
|
18
|
+
|
19
|
+
# @api private
|
20
|
+
class Request < Core::Http::Request
|
21
|
+
|
22
|
+
include Core::UriEscape
|
23
|
+
|
24
|
+
# @return [bucket] S3 bucket name
|
25
|
+
attr_accessor :bucket
|
26
|
+
|
27
|
+
# @return [String] S3 object key
|
28
|
+
attr_accessor :key
|
29
|
+
|
30
|
+
# @api private
|
31
|
+
attr_accessor :force_path_style
|
32
|
+
|
33
|
+
def host
|
34
|
+
path_style? ? @host : "#{bucket}.#{@host}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def path_style?
|
38
|
+
if force_path_style
|
39
|
+
true
|
40
|
+
else
|
41
|
+
Client.path_style_bucket_name?(bucket)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def uri
|
46
|
+
parts = []
|
47
|
+
parts << bucket if bucket and path_style?
|
48
|
+
parts << escape_path(key) if key
|
49
|
+
|
50
|
+
path = '/' + parts.join('/')
|
51
|
+
querystring = url_encoded_params
|
52
|
+
|
53
|
+
uri = ''
|
54
|
+
uri << path
|
55
|
+
uri << "?#{querystring}" if querystring
|
56
|
+
uri
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|