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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: 987f3aadc106b9ecf39f92c61722587746a2793b
4
- data.tar.gz: 00ce402d313a94af0090c0a0f32fb17ab83d2ec1
2
+ SHA256:
3
+ metadata.gz: 6b07c6432f999db39f6eaba9ac988a75c6b2617d5c9acbb4c739ae302d2e8e6d
4
+ data.tar.gz: dbb4cea0d3745b73cd6fc5b3714a853e069d3b59c6665dd53cdb970ca90b2814
5
5
  SHA512:
6
- metadata.gz: 185e0bfed9f2fe6b25cf205e54e50893d44fe24cd2be6d6ac31d7e5022fd207d10fd1af8f66c748f62e6608bc3c19ab47eb3276e96d1dce6cc5a34c36ad8d89b
7
- data.tar.gz: 29cb30b52c1d738ba9cf6c0ffc3b5cb75733372190ebdba454b5fefc61089a89636d0670633a454f911bd7c4d66124e7599caeda632f0dbdd9f4fc674c93935c
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 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.
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
- If you already configured `GOOGLE_CLOUD_KEYFILE` or `GOOGLE_CLOUD_KEYFILE_JSON` for google-cloud-ruby gem, just
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 service_account json file, or contents of it.
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 json_string: '{ "type": "service_account", ...'
39
+ signer = GcsSigner.new keyfile_json: '{ "type": "service_account", ...'
31
40
  ```
32
41
 
33
- then `#sign_url` to generate signed URL.
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 also do some magic like:
46
- signer.sign_url "buekct", "object", valid_for: 45.minutes
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
- # See https://cloud.google.com/storage/docs/access-control/signed-urls
49
- # for other avaliable options.
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
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
- require "uri"
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
- VERSION = "0.3.0"
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
- # [json_string] ...or the content of the service_account json file.
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, json_string: nil, gcs_url: nil)
23
- json_string ||= File.read(path) unless path.nil?
24
- json_string = look_for_environment_variables if json_string.nil?
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
- fail AuthError, "No credentials given." if json_string.nil?
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
- # [object_name] the name of the object for signed url..
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
- # For details information of another options,
61
- # like \+method+, \+md5+, and \+content_type+. See:
62
- # https://cloud.google.com/storage/docs/access-control/signed-urls
63
- #
64
- def sign_url(bucket, object_name, options = {})
65
- options = apply_default_options(options)
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
- url = URI.join(
68
- @gcs_url,
69
- URI.escape("/#{bucket}/"), URI.escape(object_name)
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.query = query_for_signed_url(
73
- sign(string_that_will_be_signed(url, options)),
74
- options
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
- unless ENV["GOOGLE_CLOUD_KEYFILE"].nil?
94
- return File.read(ENV["GOOGLE_CLOUD_KEYFILE"])
95
- end
127
+ ENV_KEYFILE_PATH.nil? ? ENV_KEYFILE_JSON : File.read(ENV_KEYFILE_PATH)
128
+ end
96
129
 
97
- ENV["GOOGLE_CLOUD_KEYFILE_JSON"]
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
- # Signs the string with the given private key.
101
- def sign(string)
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 apply_default_options(options)
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
- def string_that_will_be_signed(url, options)
115
- [
116
- options[:method],
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
- # Escapes and generates query string for actual result.
125
- def query_for_signed_url(signature, options)
126
- query = {
127
- "GoogleAccessId" => options[:google_access_id],
128
- "Expires" => options[:expires].to_i,
129
- "Signature" => Base64.strict_encode64(signature),
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
- }.reject { |_, v| v.nil? }
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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GcsSigner
4
+ VERSION = "0.4.0"
5
+ 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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sangwon Yi
8
8
  - Minku Lee
9
- autorequire:
9
+ - Larry Kim
10
+ autorequire:
10
11
  bindir: bin
11
12
  cert_chain: []
12
- date: 2017-09-06 00:00:00.000000000 Z
13
+ date: 2021-01-21 00:00:00.000000000 Z
13
14
  dependencies:
14
15
  - !ruby/object:Gem::Dependency
15
- name: rake
16
+ name: addressable
16
17
  requirement: !ruby/object:Gem::Requirement
17
18
  requirements:
18
19
  - - "~>"
19
20
  - !ruby/object:Gem::Version
20
- version: '12.0'
21
- type: :development
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: '12.0'
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.9'
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: '0.9'
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: 0.40.0
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: 0.40.0
70
+ version: '1.0'
56
71
  description: |2
57
72
  Simple signed URL generator for Google Cloud Storage.
58
- No additional gems and API requests required to generate signed URL.
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.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
- rubyforge_project:
88
- rubygems_version: 2.6.13
89
- signing_key:
104
+ rubygems_version: 3.0.3
105
+ signing_key:
90
106
  specification_version: 4
91
- summary: Simple signed URL generator for Google Cloud Storage.
107
+ summary: Simple URL signer for Google Cloud Storage.
92
108
  test_files: []