gcs-signer 0.1.0 → 0.4.1

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: d9da9710c3ec0d2781bb3d0002fc104a9f898ad9
4
- data.tar.gz: 8027a48a1276f076f9d0a46a5bd9aa0841da712e
2
+ SHA256:
3
+ metadata.gz: 736ffe7843310671d2c07459e94ef47ad8b9ebfa24cf46530a5b1138cd954b42
4
+ data.tar.gz: 77ee7afb236a5416f13e1f40aec1e0d37339319476d2167e56febf10c19b2349
5
5
  SHA512:
6
- metadata.gz: 3b1303e21df8a93d2cd23a8ef1636a30d79ff3238643524adfc52efa7b082df53fdcb2d5fbdaa85e6a89b2ef06323d31d481a906106a895ca42bc6b7fb4687d5
7
- data.tar.gz: 8e37da7d200265cdeb64ae82a0395a6c66a26ae92b1f9bb7d1e25698b30a349be783d372114cacb2605a3238e9afcb99fa8909fd8f4586421dbe7edb77edda6f
6
+ metadata.gz: 97985b7cbcd77a932e92a861efc6df31199a56066c255333e41cd4f156364a726de676e6eb85c9e083400cd5321917fc62c39964a4199e8efa9977c2c9316127
7
+ data.tar.gz: 6007e855ec3c35a80444ca98e7f1056cc876236f0e07090acb57e61662b5cfd558e2aea208239b1ba4c285747c958ce1aae12c6533be58318b08956cd446ddc1
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
data/lib/gcs_signer.rb CHANGED
@@ -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,31 +11,39 @@ 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.1.0"
14
+ DEFAULT_GCS_URL = Addressable::URI.new(
15
+ scheme: "https", host: "storage.googleapis.com"
16
+ ).freeze
14
17
 
15
18
  # gcs-signer requires credential that can access to GCS.
16
19
  # [path] the path of the service_account json file.
17
- # [json_string] ...or the content of the service_account json file.
20
+ # [keyfile_string] ...or the content of the service_account json file.
21
+ # [gcs_url] Custom GCS url when signing a url.
18
22
  #
19
23
  # or if you also use \+google-cloud+ gem. you can authenticate
20
24
  # using environment variable that uses.
21
- def initialize(path: nil, json_string: nil)
22
- json_string ||= File.read(path) unless path.nil?
23
- json_string = look_for_environment_variables if json_string.nil?
25
+ def initialize(path: nil, keyfile_json: nil, gcs_url: DEFAULT_GCS_URL)
26
+ keyfile_json ||= path.nil? ? look_for_environment_variables : File.read(path)
27
+ fail AuthError, "No credentials given." if keyfile_json.nil?
24
28
 
25
- fail AuthError, "No credentials given." if json_string.nil?
26
- @credentials = JSON.parse(json_string)
29
+ @credentials = JSON.parse(keyfile_json)
27
30
  @key = OpenSSL::PKey::RSA.new(@credentials["private_key"])
31
+ @gcs_url = Addressable::URI.parse(gcs_url)
28
32
  end
29
33
 
30
34
  # @return [String] Signed url
31
35
  # Generates signed url.
32
36
  # [bucket] the name of the Cloud Storage bucket that contains the object.
33
- # [object_name] the name of the object for signed url..
37
+ # [key] the name of the object for signed url.
34
38
  # Variable options are available:
39
+ # [version] signature version; \+:v2+ or \+:v4+
35
40
  # [expires] Time(stamp in UTC) when the signed url expires.
36
41
  # [valid_for] ...or how much seconds is the signed url available.
37
- # If you don't set those values, it will set to 300 seconds by default.
42
+ # [response_content_disposition] Content-Disposition of the signed URL.
43
+ # [response_content_type] Content-Type of the signed URL.
44
+ #
45
+ # If you set neither \+expires+ nor \+valid_for+,
46
+ # it will set to 300 seconds by default.
38
47
  #
39
48
  # # default is 5 minutes
40
49
  # signer.sign_url("bucket-name", "path/to/file")
@@ -49,19 +58,55 @@ class GcsSigner
49
58
  # # If you use ActiveSupport, you can also do some magic.
50
59
  # signer.sign_url("bucket", "path/to/file", valid_for: 40.minutes)
51
60
  #
