gcs-signer 0.3.0 → 0.4.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 +5 -5
- data/README.md +26 -13
- data/lib/gcs_signer.rb +95 -62
- data/lib/gcs_signer/version.rb +5 -0
- metadata +35 -19
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6b07c6432f999db39f6eaba9ac988a75c6b2617d5c9acbb4c739ae302d2e8e6d
|
4
|
+
data.tar.gz: dbb4cea0d3745b73cd6fc5b3714a853e069d3b59c6665dd53cdb970ca90b2814
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ee1368958bb27c1ac8c595a6c5718b39e87d655da471432959929a3ee46641a5ef7e865c6d1a7a976ab55b1e570e282454d6ff5095ab2797cba6935ef4fe7fa2
|
7
|
+
data.tar.gz: 5394700acd1e1b2a36420bed4e9786750fbef5d8b23c64079ed041e9a09699ff63a34b944b594dae3a530d9b39570050e60df47d7a63bfd426661246891ee823
|
data/README.md
CHANGED
@@ -4,9 +4,8 @@ Simple signed URL generator for Google Cloud Storage.
|
|
4
4
|
|
5
5
|
## Features
|
6
6
|
|
7
|
-
* No additional gems required.
|
8
7
|
* No network connection required to generate signed URL.
|
9
|
-
* Can read JSON
|
8
|
+
* Can read JSON service-account credentials from environment variables. So it can be used with [google-cloud-ruby](https://github.com/GoogleCloudPlatform/google-cloud-ruby) without additional configurations.
|
10
9
|
|
11
10
|
## Installation
|
12
11
|
|
@@ -16,21 +15,33 @@ gem install gcs-signer
|
|
16
15
|
|
17
16
|
## Usage
|
18
17
|
|
19
|
-
|
18
|
+
### Authentication
|
19
|
+
|
20
|
+
If you already configured `GOOGLE_APPLICATION_CREDENTIALS` for google-cloud-ruby gem, just
|
21
|
+
|
22
|
+
```ruby
|
23
|
+
signer = GcsSigner.new
|
24
|
+
```
|
25
|
+
|
26
|
+
You can also set `GOOGLE_CLOUD_KEYFILE_JSON` environment varialble to the content of service-account.json.
|
20
27
|
|
21
28
|
```ruby
|
29
|
+
puts ENV["GOOGLE_CLOUD_KEYFILE_JSON"]
|
30
|
+
# => { "type": "service_account", ...
|
22
31
|
signer = GcsSigner.new
|
23
32
|
```
|
24
33
|
|
25
|
-
or you can give path of the
|
34
|
+
or you can give path of the service account file, or contents of it without using environment variables.
|
26
35
|
|
27
36
|
```ruby
|
28
37
|
signer = GcsSigner.new path: "/home/leo/path/to/service_account.json"
|
29
38
|
|
30
|
-
signer = GcsSigner.new
|
39
|
+
signer = GcsSigner.new keyfile_json: '{ "type": "service_account", ...'
|
31
40
|
```
|
32
41
|
|
33
|
-
|
42
|
+
### Signing URL
|
43
|
+
|
44
|
+
`#sign_url` to generate signed URL.
|
34
45
|
|
35
46
|
```ruby
|
36
47
|
# The signed URL is valid for 5 minutes by default.
|
@@ -42,14 +53,16 @@ signer.sign_url "bucket-name", "object-name",
|
|
42
53
|
|
43
54
|
signer.sign_url "bucket-name", "object_name", valid_for: 600
|
44
55
|
|
45
|
-
# If you use AcriveSupport in your project, you can
|
46
|
-
signer.sign_url "
|
56
|
+
# If you use AcriveSupport in your project, you can use some sugar like:
|
57
|
+
signer.sign_url "bucket", "object", valid_for: 45.minutes
|
58
|
+
signer.sign_url "bucket", "object", expires_at: 5.minutes.from_now
|
59
|
+
|
60
|
+
# You can set response_content_disposition and response_content_type to change response headers.
|
61
|
+
signer.sign_url "bucket", "object", response_content_type: "video/mp4"
|
62
|
+
signer.sign_url "bucket", "object", response_content_disposition: "attachment; filename=video.mp4"
|
47
63
|
|
48
|
-
#
|
49
|
-
|
50
|
-
signer.sign_url "buekct", "object", google_access_id: "sangwon@sha.kr",
|
51
|
-
method: "PUT", content_type: "text/plain",
|
52
|
-
md5: "beefbeef..."
|
64
|
+
# You can use V4 signing if you prefer longer URL
|
65
|
+
signer.sign_url "bucket", "object", version: :v4
|
53
66
|
```
|
54
67
|
|
55
68
|
## License
|
data/lib/gcs_signer.rb
CHANGED
@@ -1,8 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "openssl"
|
4
|
-
require "base64"
|
2
|
+
|
5
3
|
require "json"
|
4
|
+
require "base64"
|
5
|
+
require "openssl"
|
6
|
+
require "addressable"
|
6
7
|
|
7
8
|
# Creates signed_url for a file on Google Cloud Storage.
|
8
9
|
#
|
@@ -10,34 +11,36 @@ require "json"
|
|
10
11
|
# signer.sign "your-bucket", "object/name"
|
11
12
|
# # => "https://storage.googleapis.com/your-bucket/object/name?..."
|
12
13
|
class GcsSigner
|
13
|
-
|
14
|
+
ENV_KEYFILE_PATH = ENV["GOOGLE_CLOUD_KEYFILE"] || ENV["GOOGLE_APPLICATION_CREDENTIALS"]
|
15
|
+
ENV_KEYFILE_JSON = ENV["GOOGLE_CLOUD_KEYFILE_JSON"]
|
16
|
+
DEFAULT_GCS_URL = Addressable::URI.new(
|
17
|
+
scheme: "https", host: "storage.googleapis.com"
|
18
|
+
).freeze
|
14
19
|
|
15
20
|
# gcs-signer requires credential that can access to GCS.
|
16
21
|
# [path] the path of the service_account json file.
|
17
|
-
# [
|
22
|
+
# [keyfile_string] ...or the content of the service_account json file.
|
18
23
|
# [gcs_url] Custom GCS url when signing a url.
|
19
24
|
#
|
20
25
|
# or if you also use \+google-cloud+ gem. you can authenticate
|
21
26
|
# using environment variable that uses.
|
22
|
-
def initialize(path: nil,
|
23
|
-
|
24
|
-
|
27
|
+
def initialize(path: nil, keyfile_json: nil, gcs_url: DEFAULT_GCS_URL)
|
28
|
+
keyfile_json ||= path.nil? ? look_for_environment_variables : File.read(path)
|
29
|
+
fail AuthError, "No credentials given." if keyfile_json.nil?
|
25
30
|
|
26
|
-
|
27
|
-
@credentials = JSON.parse(json_string)
|
31
|
+
@credentials = JSON.parse(keyfile_json)
|
28
32
|
@key = OpenSSL::PKey::RSA.new(@credentials["private_key"])
|
29
|
-
|
30
|
-
@gcs_url = gcs_url || "https://storage.googleapis.com"
|
33
|
+
@gcs_url = Addressable::URI.parse(gcs_url)
|
31
34
|
end
|
32
35
|
|
33
36
|
# @return [String] Signed url
|
34
37
|
# Generates signed url.
|
35
38
|
# [bucket] the name of the Cloud Storage bucket that contains the object.
|
36
|
-
# [
|
39
|
+
# [key] the name of the object for signed url.
|
37
40
|
# Variable options are available:
|
41
|
+
# [version] signature version; \+:v2+ or \+:v4+
|
38
42
|
# [expires] Time(stamp in UTC) when the signed url expires.
|
39
43
|
# [valid_for] ...or how much seconds is the signed url available.
|
40
|
-
# [google_access_id] Just in case if you want to change \+GoogleAccessId+
|
41
44
|
# [response_content_disposition] Content-Disposition of the signed URL.
|
42
45
|
# [response_content_type] Content-Type of the signed URL.
|
43
46
|
#
|
@@ -57,26 +60,58 @@ class GcsSigner
|
|
57
60
|
# # If you use ActiveSupport, you can also do some magic.
|
58
61
|
# signer.sign_url("bucket", "path/to/file", valid_for: 40.minutes)
|
59
62
|
#
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
63
|
+
def sign_url(bucket, key, version: :v2, **options)
|
64
|
+
case version
|
65
|
+
when :v2
|
66
|
+
sign_url_v2(bucket, key, **options)
|
67
|
+
when :v4
|
68
|
+
sign_url_v4(bucket, key, **options)
|
69
|
+
else
|
70
|
+
fail ArgumentError, "Version not supported: #{version.inspect}"
|
71
|
+
end
|
72
|
+
end
|
66
73
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
)
|
74
|
+
def sign_url_v2(bucket, key, method: "GET", valid_for: 300, **options)
|
75
|
+
url = @gcs_url + "./#{request_path(bucket, key)}"
|
76
|
+
expires_at = options[:expires] || Time.now.utc.to_i + valid_for.to_i
|
77
|
+
sign_payload = [method, "", "", expires_at.to_i, url.path].join("\n")
|
71
78
|
|
72
|
-
url.
|
73
|
-
|
74
|
-
|
75
|
-
|
79
|
+
url.query_values = (options[:params] || {}).merge(
|
80
|
+
"GoogleAccessId" => @credentials["client_email"],
|
81
|
+
"Expires" => expires_at.to_i,
|
82
|
+
"Signature" => sign_v2(sign_payload),
|
83
|
+
"response-content-disposition" => options[:response_content_disposition],
|
84
|
+
"response-content-type" => options[:response_content_type]
|
85
|
+
).compact
|
76
86
|
|
77
87
|
url.to_s
|
78
88
|
end
|
79
89
|
|
90
|
+
def sign_url_v4(bucket, key, method: "GET", headers: {}, **options)
|
91
|
+
url = @gcs_url + "./#{request_path(bucket, key)}"
|
92
|
+
time = Time.now.utc
|
93
|
+
|
94
|
+
request_headers = headers.merge(host: @gcs_url.host).transform_keys(&:downcase)
|
95
|
+
signed_headers = request_headers.keys.sort.join(";")
|
96
|
+
scopes = [time.strftime("%Y%m%d"), "auto", "storage", "goog4_request"].join("/")
|
97
|
+
|
98
|
+
url.query_values = build_query_params(time, scopes, signed_headers, **options)
|
99
|
+
|
100
|
+
canonical_request = [
|
101
|
+
method, url.path.to_s, url.query,
|
102
|
+
*request_headers.sort.map { |header| header.join(":") },
|
103
|
+
"", signed_headers, "UNSIGNED-PAYLOAD"
|
104
|
+
].join("\n")
|
105
|
+
|
106
|
+
sign_payload = [
|
107
|
+
"GOOG4-RSA-SHA256", time.strftime("%Y%m%dT%H%M%SZ"), scopes,
|
108
|
+
Digest::SHA256.hexdigest(canonical_request)
|
109
|
+
].join("\n")
|
110
|
+
|
111
|
+
url.query += "&X-Goog-Signature=#{sign_v4(sign_payload)}"
|
112
|
+
url.to_s
|
113
|
+
end
|
114
|
+
|
80
115
|
# @return [String] contains \+project_id+ and \+client_email+
|
81
116
|
# Prevents confidential information (like private key) from exposing
|
82
117
|
# when used with interactive shell such as \+pry+ and \+irb+.
|
@@ -88,53 +123,51 @@ class GcsSigner
|
|
88
123
|
|
89
124
|
private
|
90
125
|
|
91
|
-
# Look for environment variable which stores service_account.
|
92
126
|
def look_for_environment_variables
|
93
|
-
|
94
|
-
|
95
|
-
end
|
127
|
+
ENV_KEYFILE_PATH.nil? ? ENV_KEYFILE_JSON : File.read(ENV_KEYFILE_PATH)
|
128
|
+
end
|
96
129
|
|
97
|
-
|
130
|
+
def request_path(bucket, object)
|
131
|
+
[
|
132
|
+
bucket,
|
133
|
+
Addressable::URI.encode_component(
|
134
|
+
object, Addressable::URI::CharacterClasses::UNRESERVED
|
135
|
+
)
|
136
|
+
].join("/")
|
98
137
|
end
|
99
138
|
|
100
|
-
|
101
|
-
|
102
|
-
@key.sign OpenSSL::Digest::SHA256.new, string
|
139
|
+
def sign_v2(string)
|
140
|
+
Base64.strict_encode64(sign(string))
|
103
141
|
end
|
104
142
|
|
105
|
-
def
|
106
|
-
|
107
|
-
method: "GET", content_md5: nil,
|
108
|
-
content_type: nil,
|
109
|
-
expires: Time.now.utc.to_i + (options[:valid_for] || 300).to_i,
|
110
|
-
google_access_id: @credentials["client_email"]
|
111
|
-
}.merge(options)
|
143
|
+
def sign_v4(string)
|
144
|
+
sign(string).unpack1("H*")
|
112
145
|
end
|
113
146
|
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
options[:content_md5],
|
118
|
-
options[:content_type],
|
119
|
-
options[:expires].to_i,
|
120
|
-
url.path
|
121
|
-
].join "\n"
|
147
|
+
# Signs the string with the given private key.
|
148
|
+
def sign(string)
|
149
|
+
@key.sign(OpenSSL::Digest.new("SHA256"), string)
|
122
150
|
end
|
123
151
|
|
124
|
-
#
|
125
|
-
def
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
152
|
+
# only used in v4
|
153
|
+
def build_query_params(time, scopes, signed_headers, valid_for: 300, **options)
|
154
|
+
goog_expires = if options[:expires]
|
155
|
+
options[:expires].to_i - time.to_i
|
156
|
+
else
|
157
|
+
valid_for.to_i
|
158
|
+
end.clamp(0, 604_800)
|
159
|
+
|
160
|
+
(options[:params] || {}).merge(
|
161
|
+
"X-Goog-Algorithm" => "GOOG4-RSA-SHA256",
|
162
|
+
"X-Goog-Credential" => [@credentials["client_email"], scopes].join("/"),
|
163
|
+
"X-Goog-Date" => time.strftime("%Y%m%dT%H%M%SZ"),
|
164
|
+
"X-Goog-Expires" => goog_expires,
|
165
|
+
"X-Goog-SignedHeaders" => signed_headers,
|
130
166
|
"response-content-disposition" => options[:response_content_disposition],
|
131
167
|
"response-content-type" => options[:response_content_type]
|
132
|
-
|
133
|
-
|
134
|
-
URI.encode_www_form(query)
|
168
|
+
).compact.sort
|
135
169
|
end
|
136
170
|
|
137
171
|
# raised When GcsSigner could not find service_account JSON file.
|
138
|
-
class AuthError < StandardError
|
139
|
-
end
|
172
|
+
class AuthError < StandardError; end
|
140
173
|
end
|
metadata
CHANGED
@@ -1,92 +1,108 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: gcs-signer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sangwon Yi
|
8
8
|
- Minku Lee
|
9
|
-
|
9
|
+
- Larry Kim
|
10
|
+
autorequire:
|
10
11
|
bindir: bin
|
11
12
|
cert_chain: []
|
12
|
-
date:
|
13
|
+
date: 2021-01-21 00:00:00.000000000 Z
|
13
14
|
dependencies:
|
14
15
|
- !ruby/object:Gem::Dependency
|
15
|
-
name:
|
16
|
+
name: addressable
|
16
17
|
requirement: !ruby/object:Gem::Requirement
|
17
18
|
requirements:
|
18
19
|
- - "~>"
|
19
20
|
- !ruby/object:Gem::Version
|
20
|
-
version: '
|
21
|
-
type: :
|
21
|
+
version: '2.7'
|
22
|
+
type: :runtime
|
22
23
|
prerelease: false
|
23
24
|
version_requirements: !ruby/object:Gem::Requirement
|
24
25
|
requirements:
|
25
26
|
- - "~>"
|
26
27
|
- !ruby/object:Gem::Version
|
27
|
-
version: '
|
28
|
+
version: '2.7'
|
28
29
|
- !ruby/object:Gem::Dependency
|
29
30
|
name: pry
|
30
31
|
requirement: !ruby/object:Gem::Requirement
|
31
32
|
requirements:
|
32
33
|
- - "~>"
|
33
34
|
- !ruby/object:Gem::Version
|
34
|
-
version: '0.
|
35
|
+
version: '0.11'
|
36
|
+
type: :development
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - "~>"
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '0.11'
|
43
|
+
- !ruby/object:Gem::Dependency
|
44
|
+
name: rake
|
45
|
+
requirement: !ruby/object:Gem::Requirement
|
46
|
+
requirements:
|
47
|
+
- - "~>"
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '12.3'
|
35
50
|
type: :development
|
36
51
|
prerelease: false
|
37
52
|
version_requirements: !ruby/object:Gem::Requirement
|
38
53
|
requirements:
|
39
54
|
- - "~>"
|
40
55
|
- !ruby/object:Gem::Version
|
41
|
-
version: '
|
56
|
+
version: '12.3'
|
42
57
|
- !ruby/object:Gem::Dependency
|
43
58
|
name: rubocop
|
44
59
|
requirement: !ruby/object:Gem::Requirement
|
45
60
|
requirements:
|
46
61
|
- - "~>"
|
47
62
|
- !ruby/object:Gem::Version
|
48
|
-
version:
|
63
|
+
version: '1.0'
|
49
64
|
type: :development
|
50
65
|
prerelease: false
|
51
66
|
version_requirements: !ruby/object:Gem::Requirement
|
52
67
|
requirements:
|
53
68
|
- - "~>"
|
54
69
|
- !ruby/object:Gem::Version
|
55
|
-
version:
|
70
|
+
version: '1.0'
|
56
71
|
description: |2
|
57
72
|
Simple signed URL generator for Google Cloud Storage.
|
58
|
-
No
|
73
|
+
No API requests required to generate signed URL.
|
59
74
|
email:
|
60
75
|
- sangwon@sha.kr
|
61
76
|
- minku@sha.kr
|
77
|
+
- larry@sha.kr
|
62
78
|
executables: []
|
63
79
|
extensions: []
|
64
80
|
extra_rdoc_files: []
|
65
81
|
files:
|
66
82
|
- README.md
|
67
83
|
- lib/gcs_signer.rb
|
84
|
+
- lib/gcs_signer/version.rb
|
68
85
|
homepage: https://github.com/shakrmedia/gcs-signer
|
69
86
|
licenses:
|
70
87
|
- MIT
|
71
88
|
metadata: {}
|
72
|
-
post_install_message:
|
89
|
+
post_install_message:
|
73
90
|
rdoc_options: []
|
74
91
|
require_paths:
|
75
92
|
- lib
|
76
93
|
required_ruby_version: !ruby/object:Gem::Requirement
|
77
94
|
requirements:
|
78
|
-
- - "
|
95
|
+
- - ">"
|
79
96
|
- !ruby/object:Gem::Version
|
80
|
-
version: '2.
|
97
|
+
version: '2.6'
|
81
98
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
82
99
|
requirements:
|
83
100
|
- - ">="
|
84
101
|
- !ruby/object:Gem::Version
|
85
102
|
version: '0'
|
86
103
|
requirements: []
|
87
|
-
|
88
|
-
|
89
|
-
signing_key:
|
104
|
+
rubygems_version: 3.0.3
|
105
|
+
signing_key:
|
90
106
|
specification_version: 4
|
91
|
-
summary: Simple
|
107
|
+
summary: Simple URL signer for Google Cloud Storage.
|
92
108
|
test_files: []
|