faster_s3_url 1.1.0 → 1.2.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 +4 -4
- data/.github/workflows/ci.yml +1 -1
- data/CHANGELOG.md +13 -0
- data/README.md +15 -0
- data/faster_s3_url.gemspec +1 -1
- data/lib/faster_s3_url/builder.rb +63 -53
- data/lib/faster_s3_url/builder_with_new_escape.rb +296 -0
- data/lib/faster_s3_url/version.rb +1 -1
- metadata +5 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 21d5f75ad6e25913b8540b317745ccff1dacf631685963083a5fe498bb04195f
|
4
|
+
data.tar.gz: 888224050710da2bf891268a0654c0bd15e89fde0d6f7d58aae2e2f3d0a3d2ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 65bd748773211c8cd720502fd008de4dba4f1a08d81495987f702ae7e7a2a128288cb15d40df149b0166e4d7a0ec6fd94bf8aa198398b834cd8fb423829be38e
|
7
|
+
data.tar.gz: 31bb93d34192d73b787d36ff87c0223b0de430fa1fe5c712d3aa6a65d67c6bfbcccf6a23bbc45139a8e871889900ae4515d38c24028f288ff4dbdc68556aadcf
|
data/.github/workflows/ci.yml
CHANGED
data/CHANGELOG.md
CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
7
7
|
|
8
|
+
## 1.2.0
|
9
|
+
|
10
|
+
### Changed
|
11
|
+
|
12
|
+
- Now requires at least ruby 3.1
|
13
|
+
- uses CGI.escapeURIComponent for somewhat improved performance https://github.com/jrochkind/faster_s3_url/pull/8
|
14
|
+
|
15
|
+
### Added
|
16
|
+
|
17
|
+
- Add `session_token` option to `Builder` from [@BenKanouse](https://github.com/BenKanouse) https://github.com/jrochkind/faster_s3_url/pull/12
|
18
|
+
|
19
|
+
- Add 'endpoint' option to Builder, thanks @BenKanouse, https://github.com/jrochkind/faster_s3_url/pull/14
|
20
|
+
|
8
21
|
## 1.1.0
|
9
22
|
|
10
23
|
### Fixed
|
data/README.md
CHANGED
@@ -75,6 +75,20 @@ builder = FasterS3Url::Builder.new(
|
|
75
75
|
builder.presign_url(key) # performance enhanced
|
76
76
|
```
|
77
77
|
|
78
|
+
### Using AWS Security Token Service (AWS STS)?
|
79
|
+
|
80
|
+
When using AWS Security Token Service (AWS STS), AWS requires that the X-Amz-Security-Token parameter is set on presigned URLs. To accomplish this, you will need to pass the optional `session_token` parameter into the `FasterS3Url::Builder`.
|
81
|
+
|
82
|
+
```ruby
|
83
|
+
builder = FasterS3Url::Builder.new(
|
84
|
+
bucket_name: "my-bucket.example.com",
|
85
|
+
region: "us-east-1",
|
86
|
+
access_key_id: ENV['AWS_ACCESS_KEY'],
|
87
|
+
secret_access_key: ENV['AWS_SECRET_KEY'],
|
88
|
+
session_token: "session_token"
|
89
|
+
)
|
90
|
+
builder.presign_url(key) # includes required X-Amz-Security-Token
|
91
|
+
```
|
78
92
|
|
79
93
|
### Automatic AWS credentials lookup?
|
80
94
|
|
@@ -90,6 +104,7 @@ credentials = credentials.credentials if credentials.respond_to?(:credentials)
|
|
90
104
|
|
91
105
|
access_key_id = credentials.access_key_id
|
92
106
|
secret_access_key = credentials.secret_access_key
|
107
|
+
session_token = credentials.session_token # only needed when using AWS Security Token Service
|
93
108
|
region = client.config.region
|
94
109
|
```
|
95
110
|
|
data/faster_s3_url.gemspec
CHANGED
@@ -9,7 +9,7 @@ Gem::Specification.new do |spec|
|
|
9
9
|
spec.summary = %q{Generate public and presigned AWS S3 GET URLs faster}
|
10
10
|
spec.homepage = "https://github.com/jrochkind/faster_s3_url"
|
11
11
|
spec.license = "MIT"
|
12
|
-
spec.required_ruby_version = Gem::Requirement.new(">= 2.
|
12
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 3.2.0")
|
13
13
|
|
14
14
|
#spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
|
15
15
|
|
@@ -20,7 +20,8 @@ module FasterS3Url
|
|
20
20
|
|
21
21
|
MAX_CACHED_SIGNING_KEYS = 5
|
22
22
|
|
23
|
-
attr_reader :bucket_name, :region, :host, :access_key_id
|
23
|
+
attr_reader :bucket_name, :region, :host, :access_key_id, :session_token
|
24
|
+
private attr_reader :base_url, :base_path
|
24
25
|
|
25
26
|
# @option params [String] :bucket_name required
|
26
27
|
#
|
@@ -28,6 +29,8 @@ module FasterS3Url
|
|
28
29
|
#
|
29
30
|
# @option params[String] :host optional, host to use in generated URLs. If empty, will construct default AWS S3 host for bucket name and region.
|
30
31
|
#
|
32
|
+
# @option params[String] :endpoint optional. `endpoint` as in AWS SDK S3, to point at non-standard AWS locations. Mutually exclusive with `host`, can be used to point to alternate systems including local S3 clones like minio.
|
33
|
+
#
|
31
34
|
# @option params [String] :access_key_id required at present, change to allow look up from environment using standard aws sdk routines?
|
32
35
|
#
|
33
36
|
# @option params [String] :secret_access_key required at present, change to allow look up from environment using standard aws sdk routines?
|
@@ -38,24 +41,32 @@ module FasterS3Url
|
|
38
41
|
# be cached and re-used, improving performance when generating mulitple presigned urls with a single Builder by around 50%.
|
39
42
|
# NOTE WELL: This will make the Builder no longer technically concurrency-safe for sharing between multiple threads, is one
|
40
43
|
# reason it is not on by default.
|
41
|
-
def initialize(bucket_name:, region:, access_key_id:, secret_access_key:, host:nil, default_public: true, cache_signing_keys: false)
|
44
|
+
def initialize(bucket_name:, region:, access_key_id:, secret_access_key:, session_token:nil, host:nil, endpoint: nil, default_public: true, cache_signing_keys: false)
|
45
|
+
if endpoint && host
|
46
|
+
raise ArgumentError.new("`endpoint` and `host` are mutually exclusive, you can only provide one. You provided endpoint: #{endpoint.inspect} and host: #{host.inspect}")
|
47
|
+
end
|
48
|
+
|
42
49
|
@bucket_name = bucket_name
|
43
50
|
@region = region
|
44
|
-
|
51
|
+
|
52
|
+
parsed_uri = parsed_base_uri(bucket_name: bucket_name, host: host, endpoint: endpoint)
|
53
|
+
@base_url = URI.join(parsed_uri, "/").to_s.chomp("/") # without path
|
54
|
+
@base_path = parsed_uri.path # path component of base url, usually empty
|
55
|
+
@host = parsed_uri.host
|
56
|
+
@canonical_headers = "host:#{parsed_uri.port == parsed_uri.default_port ? @host : "#{parsed_uri.host}:#{parsed_uri.port}"}\n"
|
57
|
+
|
45
58
|
@default_public = default_public
|
46
59
|
@access_key_id = access_key_id
|
47
60
|
@secret_access_key = secret_access_key
|
48
61
|
@cache_signing_keys = cache_signing_keys
|
62
|
+
@session_token = session_token
|
49
63
|
if @cache_signing_keys
|
50
64
|
@signing_key_cache = {}
|
51
65
|
end
|
52
|
-
|
53
|
-
|
54
|
-
@canonical_headers = "host:#{@host}\n"
|
55
66
|
end
|
56
67
|
|
57
68
|
def public_url(key)
|
58
|
-
"
|
69
|
+
"#{self.base_url}#{self.base_path}/#{uri_escape_key(key)}"
|
59
70
|
end
|
60
71
|
|
61
72
|
# Generates a presigned GET URL for a specified S3 object key.
|
@@ -95,7 +106,7 @@ module FasterS3Url
|
|
95
106
|
version_id: nil)
|
96
107
|
validate_expires_in(expires_in)
|
97
108
|
|
98
|
-
canonical_uri = "/" + uri_escape_key(key)
|
109
|
+
canonical_uri = self.base_path + "/" + uri_escape_key(key)
|
99
110
|
|
100
111
|
now = time ? time.dup.utc : Time.now.utc # Uh Time#utc is mutating, not nice to do to an argument!
|
101
112
|
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
@@ -103,35 +114,25 @@ module FasterS3Url
|
|
103
114
|
|
104
115
|
credential_scope = datestamp + '/' + region + '/' + SERVICE + '/' + 'aws4_request'
|
105
116
|
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
117
|
+
# These have to be sorted, but sort is case-sensitive, and we have a fixed
|
118
|
+
# list of headers we know might be here... turns out they are already sorted?
|
119
|
+
canonical_query_params = {
|
120
|
+
"X-Amz-Algorithm": ALGORITHM,
|
121
|
+
"X-Amz-Credential": uri_escape(@access_key_id + "/" + credential_scope),
|
122
|
+
"X-Amz-Date": amz_date,
|
123
|
+
"X-Amz-Expires": expires_in.to_s,
|
124
|
+
"X-Amz-Security-Token": uri_escape(session_token),
|
125
|
+
"X-Amz-SignedHeaders": SIGNED_HEADERS,
|
126
|
+
"response-cache-control": uri_escape(response_cache_control),
|
127
|
+
"response-content-disposition": uri_escape(response_content_disposition),
|
128
|
+
"response-content-encoding": uri_escape(response_content_encoding),
|
129
|
+
"response-content-language": uri_escape(response_content_language),
|
130
|
+
"response-content-type": uri_escape(response_content_type),
|
131
|
+
"response-expires": uri_escape(convert_for_timestamp_shape(response_expires)),
|
132
|
+
"versionId": uri_escape(version_id)
|
122
133
|
}.compact
|
123
134
|
|
124
|
-
|
125
|
-
if extra_params.size > 0
|
126
|
-
# These have to be sorted, but sort is case-sensitive, and we have a fixed
|
127
|
-
# list of headers we know might be here... turns out they are already sorted?
|
128
|
-
extra_param_parts = extra_params.collect {|k, v| "#{k}=#{uri_escape v}" }.join("&")
|
129
|
-
canonical_query_string_parts << extra_param_parts
|
130
|
-
end
|
131
|
-
|
132
|
-
canonical_query_string = canonical_query_string_parts.join("&")
|
133
|
-
|
134
|
-
|
135
|
+
canonical_query_string = canonical_query_params.collect {|k, v| "#{k}=#{v}" }.join("&")
|
135
136
|
|
136
137
|
canonical_request = ["GET",
|
137
138
|
canonical_uri,
|
@@ -151,7 +152,7 @@ module FasterS3Url
|
|
151
152
|
signing_key = retrieve_signing_key(datestamp)
|
152
153
|
signature = OpenSSL::HMAC.hexdigest("SHA256", signing_key, string_to_sign)
|
153
154
|
|
154
|
-
return "
|
155
|
+
return "#{base_url}#{canonical_uri}?#{canonical_query_string}&X-Amz-Signature=#{signature}"
|
155
156
|
end
|
156
157
|
|
157
158
|
# just a convenience method that can call public_url or presigned_url based on flag
|
@@ -206,42 +207,51 @@ module FasterS3Url
|
|
206
207
|
end
|
207
208
|
end
|
208
209
|
|
209
|
-
|
210
|
-
#
|
211
|
-
#
|
212
|
-
# else in pure C that has the semantics we want, but does not seem available.
|
210
|
+
|
211
|
+
# CGI.escapeURIComponent has correct semantics for what AWS wants, and is
|
212
|
+
# implemented in C, so pretty fast.
|
213
213
|
def uri_escape(string)
|
214
214
|
if string.nil?
|
215
215
|
nil
|
216
216
|
else
|
217
|
-
CGI.
|
218
|
-
# there is a clever way to do this in one gsub, but doesn't necessarily help
|
219
|
-
# memory allocations or performance
|
220
|
-
s.gsub!('+'.freeze, '%20'.freeze)
|
221
|
-
s.gsub!('%7E'.freeze, '~'.freeze)
|
222
|
-
end
|
217
|
+
CGI.escapeURIComponent(string.encode('UTF-8'))
|
223
218
|
end
|
224
219
|
end
|
225
220
|
|
226
221
|
# like uri_escape but does NOT escape `/`, leaves it alone. The appropriate
|
227
222
|
# escaping algorithm for an S3 key turning into a URL.
|
228
223
|
#
|
229
|
-
#
|
230
|
-
#
|
224
|
+
# Using CGI.escapeURIComponent with a gsub is faster than anything else
|
225
|
+
# we found to get this semantics.
|
231
226
|
def uri_escape_key(string)
|
232
227
|
if string.nil?
|
233
228
|
nil
|
234
229
|
else
|
235
|
-
CGI.
|
236
|
-
# there is a clever way to do this in one gsub, but doesn't necessarily help
|
237
|
-
# memory allocations or performance
|
238
|
-
s.gsub!('+'.freeze, '%20'.freeze)
|
239
|
-
s.gsub!('%7E'.freeze, '~'.freeze)
|
230
|
+
CGI.escapeURIComponent(string.encode('UTF-8')).tap do |s|
|
240
231
|
s.gsub!('%2F'.freeze, '/'.freeze)
|
241
232
|
end
|
242
233
|
end
|
243
234
|
end
|
244
235
|
|
236
|
+
# Handle endpoint, modifying host or path with bucketname, and setting
|
237
|
+
# host.
|
238
|
+
#
|
239
|
+
# if none set, set default host.
|
240
|
+
#
|
241
|
+
# Set base_url correct for host or endpoint.
|
242
|
+
def parsed_base_uri(bucket_name:, host:, endpoint:)
|
243
|
+
return URI.parse("https://#{host}") if host
|
244
|
+
return URI.parse("https://#{default_host(bucket_name)}") if endpoint.nil?
|
245
|
+
|
246
|
+
parsed = URI.parse(endpoint)
|
247
|
+
if parsed.host =~ /\A\d+\.\d+\.\d+\.\d+\Z/
|
248
|
+
parsed.path = "/#{bucket_name}"
|
249
|
+
else
|
250
|
+
parsed.host = "#{bucket_name}.#{parsed.host}"
|
251
|
+
end
|
252
|
+
parsed
|
253
|
+
end
|
254
|
+
|
245
255
|
def default_host(bucket_name)
|
246
256
|
if region == "us-east-1"
|
247
257
|
# use legacy one without region, as S3 seems to
|
@@ -0,0 +1,296 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'cgi'
|
4
|
+
|
5
|
+
module FasterS3Url
|
6
|
+
# Signing algorithm based on Amazon docs at https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html ,
|
7
|
+
# as well as some interactive code reading of Aws::Sigv4::Signer
|
8
|
+
# https://github.com/aws/aws-sdk-ruby/blob/6114bc9692039ac75c8292c66472dacd14fa6f9a/gems/aws-sigv4/lib/aws-sigv4/signer.rb
|
9
|
+
# as used by Aws::S3::Presigner https://github.com/aws/aws-sdk-ruby/blob/6114bc9692039ac75c8292c66472dacd14fa6f9a/gems/aws-sdk-s3/lib/aws-sdk-s3/presigner.rb
|
10
|
+
class BuilderWithNewEscape
|
11
|
+
FIFTEEN_MINUTES = 60 * 15
|
12
|
+
ONE_WEEK = 60 * 60 * 24 * 7
|
13
|
+
|
14
|
+
SIGNED_HEADERS = "host".freeze
|
15
|
+
METHOD = "GET".freeze
|
16
|
+
ALGORITHM = "AWS4-HMAC-SHA256".freeze
|
17
|
+
SERVICE = "s3".freeze
|
18
|
+
|
19
|
+
DEFAULT_EXPIRES_IN = FIFTEEN_MINUTES # 15 minutes, seems to be AWS SDK default
|
20
|
+
|
21
|
+
MAX_CACHED_SIGNING_KEYS = 5
|
22
|
+
|
23
|
+
attr_reader :bucket_name, :region, :host, :access_key_id
|
24
|
+
|
25
|
+
# @option params [String] :bucket_name required
|
26
|
+
#
|
27
|
+
# @option params [String] :region eg "us-east-1", required
|
28
|
+
#
|
29
|
+
# @option params[String] :host optional, host to use in generated URLs. If empty, will construct default AWS S3 host for bucket name and region.
|
30
|
+
#
|
31
|
+
# @option params [String] :access_key_id required at present, change to allow look up from environment using standard aws sdk routines?
|
32
|
+
#
|
33
|
+
# @option params [String] :secret_access_key required at present, change to allow look up from environment using standard aws sdk routines?
|
34
|
+
#
|
35
|
+
# @option params [boolean] :default_public (true) default value of `public` when instance method #url is called.
|
36
|
+
#
|
37
|
+
# @option params [boolean] :cache_signing_keys (false). If set to true, up to five signing keys used for presigned URLs will
|
38
|
+
# be cached and re-used, improving performance when generating mulitple presigned urls with a single Builder by around 50%.
|
39
|
+
# NOTE WELL: This will make the Builder no longer technically concurrency-safe for sharing between multiple threads, is one
|
40
|
+
# reason it is not on by default.
|
41
|
+
def initialize(bucket_name:, region:, access_key_id:, secret_access_key:, host:nil, default_public: true, cache_signing_keys: false)
|
42
|
+
@bucket_name = bucket_name
|
43
|
+
@region = region
|
44
|
+
@host = host || default_host(bucket_name)
|
45
|
+
@default_public = default_public
|
46
|
+
@access_key_id = access_key_id
|
47
|
+
@secret_access_key = secret_access_key
|
48
|
+
@cache_signing_keys = cache_signing_keys
|
49
|
+
if @cache_signing_keys
|
50
|
+
@signing_key_cache = {}
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
@canonical_headers = "host:#{@host}\n"
|
55
|
+
end
|
56
|
+
|
57
|
+
def public_url(key)
|
58
|
+
"https://#{self.host}/#{uri_escape_key(key)}"
|
59
|
+
end
|
60
|
+
|
61
|
+
# Generates a presigned GET URL for a specified S3 object key.
|
62
|
+
#
|
63
|
+
# @param [String] key The S3 key to create a URL pointing to.
|
64
|
+
#
|
65
|
+
# @option params [Time] :time (Time.now) The starting time for when the
|
66
|
+
# presigned url becomes active.
|
67
|
+
#
|
68
|
+
# @option params [String] :response_cache_control
|
69
|
+
# Adds a `response-cache-control` query param to set the `Cache-Control` header of the subsequent response from S3.
|
70
|
+
#
|
71
|
+
# @option params [String] :response_content_disposition
|
72
|
+
# Adds a `response-content-disposition` query param to set the `Content-Disposition` header of the subsequent response from S3
|
73
|
+
#
|
74
|
+
# @option params [String] :response_content_encoding
|
75
|
+
# Adds a `response-content-encoding` query param to set `Content-Encoding` header of the subsequent response from S3
|
76
|
+
#
|
77
|
+
# @option params [String] :response_content_language
|
78
|
+
# Adds a `response-content-language` query param to sets the `Content-Language` header of the subsequent response from S3
|
79
|
+
#
|
80
|
+
# @option params [String] :response_content_type
|
81
|
+
# Adds a `response-content-type` query param to sets the `Content-Type` header of the subsequent response from S3
|
82
|
+
#
|
83
|
+
# @option params [String] :response_expires
|
84
|
+
# Adds a `response-expires` query param to sets the `Expires` header of of the subsequent response from S3
|
85
|
+
#
|
86
|
+
# @option params [String] :version_id
|
87
|
+
# Adds a `versionId` query param to reference a specific version of the object from S3.
|
88
|
+
def presigned_url(key, time: nil, expires_in: DEFAULT_EXPIRES_IN,
|
89
|
+
response_cache_control: nil,
|
90
|
+
response_content_disposition: nil,
|
91
|
+
response_content_encoding: nil,
|
92
|
+
response_content_language: nil,
|
93
|
+
response_content_type: nil,
|
94
|
+
response_expires: nil,
|
95
|
+
version_id: nil)
|
96
|
+
validate_expires_in(expires_in)
|
97
|
+
|
98
|
+
canonical_uri = "/" + uri_escape_key(key)
|
99
|
+
|
100
|
+
now = time ? time.dup.utc : Time.now.utc # Uh Time#utc is mutating, not nice to do to an argument!
|
101
|
+
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
102
|
+
datestamp = now.strftime("%Y%m%d")
|
103
|
+
|
104
|
+
credential_scope = datestamp + '/' + region + '/' + SERVICE + '/' + 'aws4_request'
|
105
|
+
|
106
|
+
canonical_query_string_parts = [
|
107
|
+
"X-Amz-Algorithm=#{ALGORITHM}",
|
108
|
+
"X-Amz-Credential=" + uri_escape(@access_key_id + "/" + credential_scope),
|
109
|
+
"X-Amz-Date=" + amz_date,
|
110
|
+
"X-Amz-Expires=" + expires_in.to_s,
|
111
|
+
"X-Amz-SignedHeaders=" + SIGNED_HEADERS,
|
112
|
+
]
|
113
|
+
|
114
|
+
extra_params = {
|
115
|
+
:"response-cache-control" => response_cache_control,
|
116
|
+
:"response-content-disposition" => response_content_disposition,
|
117
|
+
:"response-content-encoding" => response_content_encoding,
|
118
|
+
:"response-content-language" => response_content_language,
|
119
|
+
:"response-content-type" => response_content_type,
|
120
|
+
:"response-expires" => convert_for_timestamp_shape(response_expires),
|
121
|
+
:"versionId" => version_id
|
122
|
+
}.compact
|
123
|
+
|
124
|
+
|
125
|
+
if extra_params.size > 0
|
126
|
+
# These have to be sorted, but sort is case-sensitive, and we have a fixed
|
127
|
+
# list of headers we know might be here... turns out they are already sorted?
|
128
|
+
extra_param_parts = extra_params.collect {|k, v| "#{k}=#{uri_escape v}" }.join("&")
|
129
|
+
canonical_query_string_parts << extra_param_parts
|
130
|
+
end
|
131
|
+
|
132
|
+
canonical_query_string = canonical_query_string_parts.join("&")
|
133
|
+
|
134
|
+
|
135
|
+
|
136
|
+
canonical_request = ["GET",
|
137
|
+
canonical_uri,
|
138
|
+
canonical_query_string,
|
139
|
+
@canonical_headers,
|
140
|
+
SIGNED_HEADERS,
|
141
|
+
'UNSIGNED-PAYLOAD'
|
142
|
+
].join("\n")
|
143
|
+
|
144
|
+
string_to_sign = [
|
145
|
+
ALGORITHM,
|
146
|
+
amz_date,
|
147
|
+
credential_scope,
|
148
|
+
Digest::SHA256.hexdigest(canonical_request)
|
149
|
+
].join("\n")
|
150
|
+
|
151
|
+
signing_key = retrieve_signing_key(datestamp)
|
152
|
+
signature = OpenSSL::HMAC.hexdigest("SHA256", signing_key, string_to_sign)
|
153
|
+
|
154
|
+
return "https://" + self.host + canonical_uri + "?" + canonical_query_string + "&X-Amz-Signature=" + signature
|
155
|
+
end
|
156
|
+
|
157
|
+
# just a convenience method that can call public_url or presigned_url based on flag
|
158
|
+
#
|
159
|
+
# signer.url(object_key, public: true)
|
160
|
+
# #=> forwards to signer.public_url(object_key)
|
161
|
+
#
|
162
|
+
# signer.url(object_key, public: false, response_content_type: "image/jpeg")
|
163
|
+
# #=> forwards to signer.presigned_url(object_key, response_content_type: "image/jpeg")
|
164
|
+
#
|
165
|
+
# Options (sucn as response_content_type) that are not applicable to #public_url
|
166
|
+
# are ignored in public mode.
|
167
|
+
#
|
168
|
+
# The default value of `public` can be set by initializer arg `default_public`, which
|
169
|
+
# is itself default true.
|
170
|
+
#
|
171
|
+
# builder = FasterS3Url::Builder.new(..., default_public: false)
|
172
|
+
# builder.url(object_key) # will call #presigned_url
|
173
|
+
def url(key, public: @default_public, **options)
|
174
|
+
if public
|
175
|
+
public_url(key)
|
176
|
+
else
|
177
|
+
presigned_url(key, **options)
|
178
|
+
end
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
private
|
183
|
+
|
184
|
+
def make_signing_key(datestamp)
|
185
|
+
aws_get_signature_key(@secret_access_key, datestamp, @region, SERVICE)
|
186
|
+
end
|
187
|
+
|
188
|
+
# If caching of signing keys is turned on, use and cache signing key, while
|
189
|
+
# making sure not to cache more than MAX_CACHED_SIGNING_KEYS
|
190
|
+
#
|
191
|
+
# Otherwise if caching of signing keys is not turned on, just generate and return
|
192
|
+
# a signing key.
|
193
|
+
def retrieve_signing_key(datestamp)
|
194
|
+
if @cache_signing_keys
|
195
|
+
if value = @signing_key_cache[datestamp]
|
196
|
+
value
|
197
|
+
else
|
198
|
+
value = @signing_key_cache[datestamp] = make_signing_key(datestamp)
|
199
|
+
while @signing_key_cache.size > MAX_CACHED_SIGNING_KEYS
|
200
|
+
@signing_key_cache.delete(@signing_key_cache.keys.first)
|
201
|
+
end
|
202
|
+
value
|
203
|
+
end
|
204
|
+
else
|
205
|
+
make_signing_key(datestamp)
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
209
|
+
# Becaues CGI.escape in MRI is written in C, this really does seem
|
210
|
+
# to be the fastest way to get the semantics we want, starting with
|
211
|
+
# CGI.escape and doing extra gsubs. Alternative would be using something
|
212
|
+
# else in pure C that has the semantics we want, but does not seem available.
|
213
|
+
def uri_escape(string)
|
214
|
+
if string.nil?
|
215
|
+
nil
|
216
|
+
else
|
217
|
+
CGI.escapeURIComponent(string.encode('UTF-8'))
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
# like uri_escape but does NOT escape `/`, leaves it alone. The appropriate
|
222
|
+
# escaping algorithm for an S3 key turning into a URL.
|
223
|
+
#
|
224
|
+
# Faster to un-DRY the code with uri_escape. Yes, faster to actually just gsub
|
225
|
+
# %2F back to /
|
226
|
+
def uri_escape_key(string)
|
227
|
+
if string.nil?
|
228
|
+
nil
|
229
|
+
else
|
230
|
+
CGI.escapeURIComponent(string.encode('UTF-8')).tap do |s|
|
231
|
+
# there is a clever way to do this in one gsub, but doesn't necessarily help
|
232
|
+
# memory allocations or performance
|
233
|
+
#s.gsub!('+'.freeze, '%20'.freeze)
|
234
|
+
#s.gsub!('%7E'.freeze, '~'.freeze)
|
235
|
+
s.gsub!('%2F'.freeze, '/'.freeze)
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
def default_host(bucket_name)
|
241
|
+
if region == "us-east-1"
|
242
|
+
# use legacy one without region, as S3 seems to
|
243
|
+
"#{bucket_name}.s3.amazonaws.com".freeze
|
244
|
+
else
|
245
|
+
"#{bucket_name}.s3.#{region}.amazonaws.com".freeze
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
# `def get_signature_key` `from python example at https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
|
250
|
+
def aws_get_signature_key(key, date_stamp, region_name, service_name)
|
251
|
+
k_date = aws_sign("AWS4" + key, date_stamp)
|
252
|
+
k_region = aws_sign(k_date, region_name)
|
253
|
+
k_service = aws_sign(k_region, service_name)
|
254
|
+
aws_sign(k_service, "aws4_request")
|
255
|
+
end
|
256
|
+
|
257
|
+
# `def sign` from python example at https://docs.aws.amazon.com/general/latest/gr/sigv4-signed-request-examples.html
|
258
|
+
def aws_sign(key, data)
|
259
|
+
OpenSSL::HMAC.digest("SHA256", key, data)
|
260
|
+
end
|
261
|
+
|
262
|
+
def validate_expires_in(expires_in)
|
263
|
+
if expires_in > ONE_WEEK
|
264
|
+
raise ArgumentError.new("expires_in value of #{expires_in} exceeds one-week (#{ONE_WEEK}) maximum.")
|
265
|
+
elsif expires_in <= 0
|
266
|
+
raise ArgumentError.new("expires_in value of #{expires_in} cannot be 0 or less.")
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Crazy kind of reverse engineered from aws-sdk-ruby,
|
271
|
+
# for compatible handling of Expires header.
|
272
|
+
#
|
273
|
+
# Recent versions of ruby AWS SDK use "httpdate" format here, as a result of
|
274
|
+
# an issue we filed: https://github.com/aws/aws-sdk-ruby/issues/2415
|
275
|
+
#
|
276
|
+
# We match what recent AWS SDK does.
|
277
|
+
#
|
278
|
+
# Note while the AWS SDK source says "rfc 822", it's ruby #httpdate that matches
|
279
|
+
# rather than ruby #rfc822 (timezone should be `GMT` to match AWS SDK, not `-0000`)
|
280
|
+
def convert_for_timestamp_shape(arg)
|
281
|
+
return nil if arg.nil?
|
282
|
+
|
283
|
+
time_value = case arg
|
284
|
+
when Time
|
285
|
+
arg
|
286
|
+
when Date, DateTime
|
287
|
+
arg.to_time
|
288
|
+
when Integer, Float
|
289
|
+
Time.at(arg)
|
290
|
+
else
|
291
|
+
Time.parse(arg.to_s)
|
292
|
+
end
|
293
|
+
time_value.utc.httpdate
|
294
|
+
end
|
295
|
+
end
|
296
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: faster_s3_url
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Jonathan Rochkind
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2024-10-30 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: aws-sdk-s3
|
@@ -115,6 +115,7 @@ files:
|
|
115
115
|
- faster_s3_url.gemspec
|
116
116
|
- lib/faster_s3_url.rb
|
117
117
|
- lib/faster_s3_url/builder.rb
|
118
|
+
- lib/faster_s3_url/builder_with_new_escape.rb
|
118
119
|
- lib/faster_s3_url/shrine/storage.rb
|
119
120
|
- lib/faster_s3_url/version.rb
|
120
121
|
- perf/presigned_bench.rb
|
@@ -133,14 +134,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
133
134
|
requirements:
|
134
135
|
- - ">="
|
135
136
|
- !ruby/object:Gem::Version
|
136
|
-
version: 2.
|
137
|
+
version: 3.2.0
|
137
138
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
138
139
|
requirements:
|
139
140
|
- - ">="
|
140
141
|
- !ruby/object:Gem::Version
|
141
142
|
version: '0'
|
142
143
|
requirements: []
|
143
|
-
rubygems_version: 3.
|
144
|
+
rubygems_version: 3.5.9
|
144
145
|
signing_key:
|
145
146
|
specification_version: 4
|
146
147
|
summary: Generate public and presigned AWS S3 GET URLs faster
|