52
- # For details information of another options,
53
- # like \+method+, \+md5+, and \+content_type+. See:
54
- # https://cloud.google.com/storage/docs/access-control/signed-urls
55
- #
56
- # Just in case if you want to change \+GoogleAccessId+ in the signed url,
57
- # You can set \+google_access_id+ as an option.
58
- def sign_url(bucket, object_name, options = {})
59
- options = apply_default_options(options)
60
-
61
- url = URI.join "https://storage.googleapis.com",
62
- URI.escape("/#{bucket}/"), URI.escape(object_name)
63
- signature = sign string_that_will_be_signed(url, options)
64
- url.query = query_for_signed_url(signature, options)
61
+ def sign_url(bucket, key, version: :v2, **options)
62
+ case version
63
+ when :v2
64
+ sign_url_v2(bucket, key, **options)
65
+ when :v4
66
+ sign_url_v4(bucket, key, **options)
67
+ else
68
+ fail ArgumentError, "Version not supported: #{version.inspect}"
69
+ end
70
+ end
71
+
72
+ def sign_url_v2(bucket, key, method: "GET", valid_for: 300, **options)
73
+ url = @gcs_url + "./#{request_path(bucket, key)}"
74
+ expires_at = options[:expires] || Time.now.utc.to_i + valid_for.to_i
75
+ sign_payload = [method, "", "", expires_at.to_i, url.path].join("\n")
76
+
77
+ url.query_values = (options[:params] || {}).merge(
78
+ "GoogleAccessId" => @credentials["client_email"],
79
+ "Expires" => expires_at.to_i,
80
+ "Signature" => sign_v2(sign_payload),
81
+ "response-content-disposition" => options[:response_content_disposition],
82
+ "response-content-type" => options[:response_content_type]
83
+ ).compact
84
+
85
+ url.to_s
86
+ end
87
+
88
+ def sign_url_v4(bucket, key, method: "GET", headers: {}, **options)
89
+ url = @gcs_url + "./#{request_path(bucket, key)}"
90
+ time = Time.now.utc
91
+
92
+ request_headers = headers.merge(host: @gcs_url.host).transform_keys(&:downcase)
93
+ signed_headers = request_headers.keys.sort.join(";")
94
+ scopes = [time.strftime("%Y%m%d"), "auto", "storage", "goog4_request"].join("/")
95
+
96
+ url.query_values = build_query_params(time, scopes, signed_headers, **options)
97
+
98
+ canonical_request = [
99
+ method, url.path.to_s, url.query,
100
+ *request_headers.sort.map { |header| header.join(":") },
101
+ "", signed_headers, "UNSIGNED-PAYLOAD"
102
+ ].join("\n")
103
+
104
+ sign_payload = [
105
+ "GOOG4-RSA-SHA256", time.strftime("%Y%m%dT%H%M%SZ"), scopes,
106
+ Digest::SHA256.hexdigest(canonical_request)
107
+ ].join("\n")
108
+
109
+ url.query += "&X-Goog-Signature=#{sign_v4(sign_payload)}"
65
110
  url.to_s
66
111
  end
67
112
 
@@ -76,47 +121,51 @@ class GcsSigner
76
121
 
77
122
  private
78
123
 
79
- # Look for environment variable which stores service_account.
80
124
  def look_for_environment_variables
81
- unless ENV["GOOGLE_CLOUD_KEYFILE"].nil?
82
- return File.read(ENV["GOOGLE_CLOUD_KEYFILE"])
83
- end
125
+ env_keyfile_path = ENV["GOOGLE_CLOUD_KEYFILE"] || ENV["GOOGLE_APPLICATION_CREDENTIALS"]
126
+ env_keyfile_path.nil? ? ENV["GOOGLE_CLOUD_KEYFILE_JSON"] : File.read(env_keyfile_path)
127
+ end
84
128
 
85
- ENV["GOOGLE_CLOUD_KEYFILE_JSON"]
129
+ def request_path(bucket, object)
130
+ [
131
+ bucket, *object.split("/")
132
+ ].map do |str|
133
+ Addressable::URI.encode_component(str, Addressable::URI::CharacterClasses::UNRESERVED)
134
+ end.join("/")
86
135
  end
87
136
 
88
- # Signs the string with the given private key.
89
- def sign(string)
90
- @key.sign OpenSSL::Digest::SHA256.new, string
137
+ def sign_v2(string)
138
+ Base64.strict_encode64(sign(string))
91
139
  end
92
140
 
93
- def apply_default_options(options)
94
- {
95
- method: "GET", content_md5: nil,
96
- content_type: nil,
97
- expires: Time.now.utc.to_i + (options[:valid_for] || 300).to_i,
98
- google_access_id: @credentials["client_email"]
99
- }.merge(options)
141
+ def sign_v4(string)
142
+ sign(string).unpack1("H*")
100
143
  end
101
144
 
102
- def string_that_will_be_signed(url, options)
103
- [
104
- options[:method],
105
- options[:content_md5],
106
- options[:content_type],
107
- options[:expires].to_i,
108
- url.path
109
- ].join "\n"
145
+ # Signs the string with the given private key.
146
+ def sign(string)
147
+ @key.sign(OpenSSL::Digest.new("SHA256"), string)
110
148
  end
111
149
 
