gcs-signer 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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: []