google-cloud-storage 1.25.1 → 1.28.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/AUTHENTICATION.md +8 -2
- data/CHANGELOG.md +56 -0
- data/TROUBLESHOOTING.md +2 -8
- data/lib/google/cloud/storage/bucket.rb +378 -63
- data/lib/google/cloud/storage/bucket/lifecycle.rb +160 -26
- data/lib/google/cloud/storage/errors.rb +7 -2
- data/lib/google/cloud/storage/file.rb +141 -29
- data/lib/google/cloud/storage/file/list.rb +7 -5
- data/lib/google/cloud/storage/file/signer_v2.rb +21 -12
- data/lib/google/cloud/storage/file/signer_v4.rb +245 -60
- data/lib/google/cloud/storage/policy/binding.rb +3 -2
- data/lib/google/cloud/storage/post_object.rb +18 -1
- data/lib/google/cloud/storage/project.rb +116 -26
- data/lib/google/cloud/storage/service.rb +12 -15
- data/lib/google/cloud/storage/version.rb +1 -1
- metadata +4 -3
@@ -77,11 +77,13 @@ module Google
|
|
77
77
|
def next
|
78
78
|
return nil unless next?
|
79
79
|
ensure_service!
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
80
|
+
|
81
|
+
gapi = @service.list_files @bucket, prefix: @prefix,
|
82
|
+
delimiter: @delimiter,
|
83
|
+
token: @token,
|
84
|
+
max: @max,
|
85
|
+
versions: @versions,
|
86
|
+
user_project: @user_project
|
85
87
|
File::List.from_gapi gapi, @service, @bucket, @prefix,
|
86
88
|
@delimiter, @max, @versions,
|
87
89
|
user_project: @user_project
|
@@ -77,13 +77,21 @@ module Google
|
|
77
77
|
end
|
78
78
|
|
79
79
|
def determine_signing_key options = {}
|
80
|
-
options[:signing_key] || options[:private_key] ||
|
81
|
-
|
80
|
+
signing_key = options[:signing_key] || options[:private_key] ||
|
81
|
+
options[:signer] || @service.credentials.signing_key
|
82
|
+
raise SignedUrlUnavailable, error_msg("signing_key (private_key, signer)") unless signing_key
|
83
|
+
signing_key
|
82
84
|
end
|
83
85
|
|
84
86
|
def determine_issuer options = {}
|
85
|
-
options[:issuer] || options[:client_email] ||
|
86
|
-
|
87
|
+
issuer = options[:issuer] || options[:client_email] || @service.credentials.issuer
|
88
|
+
raise SignedUrlUnavailable, error_msg("issuer (client_email)") unless issuer
|
89
|
+
issuer
|
90
|
+
end
|
91
|
+
|
92
|
+
def error_msg attr_name
|
93
|
+
"Service account credentials '#{attr_name}' is missing. To generate service account credentials " \
|
94
|
+
"see https://cloud.google.com/iam/docs/service-accounts"
|
87
95
|
end
|
88
96
|
|
89
97
|
def post_object options
|
@@ -99,8 +107,6 @@ module Google
|
|
99
107
|
i = determine_issuer options
|
100
108
|
s = determine_signing_key options
|
101
109
|
|
102
|
-
raise SignedUrlUnavailable unless i && s
|
103
|
-
|
104
110
|
policy_str = p.to_json
|
105
111
|
policy = Base64.strict_encode64(policy_str).delete "\n"
|
106
112
|
|
@@ -119,18 +125,21 @@ module Google
|
|
119
125
|
i = determine_issuer options
|
120
126
|
s = determine_signing_key options
|
121
127
|
|
122
|
-
raise SignedUrlUnavailable unless i && s
|
123
|
-
|
124
128
|
sig = generate_signature s, signature_str(options)
|
125
129
|
generate_signed_url i, sig, options[:expires], options[:query]
|
126
130
|
end
|
127
131
|
|
128
132
|
def generate_signature signing_key, secret
|
129
|
-
|
130
|
-
|
133
|
+
unencoded_signature = ""
|
134
|
+
if signing_key.is_a? Proc
|
135
|
+
unencoded_signature = signing_key.call secret
|
136
|
+
else
|
137
|
+
unless signing_key.respond_to? :sign
|
138
|
+
signing_key = OpenSSL::PKey::RSA.new signing_key
|
139
|
+
end
|
140
|
+
unencoded_signature = signing_key.sign OpenSSL::Digest::SHA256.new, secret
|
131
141
|
end
|
132
|
-
|
133
|
-
Base64.strict_encode64(signature).delete "\n"
|
142
|
+
Base64.strict_encode64(unencoded_signature).delete "\n"
|
134
143
|
end
|
135
144
|
|
136
145
|
def generate_signed_url issuer, signed_string, expires, query
|
@@ -39,37 +39,83 @@ module Google
|
|
39
39
|
new bucket.name, file_name, bucket.service
|
40
40
|
end
|
41
41
|
|
42
|
-
def
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
42
|
+
def post_object issuer: nil,
|
43
|
+
client_email: nil,
|
44
|
+
signing_key: nil,
|
45
|
+
private_key: nil,
|
46
|
+
signer: nil,
|
47
|
+
expires: nil,
|
48
|
+
fields: nil,
|
49
|
+
conditions: nil,
|
50
|
+
scheme: "https",
|
51
|
+
virtual_hosted_style: nil,
|
52
|
+
bucket_bound_hostname: nil
|
53
|
+
i = determine_issuer issuer, client_email
|
54
|
+
s = determine_signing_key signing_key, private_key, signer
|
55
|
+
|
56
|
+
now = Time.now.utc
|
57
|
+
base_fields = required_fields i, now
|
58
|
+
post_fields = fields.dup || {}
|
59
|
+
post_fields.merge! base_fields
|
60
|
+
|
61
|
+
p = {}
|
62
|
+
p["conditions"] = policy_conditions base_fields, conditions, fields
|
63
|
+
expires ||= 60*60*24
|
64
|
+
p["expiration"] = (now + expires).strftime "%Y-%m-%dT%H:%M:%SZ"
|
65
|
+
|
66
|
+
policy_str = escape_characters p.to_json
|
67
|
+
|
68
|
+
policy = Base64.strict_encode64(policy_str).force_encoding "utf-8"
|
69
|
+
signature = generate_signature s, policy
|
70
|
+
|
71
|
+
post_fields["x-goog-signature"] = signature
|
72
|
+
post_fields["policy"] = policy
|
73
|
+
url = post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname
|
74
|
+
hostname = "#{url}#{bucket_path path_style?(virtual_hosted_style, bucket_bound_hostname)}"
|
75
|
+
Google::Cloud::Storage::PostObject.new hostname, post_fields
|
76
|
+
end
|
77
|
+
|
78
|
+
def signed_url method: "GET",
|
79
|
+
expires: nil,
|
80
|
+
headers: nil,
|
81
|
+
issuer: nil,
|
82
|
+
client_email: nil,
|
83
|
+
signing_key: nil,
|
84
|
+
private_key: nil,
|
85
|
+
signer: nil,
|
86
|
+
query: nil,
|
87
|
+
scheme: "https",
|
88
|
+
virtual_hosted_style: nil,
|
89
|
+
bucket_bound_hostname: nil
|
90
|
+
raise ArgumentError, "method is required" unless method
|
91
|
+
issuer, signer = issuer_and_signer issuer, client_email, signing_key, private_key, signer
|
47
92
|
datetime_now = Time.now.utc
|
48
93
|
goog_date = datetime_now.strftime "%Y%m%dT%H%M%SZ"
|
49
94
|
datestamp = datetime_now.strftime "%Y%m%d"
|
50
95
|
# goog4_request is not checked.
|
51
96
|
scope = "#{datestamp}/auto/storage/goog4_request"
|
52
97
|
|
53
|
-
canonical_headers_str, signed_headers_str = \
|
54
|
-
|
98
|
+
canonical_headers_str, signed_headers_str = canonical_and_signed_headers \
|
99
|
+
headers, virtual_hosted_style, bucket_bound_hostname
|
55
100
|
|
56
101
|
algorithm = "GOOG4-RSA-SHA256"
|
57
102
|
expires = determine_expires expires
|
58
|
-
credential =
|
59
|
-
canonical_query_str = canonical_query query, algorithm,
|
60
|
-
credential, goog_date,
|
61
|
-
expires, signed_headers_str
|
103
|
+
credential = issuer + "/" + scope
|
104
|
+
canonical_query_str = canonical_query query, algorithm, credential, goog_date, expires, signed_headers_str
|
62
105
|
|
63
106
|
# From AWS: You don't include a payload hash in the Canonical
|
64
107
|
# Request, because when you create a presigned URL, you don't know
|
65
108
|
# the payload content because the URL is used to upload an arbitrary
|
66
109
|
# payload. Instead, you use a constant string UNSIGNED-PAYLOAD.
|
110
|
+
payload = headers&.key?("X-Goog-Content-SHA256") ? headers["X-Goog-Content-SHA256"] : "UNSIGNED-PAYLOAD"
|
111
|
+
|
67
112
|
canonical_request = [method,
|
68
|
-
|
113
|
+
file_path(!(virtual_hosted_style || bucket_bound_hostname)),
|
69
114
|
canonical_query_str,
|
70
115
|
canonical_headers_str,
|
71
116
|
signed_headers_str,
|
72
|
-
|
117
|
+
payload].join("\n")
|
118
|
+
|
73
119
|
# Construct string to sign
|
74
120
|
req_sha = Digest::SHA256.hexdigest canonical_request
|
75
121
|
string_to_sign = [algorithm, goog_date, scope, req_sha].join "\n"
|
@@ -78,66 +124,129 @@ module Google
|
|
78
124
|
signature = signer.call string_to_sign
|
79
125
|
|
80
126
|
# Construct signed URL
|
81
|
-
|
127
|
+
hostname = signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname
|
128
|
+
"#{hostname}?#{canonical_query_str}&X-Goog-Signature=#{signature}"
|
129
|
+
end
|
130
|
+
|
131
|
+
# methods below are public visibility only for unit testing
|
132
|
+
def escape_characters str
|
133
|
+
str.split("").map do |s|
|
134
|
+
if !s.ascii_only?
|
135
|
+
escape_special_unicode s
|
136
|
+
else
|
137
|
+
case s
|
138
|
+
when "\\"
|
139
|
+
'\\'
|
140
|
+
when "\b"
|
141
|
+
'\b'
|
142
|
+
when "\f"
|
143
|
+
'\f'
|
144
|
+
when "\n"
|
145
|
+
'\n'
|
146
|
+
when "\r"
|
147
|
+
'\r'
|
148
|
+
when "\t"
|
149
|
+
'\t'
|
150
|
+
when "\v"
|
151
|
+
'\v'
|
152
|
+
else
|
153
|
+
s
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end.join
|
157
|
+
end
|
158
|
+
|
159
|
+
def escape_special_unicode str
|
160
|
+
str.unpack("U*").map { |i| '\u' + i.to_s(16).rjust(4, "0") }.join
|
82
161
|
end
|
83
162
|
|
84
163
|
protected
|
85
164
|
|
165
|
+
def required_fields issuer, time
|
166
|
+
{
|
167
|
+
"key" => @file_name,
|
168
|
+
"x-goog-date" => time.strftime("%Y%m%dT%H%M%SZ"),
|
169
|
+
"x-goog-credential" => "#{issuer}/#{time.strftime '%Y%m%d'}/auto/storage/goog4_request",
|
170
|
+
"x-goog-algorithm" => "GOOG4-RSA-SHA256"
|
171
|
+
}.freeze
|
172
|
+
end
|
173
|
+
|
174
|
+
def policy_conditions base_fields, user_conditions, user_fields
|
175
|
+
# Convert each pair in base_fields hash to a single-entry hash in an array.
|
176
|
+
conditions = base_fields.to_a.map { |f| Hash[*f] }
|
177
|
+
# Add the bucket to the head of the base_fields. This is not returned in the PostObject fields.
|
178
|
+
conditions.unshift "bucket" => @bucket_name
|
179
|
+
# Add user-provided conditions to the head of the conditions array.
|
180
|
+
conditions.unshift user_conditions if user_conditions && !user_conditions.empty?
|
181
|
+
if user_fields
|
182
|
+
# Convert each pair in fields hash to a single-entry hash and add it to the head of the conditions array.
|
183
|
+
user_fields.to_a.reverse.each { |f| conditions.unshift Hash[*f] }
|
184
|
+
end
|
185
|
+
conditions.freeze
|
186
|
+
end
|
187
|
+
|
188
|
+
def signed_url_hostname scheme, virtual_hosted_style, bucket_bound_hostname
|
189
|
+
url = ext_url scheme, virtual_hosted_style, bucket_bound_hostname
|
190
|
+
"#{url}#{file_path path_style?(virtual_hosted_style, bucket_bound_hostname)}"
|
191
|
+
end
|
192
|
+
|
86
193
|
def determine_issuer issuer, client_email
|
87
194
|
# Parse the Service Account and get client id and private key
|
88
195
|
issuer = issuer || client_email || @service.credentials.issuer
|
89
|
-
unless issuer
|
90
|
-
raise SignedUrlUnavailable, "issuer (client_email) missing"
|
91
|
-
end
|
196
|
+
raise SignedUrlUnavailable, error_msg("issuer (client_email)") unless issuer
|
92
197
|
issuer
|
93
198
|
end
|
94
199
|
|
95
|
-
def determine_signing_key signing_key, private_key
|
96
|
-
signing_key = signing_key || private_key ||
|
97
|
-
|
98
|
-
unless signing_key
|
99
|
-
raise SignedUrlUnavailable, "signing_key (private_key) missing"
|
100
|
-
end
|
200
|
+
def determine_signing_key signing_key, private_key, signer
|
201
|
+
signing_key = signing_key || private_key || signer || @service.credentials.signing_key
|
202
|
+
raise SignedUrlUnavailable, error_msg("signing_key (private_key, signer)") unless signing_key
|
101
203
|
signing_key
|
102
204
|
end
|
103
205
|
|
206
|
+
def error_msg attr_name
|
207
|
+
"Service account credentials '#{attr_name}' is missing. To generate service account credentials " \
|
208
|
+
"see https://cloud.google.com/iam/docs/service-accounts"
|
209
|
+
end
|
210
|
+
|
104
211
|
def service_account_signer signer
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
212
|
+
if signer.is_a? Proc
|
213
|
+
lambda do |string_to_sign|
|
214
|
+
sig = signer.call string_to_sign
|
215
|
+
sig.unpack("H*").first
|
216
|
+
end
|
217
|
+
else
|
218
|
+
signer = OpenSSL::PKey::RSA.new signer unless signer.respond_to? :sign
|
219
|
+
# Sign string to sign
|
220
|
+
lambda do |string_to_sign|
|
221
|
+
sig = signer.sign OpenSSL::Digest::SHA256.new, string_to_sign
|
222
|
+
sig.unpack("H*").first
|
223
|
+
end
|
112
224
|
end
|
113
225
|
end
|
114
226
|
|
115
|
-
def issuer_and_signer issuer, client_email, signing_key, private_key
|
227
|
+
def issuer_and_signer issuer, client_email, signing_key, private_key, signer
|
116
228
|
issuer = determine_issuer issuer, client_email
|
117
|
-
signing_key = determine_signing_key signing_key, private_key
|
229
|
+
signing_key = determine_signing_key signing_key, private_key, signer
|
118
230
|
signer = service_account_signer signing_key
|
119
231
|
[issuer, signer]
|
120
232
|
end
|
121
233
|
|
122
|
-
def canonical_and_signed_headers headers
|
123
|
-
|
234
|
+
def canonical_and_signed_headers headers, virtual_hosted_style, bucket_bound_hostname
|
235
|
+
if virtual_hosted_style && bucket_bound_hostname
|
236
|
+
raise "virtual_hosted_style: #{virtual_hosted_style} and bucket_bound_hostname: " \
|
237
|
+
"#{bucket_bound_hostname} params cannot both be passed together"
|
238
|
+
end
|
239
|
+
|
124
240
|
canonical_headers = headers || {}
|
125
241
|
headers_arr = canonical_headers.map do |k, v|
|
126
|
-
[k.downcase, v.strip.gsub(/[^\S\t]+/, " ").gsub(/\t+/, "
|
242
|
+
[k.downcase, v.strip.gsub(/[^\S\t]+/, " ").gsub(/\t+/, " ")]
|
127
243
|
end
|
128
244
|
canonical_headers = Hash[headers_arr]
|
129
|
-
canonical_headers["host"] =
|
130
|
-
|
131
|
-
canonical_headers = canonical_headers.sort_by
|
132
|
-
|
133
|
-
|
134
|
-
canonical_headers_str = ""
|
135
|
-
canonical_headers.each do |k, v|
|
136
|
-
canonical_headers_str += "#{k}:#{v}\n"
|
137
|
-
end
|
138
|
-
signed_headers_str = ""
|
139
|
-
canonical_headers.each_key { |k| signed_headers_str += "#{k};" }
|
140
|
-
signed_headers_str = signed_headers_str.chomp ";"
|
245
|
+
canonical_headers["host"] = host_name virtual_hosted_style, bucket_bound_hostname
|
246
|
+
|
247
|
+
canonical_headers = canonical_headers.sort_by(&:first).to_h
|
248
|
+
canonical_headers_str = canonical_headers.map { |k, v| "#{k}:#{v}\n" }.join
|
249
|
+
signed_headers_str = canonical_headers.keys.join ";"
|
141
250
|
[canonical_headers_str, signed_headers_str]
|
142
251
|
end
|
143
252
|
|
@@ -149,32 +258,108 @@ module Google
|
|
149
258
|
expires
|
150
259
|
end
|
151
260
|
|
152
|
-
def canonical_query query, algorithm, credential, goog_date, expires,
|
153
|
-
signed_headers_str
|
261
|
+
def canonical_query query, algorithm, credential, goog_date, expires, signed_headers_str
|
154
262
|
query ||= {}
|
155
263
|
query["X-Goog-Algorithm"] = algorithm
|
156
264
|
query["X-Goog-Credential"] = credential
|
157
265
|
query["X-Goog-Date"] = goog_date
|
158
266
|
query["X-Goog-Expires"] = expires
|
159
|
-
query["X-Goog-SignedHeaders"] =
|
160
|
-
query = query.
|
161
|
-
|
162
|
-
|
163
|
-
|
267
|
+
query["X-Goog-SignedHeaders"] = signed_headers_str
|
268
|
+
query = query.map { |k, v| [escape_query_param(k), escape_query_param(v)] }.sort_by(&:first).to_h
|
269
|
+
query.map { |k, v| "#{k}=#{v}" }.join "&"
|
270
|
+
end
|
271
|
+
|
272
|
+
##
|
273
|
+
# Only the characters in the regex set [A-Za-z0-9.~_-] must be left un-escaped; all others must be
|
274
|
+
# percent-encoded using %XX UTF-8 style.
|
275
|
+
def escape_query_param str
|
276
|
+
CGI.escape(str.to_s).gsub("%7E", "~")
|
277
|
+
end
|
278
|
+
|
279
|
+
def host_name virtual_hosted_style, bucket_bound_hostname
|
280
|
+
return bucket_bound_hostname if bucket_bound_hostname
|
281
|
+
virtual_hosted_style ? "#{@bucket_name}.storage.googleapis.com" : "storage.googleapis.com"
|
164
282
|
end
|
165
283
|
|
166
284
|
##
|
167
285
|
# The URI-encoded (percent encoded) external path to the file.
|
168
|
-
def
|
169
|
-
path =
|
170
|
-
path
|
171
|
-
|
286
|
+
def file_path path_style
|
287
|
+
path = []
|
288
|
+
path << "/#{@bucket_name}" if path_style
|
289
|
+
path << "/#{String(@file_name)}" if @file_name && !@file_name.empty?
|
290
|
+
CGI.escape(path.join).gsub "%2F", "/"
|
291
|
+
end
|
292
|
+
|
293
|
+
##
|
294
|
+
# The external path to the bucket, with trailing slash.
|
295
|
+
def bucket_path path_style
|
296
|
+
return "/#{@bucket_name}/" if path_style
|
297
|
+
end
|
298
|
+
|
299
|
+
##
|
300
|
+
# The external url to the file.
|
301
|
+
def ext_url scheme, virtual_hosted_style, bucket_bound_hostname
|
302
|
+
url = GOOGLEAPIS_URL.dup
|
303
|
+
if virtual_hosted_style
|
304
|
+
parts = url.split "//"
|
305
|
+
parts[1] = "#{@bucket_name}.#{parts[1]}"
|
306
|
+
parts.join "//"
|
307
|
+
elsif bucket_bound_hostname
|
308
|
+
raise ArgumentError, "scheme is required" unless scheme
|
309
|
+
URI "#{scheme.to_s.downcase}://#{bucket_bound_hostname}"
|
310
|
+
else
|
311
|
+
url
|
312
|
+
end
|
313
|
+
end
|
314
|
+
|
315
|
+
def path_style? virtual_hosted_style, bucket_bound_hostname
|
316
|
+
!(virtual_hosted_style || bucket_bound_hostname)
|
317
|
+
end
|
318
|
+
|
319
|
+
##
|
320
|
+
# The external path to the file, URI-encoded.
|
321
|
+
# Will not URI encode the special `${filename}` variable.
|
322
|
+
# "You can also use the ${filename} variable..."
|
323
|
+
# https://cloud.google.com/storage/docs/xml-api/post-object
|
324
|
+
#
|
325
|
+
def post_object_ext_path
|
326
|
+
path = "/#{@bucket_name}/#{@file_name}"
|
327
|
+
escaped = Addressable::URI.escape path
|
328
|
+
special_var = "${filename}"
|
329
|
+
# Restore the unencoded `${filename}` variable, if present.
|
330
|
+
if path.include? special_var
|
331
|
+
return escaped.gsub "$%7Bfilename%7D", special_var
|
332
|
+
end
|
333
|
+
escaped
|
172
334
|
end
|
173
335
|
|
174
336
|
##
|
175
337
|
# The external url to the file.
|
176
|
-
def
|
177
|
-
|
338
|
+
def post_object_ext_url scheme, virtual_hosted_style, bucket_bound_hostname
|
339
|
+
url = GOOGLEAPIS_URL.dup
|
340
|
+
if virtual_hosted_style
|
341
|
+
parts = url.split "//"
|
342
|
+
parts[1] = "#{@bucket_name}.#{parts[1]}/"
|
343
|
+
parts.join "//"
|
344
|
+
elsif bucket_bound_hostname
|
345
|
+
raise ArgumentError, "scheme is required" unless scheme
|
346
|
+
URI "#{scheme.to_s.downcase}://#{bucket_bound_hostname}/"
|
347
|
+
else
|
348
|
+
url
|
349
|
+
end
|
350
|
+
end
|
351
|
+
|
352
|
+
def generate_signature signing_key, data
|
353
|
+
packed_signature = nil
|
354
|
+
if signing_key.is_a? Proc
|
355
|
+
packed_signature = signing_key.call data
|
356
|
+
else
|
357
|
+
unless signing_key.respond_to? :sign
|
358
|
+
signing_key = OpenSSL::PKey::RSA.new signing_key
|
359
|
+
end
|
360
|
+
packed_signature = signing_key.sign OpenSSL::Digest::SHA256.new, data
|
361
|
+
end
|
362
|
+
packed_signature.unpack("H*").first.force_encoding "utf-8"
|
178
363
|
end
|
179
364
|
end
|
180
365
|
end
|