112
- # Escapes and generates query string for actual result.
113
- def query_for_signed_url(signature, options)
114
- URI.encode_www_form GoogleAccessId: options[:google_access_id],
115
- Expires: options[:expires].to_i,
116
- Signature: Base64.strict_encode64(signature)
150
+ # only used in v4
151
+ def build_query_params(time, scopes, signed_headers, valid_for: 300, **options)
152
+ goog_expires = if options[:expires]
153
+ options[:expires].to_i - time.to_i
154
+ else
155
+ valid_for.to_i
156
+ end.clamp(0, 604_800)
157
+
158
+ (options[:params] || {}).merge(
159
+ "X-Goog-Algorithm" => "GOOG4-RSA-SHA256",
160
+ "X-Goog-Credential" => [@credentials["client_email"], scopes].join("/"),
161
+ "X-Goog-Date" => time.strftime("%Y%m%dT%H%M%SZ"),
162
+ "X-Goog-Expires" => goog_expires,
163
+ "X-Goog-SignedHeaders" => signed_headers,
164
+ "response-content-disposition" => options[:response_content_disposition],
165
+ "response-content-type" => options[:response_content_type]
166
+ ).compact.sort
117
167
  end
118
168
 
119
169
  # raised When GcsSigner could not find service_account JSON file.
120
- class AuthError < StandardError
121
- end
170
+ class AuthError < StandardError; end
122
171
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GcsSigner
4
+ VERSION = "0.4.1"
5
+ end
metadata CHANGED
@@ -1,90 +1,108 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gcs-signer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sangwon Yi
8
- autorequire:
8
+ - Minku Lee
9
+ - Larry Kim
10
+ autorequire:
9
11
  bindir: bin
10
12
  cert_chain: []
11
- date: 2016-12-27 00:00:00.000000000 Z
13
+ date: 2021-02-03 00:00:00.000000000 Z
12
14
  dependencies:
13
15
  - !ruby/object:Gem::Dependency
14
- name: rake
16
+ name: addressable
15
17
  requirement: !ruby/object:Gem::Requirement
16
18
  requirements:
17
19
  - - "~>"
18
20
  - !ruby/object:Gem::Version
19
- version: '12.0'
20
- type: :development
21
+ version: '2.7'
22
+ type: :runtime
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
23
25
  requirements:
24
26
  - - "~>"
25
27
  - !ruby/object:Gem::Version
26
- version: '12.0'
28
+ version: '2.7'
27
29
  - !ruby/object:Gem::Dependency
28
30
  name: pry
29
31
  requirement: !ruby/object:Gem::Requirement
30
32
  requirements:
31
33
  - - "~>"
32
34
  - !ruby/object:Gem::Version
33
- 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'
34
50
  type: :development
35
51
  prerelease: false
36
52
  version_requirements: !ruby/object:Gem::Requirement
37
53
  requirements:
38
54
  - - "~>"
39
55
  - !ruby/object:Gem::Version
40
- version: '0.9'
56
+ version: '12.3'
41
57
  - !ruby/object:Gem::Dependency
42
58
  name: rubocop
43
59
  requirement: !ruby/object:Gem::Requirement
44
60
  requirements:
45
61
  - - "~>"
46
62
  - !ruby/object:Gem::Version
47
- version: 0.40.0
63
+ version: '1.0'
48
64
  type: :development
49
65
  prerelease: false
50
66
  version_requirements: !ruby/object:Gem::Requirement
51
67
  requirements:
52
68
  - - "~>"
53
69
  - !ruby/object:Gem::Version
54
- version: 0.40.0
70
+ version: '1.0'
55
71
  description: |2
56
72
  Simple signed URL generator for Google Cloud Storage.
57
- No additional gems and API requests required to generate signed URL.
73
+ No API requests required to generate signed URL.
58
74
  email:
59
75
  - sangwon@sha.kr
76
+ - minku@sha.kr
77
+ - larry@sha.kr
60
78
  executables: []
61
79
  extensions: []
62
80
  extra_rdoc_files: []
63
81
  files:
64
82
  - README.md
65
83
  - lib/gcs_signer.rb
84
+ - lib/gcs_signer/version.rb
66
85
  homepage: https://github.com/shakrmedia/gcs-signer
67
86
  licenses:
68
87
  - MIT
69
88
  metadata: {}
70
- post_install_message:
89
+ post_install_message:
71
90
  rdoc_options: []
72
91
  require_paths:
73
92
  - lib
74
93
  required_ruby_version: !ruby/object:Gem::Requirement
75
94
  requirements:
76
- - - "~>"
95
+ - - ">"
77
96
  - !ruby/object:Gem::Version
78
- version: '2.2'
97
+ version: '2.6'
79
98
  required_rubygems_version: !ruby/object:Gem::Requirement
80
99
  requirements:
81
100
  - - ">="
82
101
  - !ruby/object:Gem::Version
83
102
  version: '0'
84
103
  requirements: []
85
- rubyforge_project:
86
- rubygems_version: 2.5.2
87
- signing_key:
104
+ rubygems_version: 3.2.7
105
+ signing_key:
88
106
  specification_version: 4
89
- summary: Simple signed URL generator for Google Cloud Storage.
107
+ summary: Simple URL signer for Google Cloud Storage.
90
108
  test_files: []