google-cloud-storage 1.25.0 → 1.27.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/AUTHENTICATION.md +8 -2
- data/CHANGELOG.md +53 -0
- data/TROUBLESHOOTING.md +2 -8
- data/lib/google/cloud/storage/bucket.rb +379 -70
- data/lib/google/cloud/storage/bucket/lifecycle.rb +5 -5
- data/lib/google/cloud/storage/convert.rb +4 -3
- data/lib/google/cloud/storage/errors.rb +7 -2
- data/lib/google/cloud/storage/file.rb +115 -27
- 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 +118 -28
- data/lib/google/cloud/storage/service.rb +10 -13
- data/lib/google/cloud/storage/version.rb +1 -1
- metadata +3 -3
@@ -111,7 +111,7 @@ module Google
|
|
111
111
|
# @param [String,Symbol,Array<String,Symbol>] matches_storage_class
|
112
112
|
# Files having any of the storage classes specified by this
|
113
113
|
# condition will be matched. Values include `STANDARD`, `NEARLINE`,
|
114
|
-
# and `
|
114
|
+
# `COLDLINE`, and `ARCHIVE`. `REGIONAL`,`MULTI_REGIONAL`, and
|
115
115
|
# `DURABLE_REDUCED_AVAILABILITY` are supported as legacy storage
|
116
116
|
# classes. Arguments will be converted from symbols and lower-case
|
117
117
|
# to upper-case strings.
|
@@ -162,7 +162,7 @@ module Google
|
|
162
162
|
# @param [String,Symbol,Array<String,Symbol>] matches_storage_class
|
163
163
|
# Files having any of the storage classes specified by this
|
164
164
|
# condition will be matched. Values include `STANDARD`, `NEARLINE`,
|
165
|
-
# and `
|
165
|
+
# `COLDLINE`, and `ARCHIVE`. `REGIONAL`,`MULTI_REGIONAL`, and
|
166
166
|
# `DURABLE_REDUCED_AVAILABILITY` are supported as legacy storage
|
167
167
|
# classes. Arguments will be converted from symbols and lower-case
|
168
168
|
# to upper-case strings.
|
@@ -240,9 +240,9 @@ module Google
|
|
240
240
|
# is `false`, it matches archived files.
|
241
241
|
# @attr [Array<String>] matches_storage_class Files having any of the
|
242
242
|
# storage classes specified by this condition will be matched.
|
243
|
-
# Values include `STANDARD`, `NEARLINE`, and `
|
244
|
-
# `MULTI_REGIONAL`, and `DURABLE_REDUCED_AVAILABILITY`
|
245
|
-
# as legacy storage classes.
|
243
|
+
# Values include `STANDARD`, `NEARLINE`, `COLDLINE`, and `ARCHIVE`.
|
244
|
+
# `REGIONAL`, `MULTI_REGIONAL`, and `DURABLE_REDUCED_AVAILABILITY`
|
245
|
+
# are supported as legacy storage classes.
|
246
246
|
# @attr [Integer] num_newer_versions Relevant only for versioned
|
247
247
|
# files. If the value is N, this condition is satisfied when there
|
248
248
|
# are at least N versions (including the live version) newer than
|
@@ -25,12 +25,13 @@ module Google
|
|
25
25
|
def storage_class_for str
|
26
26
|
return nil if str.nil?
|
27
27
|
return str.map { |s| storage_class_for s } if str.is_a? Array
|
28
|
-
{ "
|
28
|
+
{ "archive" => "ARCHIVE",
|
29
|
+
"coldline" => "COLDLINE",
|
29
30
|
"dra" => "DURABLE_REDUCED_AVAILABILITY",
|
30
31
|
"durable" => "DURABLE_REDUCED_AVAILABILITY",
|
31
|
-
"
|
32
|
-
"coldline" => "COLDLINE",
|
32
|
+
"durable_reduced_availability" => "DURABLE_REDUCED_AVAILABILITY",
|
33
33
|
"multi_regional" => "MULTI_REGIONAL",
|
34
|
+
"nearline" => "NEARLINE",
|
34
35
|
"regional" => "REGIONAL",
|
35
36
|
"standard" => "STANDARD" }[str.to_s.downcase] || str.to_s
|
36
37
|
end
|
@@ -58,8 +58,13 @@ module Google
|
|
58
58
|
##
|
59
59
|
# # SignedUrlUnavailable Error
|
60
60
|
#
|
61
|
-
#
|
62
|
-
# missing credentials
|
61
|
+
# Raised by signed URL methods if the service account credentials
|
62
|
+
# are missing. Service account credentials are acquired by following the
|
63
|
+
# steps in [Service Account Authentication](
|
64
|
+
# https://cloud.google.com/iam/docs/service-accounts).
|
65
|
+
#
|
66
|
+
# @see https://cloud.google.com/storage/docs/access-control/signed-urls Signed URLs
|
67
|
+
#
|
63
68
|
class SignedUrlUnavailable < Google::Cloud::Error
|
64
69
|
end
|
65
70
|
end
|
@@ -431,6 +431,7 @@ module Google
|
|
431
431
|
# * `:standard`
|
432
432
|
# * `:nearline`
|
433
433
|
# * `:coldline`
|
434
|
+
# * `:archive`
|
434
435
|
#
|
435
436
|
# as well as the equivalent strings returned by {File#storage_class} or
|
436
437
|
# {Bucket#storage_class}. For more information, see [Storage
|
@@ -1441,7 +1442,7 @@ module Google
|
|
1441
1442
|
# A {SignedUrlUnavailable} is raised if the service account credentials
|
1442
1443
|
# are missing. Service account credentials are acquired by following the
|
1443
1444
|
# steps in [Service Account Authentication](
|
1444
|
-
# https://cloud.google.com/
|
1445
|
+
# https://cloud.google.com/iam/docs/service-accounts).
|
1445
1446
|
#
|
1446
1447
|
# @see https://cloud.google.com/storage/docs/access-control/signed-urls
|
1447
1448
|
# Signed URLs guide
|
@@ -1466,10 +1467,22 @@ module Google
|
|
1466
1467
|
# use the signed URL.
|
1467
1468
|
# @param [String] issuer Service Account's Client Email.
|
1468
1469
|
# @param [String] client_email Service Account's Client Email.
|
1469
|
-
# @param [OpenSSL::PKey::RSA, String] signing_key Service Account's
|
1470
|
-
# Private Key
|
1471
|
-
#
|
1472
|
-
#
|
1470
|
+
# @param [OpenSSL::PKey::RSA, String, Proc] signing_key Service Account's
|
1471
|
+
# Private Key or a Proc that accepts a single String parameter and returns a
|
1472
|
+
# RSA SHA256 signature using a valid Google Service Account Private Key.
|
1473
|
+
# @param [OpenSSL::PKey::RSA, String, Proc] private_key Service Account's
|
1474
|
+
# Private Key or a Proc that accepts a single String parameter and returns a
|
1475
|
+
# RSA SHA256 signature using a valid Google Service Account Private Key.
|
1476
|
+
# @param [OpenSSL::PKey::RSA, String, Proc] signer Service Account's
|
1477
|
+
# Private Key or a Proc that accepts a single String parameter and returns a
|
1478
|
+
# RSA SHA256 signature using a valid Google Service Account Private Key.
|
1479
|
+
#
|
1480
|
+
# When using this method in environments such as GAE Flexible Environment,
|
1481
|
+
# GKE, or Cloud Functions where the private key is unavailable, it may be
|
1482
|
+
# necessary to provide a Proc (or lambda) via the signer parameter. This
|
1483
|
+
# Proc should return a signature created using a RPC call to the
|
1484
|
+
# [Service Account Credentials signBlob](https://cloud.google.com/iam/docs/reference/credentials/rest/v1/projects.serviceAccounts/signBlob)
|
1485
|
+
# method as shown in the example below.
|
1473
1486
|
# @param [Hash] query Query string parameters to include in the signed
|
1474
1487
|
# URL. The given parameters are not verified by the signature.
|
1475
1488
|
#
|
@@ -1478,11 +1491,29 @@ module Google
|
|
1478
1491
|
# using the URL, but only when the file resource is missing the
|
1479
1492
|
# corresponding values. (These values can be permanently set using
|
1480
1493
|
# {#content_disposition=} and {#content_type=}.)
|
1494
|
+
# @param [String] scheme The URL scheme. The default value is `HTTPS`.
|
1495
|
+
# @param [Boolean] virtual_hosted_style Whether to use a virtual hosted-style
|
1496
|
+
# hostname, which adds the bucket into the host portion of the URI rather
|
1497
|
+
# than the path, e.g. `https://mybucket.storage.googleapis.com/...`.
|
1498
|
+
# For V4 signing, this also sets the `host` header in the canonicalized
|
1499
|
+
# extension headers to the virtual hosted-style host, unless that header is
|
1500
|
+
# supplied via the `headers` param. The default value of `false` uses the
|
1501
|
+
# form of `https://storage.googleapis.com/mybucket`.
|
1502
|
+
# @param [String] bucket_bound_hostname Use a bucket-bound hostname, which
|
1503
|
+
# replaces the `storage.googleapis.com` host with the name of a `CNAME`
|
1504
|
+
# bucket, e.g. a bucket named `gcs-subdomain.my.domain.tld`, or a Google
|
1505
|
+
# Cloud Load Balancer which routes to a bucket you own, e.g.
|
1506
|
+
# `my-load-balancer-domain.tld`.
|
1481
1507
|
# @param [Symbol, String] version The version of the signed credential
|
1482
1508
|
# to create. Must be one of `:v2` or `:v4`. The default value is
|
1483
1509
|
# `:v2`.
|
1484
1510
|
#
|
1485
|
-
# @return [String]
|
1511
|
+
# @return [String] The signed URL.
|
1512
|
+
#
|
1513
|
+
# @raise [SignedUrlUnavailable] If the service account credentials
|
1514
|
+
# are missing. Service account credentials are acquired by following the
|
1515
|
+
# steps in [Service Account Authentication](
|
1516
|
+
# https://cloud.google.com/iam/docs/service-accounts).
|
1486
1517
|
#
|
1487
1518
|
# @example
|
1488
1519
|
# require "google/cloud/storage"
|
@@ -1502,7 +1533,7 @@ module Google
|
|
1502
1533
|
# file = bucket.file "avatars/heidi/400x400.png"
|
1503
1534
|
# shared_url = file.signed_url expires: 300, # 5 minutes from now
|
1504
1535
|
# version: :v4
|
1505
|
-
|
1536
|
+
#
|
1506
1537
|
# @example Using the `issuer` and `signing_key` options:
|
1507
1538
|
# require "google/cloud/storage"
|
1508
1539
|
#
|
@@ -1542,28 +1573,85 @@ module Google
|
|
1542
1573
|
# # Send the `x-goog-resumable:start` header and the content type
|
1543
1574
|
# # with the resumable upload POST request.
|
1544
1575
|
#
|
1545
|
-
|
1546
|
-
|
1547
|
-
|
1548
|
-
|
1576
|
+
# @example Using Cloud IAMCredentials signBlob to create the signature:
|
1577
|
+
# require "google/cloud/storage"
|
1578
|
+
# require "google/apis/iamcredentials_v1"
|
1579
|
+
# require "googleauth"
|
1580
|
+
#
|
1581
|
+
# # Issuer is the service account email that the Signed URL will be signed with
|
1582
|
+
# # and any permission granted in the Signed URL must be granted to the
|
1583
|
+
# # Google Service Account.
|
1584
|
+
# issuer = "service-account@project-id.iam.gserviceaccount.com"
|
1585
|
+
#
|
1586
|
+
# # Create a lambda that accepts the string_to_sign
|
1587
|
+
# signer = lambda do |string_to_sign|
|
1588
|
+
# IAMCredentials = Google::Apis::IamcredentialsV1
|
1589
|
+
# iam_client = IAMCredentials::IAMCredentialsService.new
|
1590
|
+
#
|
1591
|
+
# # Get the environment configured authorization
|
1592
|
+
# scopes = ["https://www.googleapis.com/auth/iam"]
|
1593
|
+
# iam_client.authorization = Google::Auth.get_application_default scopes
|
1594
|
+
#
|
1595
|
+
# request = {
|
1596
|
+
# "payload": string_to_sign,
|
1597
|
+
# }
|
1598
|
+
# resource = "projects/-/serviceAccounts/#{issuer}"
|
1599
|
+
# response = iam_client.sign_service_account_blob resource, request, {}
|
1600
|
+
# response.signed_blob
|
1601
|
+
# end
|
1602
|
+
#
|
1603
|
+
# storage = Google::Cloud::Storage.new
|
1604
|
+
#
|
1605
|
+
# bucket = storage.bucket "my-todo-app"
|
1606
|
+
# file = bucket.file "avatars/heidi/400x400.png", skip_lookup: true
|
1607
|
+
# url = file.signed_url method: "GET", issuer: issuer,
|
1608
|
+
# signer: signer
|
1609
|
+
#
|
1610
|
+
def signed_url method: "GET",
|
1611
|
+
expires: nil,
|
1612
|
+
content_type: nil,
|
1613
|
+
content_md5: nil,
|
1614
|
+
headers: nil,
|
1615
|
+
issuer: nil,
|
1616
|
+
client_email: nil,
|
1617
|
+
signing_key: nil,
|
1618
|
+
private_key: nil,
|
1619
|
+
signer: nil,
|
1620
|
+
query: nil,
|
1621
|
+
scheme: "HTTPS",
|
1622
|
+
virtual_hosted_style: nil,
|
1623
|
+
bucket_bound_hostname: nil,
|
1624
|
+
version: nil
|
1549
1625
|
ensure_service!
|
1550
1626
|
version ||= :v2
|
1551
1627
|
case version.to_sym
|
1552
1628
|
when :v2
|
1553
|
-
|
1554
|
-
|
1555
|
-
|
1556
|
-
|
1557
|
-
|
1558
|
-
|
1559
|
-
|
1629
|
+
sign = File::SignerV2.from_file self
|
1630
|
+
sign.signed_url method: method,
|
1631
|
+
expires: expires,
|
1632
|
+
headers: headers,
|
1633
|
+
content_type: content_type,
|
1634
|
+
content_md5: content_md5,
|
1635
|
+
issuer: issuer,
|
1636
|
+
client_email: client_email,
|
1637
|
+
signing_key: signing_key,
|
1638
|
+
private_key: private_key,
|
1639
|
+
signer: signer,
|
1640
|
+
query: query
|
1560
1641
|
when :v4
|
1561
|
-
|
1562
|
-
|
1563
|
-
|
1564
|
-
|
1565
|
-
|
1566
|
-
|
1642
|
+
sign = File::SignerV4.from_file self
|
1643
|
+
sign.signed_url method: method,
|
1644
|
+
expires: expires,
|
1645
|
+
headers: headers,
|
1646
|
+
issuer: issuer,
|
1647
|
+
client_email: client_email,
|
1648
|
+
signing_key: signing_key,
|
1649
|
+
private_key: private_key,
|
1650
|
+
signer: signer,
|
1651
|
+
query: query,
|
1652
|
+
scheme: scheme,
|
1653
|
+
virtual_hosted_style: virtual_hosted_style,
|
1654
|
+
bucket_bound_hostname: bucket_bound_hostname
|
1567
1655
|
else
|
1568
1656
|
raise ArgumentError, "version '#{version}' not supported"
|
1569
1657
|
end
|
@@ -1732,7 +1820,7 @@ module Google
|
|
1732
1820
|
# Sending nil metadata results in an Apiary runtime error:
|
1733
1821
|
# NoMethodError: undefined method `each' for nil:NilClass
|
1734
1822
|
attr_params.reject! { |k, v| k == :metadata && v.nil? }
|
1735
|
-
Google::Apis::StorageV1::Object.new
|
1823
|
+
Google::Apis::StorageV1::Object.new(**attr_params)
|
1736
1824
|
end
|
1737
1825
|
|
1738
1826
|
protected
|
@@ -1783,12 +1871,12 @@ module Google
|
|
1783
1871
|
user_project: user_project }.delete_if { |_k, v| v.nil? }
|
1784
1872
|
|
1785
1873
|
resp = service.rewrite_file \
|
1786
|
-
bucket, name, new_bucket, new_name, updated_gapi, options
|
1874
|
+
bucket, name, new_bucket, new_name, updated_gapi, **options
|
1787
1875
|
until resp.done
|
1788
1876
|
sleep 1
|
1789
1877
|
retry_options = options.merge token: resp.rewrite_token
|
1790
1878
|
resp = service.rewrite_file \
|
1791
|
-
bucket, name, new_bucket, new_name, updated_gapi, retry_options
|
1879
|
+
bucket, name, new_bucket, new_name, updated_gapi, **retry_options
|
1792
1880
|
end
|
1793
1881
|
resp.resource
|
1794
1882
|
end
|
@@ -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
|