ve-tos-ruby-sdk 0.1.0 → 0.1.2
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/lib/tos/bucket.rb +9 -25
- data/lib/tos/client.rb +4 -8
- data/lib/tos/signer.rb +56 -117
- data/lib/tos/version.rb +1 -1
- data/lib/tos.rb +1 -1
- data/ve-tos-ruby-sdk.gemspec +0 -3
- metadata +1 -15
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 95bc0ad985868c8d32fa03105d98e288a7b0deb98d2ad148a359019a6da12a28
|
|
4
|
+
data.tar.gz: b40de0e450ed402f29090100918f5ebae9494f981f1cd71c2ec1824d47073bce
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 62f7c51e8891584e346be7bcccb6ffe4a25032f016d7abfb49d19e21d89c4addcb5195f0dc83e3b9d253ca2bc00ab87cbad3e0fbb0eb3a1f7add2ea0c34fc173
|
|
7
|
+
data.tar.gz: f8d743fffb0e46db0588911a6223170dbf9c664abbf4c978bb2cab927ecf4f8778d336594c93c9c0e9d890a01d34f5ce626e2671e853f76ecc39eb9ba425d4a4
|
data/lib/tos/bucket.rb
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "rexml/document"
|
|
4
|
-
|
|
5
3
|
module TOS
|
|
6
4
|
# All operations are bucket-scoped: get a bucket via `client.bucket(name)`
|
|
7
5
|
# then call put_object / get_object / etc. on it.
|
|
@@ -66,7 +64,10 @@ module TOS
|
|
|
66
64
|
keys = Array(keys).reject { |k| k.to_s.empty? }
|
|
67
65
|
return [] if keys.empty?
|
|
68
66
|
|
|
69
|
-
body =
|
|
67
|
+
body = JSON.generate(
|
|
68
|
+
Quiet: quiet,
|
|
69
|
+
Objects: keys.map { |k| { Key: k.to_s } },
|
|
70
|
+
)
|
|
70
71
|
content_md5 = OpenSSL::Digest::MD5.base64digest(body)
|
|
71
72
|
|
|
72
73
|
client.request(
|
|
@@ -74,7 +75,7 @@ module TOS
|
|
|
74
75
|
host: host,
|
|
75
76
|
path: "/",
|
|
76
77
|
query: { "delete" => "" },
|
|
77
|
-
headers: { "Content-Type" => "application/
|
|
78
|
+
headers: { "Content-Type" => "application/json", "Content-MD5" => content_md5 },
|
|
78
79
|
body: body,
|
|
79
80
|
)
|
|
80
81
|
end
|
|
@@ -96,32 +97,15 @@ module TOS
|
|
|
96
97
|
end
|
|
97
98
|
|
|
98
99
|
def parse_list_response(res)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
keys = root.get_elements("Contents/Key").map(&:text)
|
|
102
|
-
truncated = root.elements["IsTruncated"]&.text == "true"
|
|
103
|
-
next_token = root.elements["NextContinuationToken"]&.text
|
|
100
|
+
data = JSON.parse(res.body)
|
|
101
|
+
keys = (data["Contents"] || []).map { |obj| obj["Key"] }
|
|
104
102
|
|
|
105
103
|
{
|
|
106
104
|
keys: keys,
|
|
107
|
-
is_truncated:
|
|
108
|
-
next_continuation_token:
|
|
105
|
+
is_truncated: data["IsTruncated"] == true,
|
|
106
|
+
next_continuation_token: data["NextContinuationToken"],
|
|
109
107
|
raw: res,
|
|
110
108
|
}
|
|
111
109
|
end
|
|
112
|
-
|
|
113
|
-
def build_delete_payload(keys, quiet)
|
|
114
|
-
doc = REXML::Document.new
|
|
115
|
-
doc.add_element("Delete").tap do |delete|
|
|
116
|
-
delete.add_element("Quiet").text = quiet ? "true" : "false"
|
|
117
|
-
keys.each do |key|
|
|
118
|
-
obj = delete.add_element("Object")
|
|
119
|
-
obj.add_element("Key").text = key
|
|
120
|
-
end
|
|
121
|
-
end
|
|
122
|
-
out = +""
|
|
123
|
-
doc.write(out)
|
|
124
|
-
out
|
|
125
|
-
end
|
|
126
110
|
end
|
|
127
111
|
end
|
data/lib/tos/client.rb
CHANGED
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require "net/http"
|
|
4
4
|
require "uri"
|
|
5
|
-
require "rexml/document"
|
|
6
5
|
|
|
7
6
|
module TOS
|
|
8
7
|
# Low-level HTTP client. Holds credentials, region, endpoint resolution, and
|
|
@@ -53,7 +52,7 @@ module TOS
|
|
|
53
52
|
|
|
54
53
|
signed = signer.sign_headers(
|
|
55
54
|
method: method, path: path, query: query,
|
|
56
|
-
headers: headers, body: body
|
|
55
|
+
headers: headers.merge("Host" => host), body: body
|
|
57
56
|
)
|
|
58
57
|
signed.each { |k, v| req[k] = v }
|
|
59
58
|
# Signer-only headers that aren't in the user's `headers` hash.
|
|
@@ -136,12 +135,9 @@ module TOS
|
|
|
136
135
|
request_id = res["x-tos-request-id"]
|
|
137
136
|
return [body, nil, request_id] if body.empty?
|
|
138
137
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
request_id ||= doc.root&.elements&.[]("RequestId")&.text
|
|
143
|
-
[message, code, request_id]
|
|
144
|
-
rescue REXML::ParseException
|
|
138
|
+
data = JSON.parse(body)
|
|
139
|
+
[data["Message"] || body, data["Code"], request_id || data["RequestId"]]
|
|
140
|
+
rescue JSON::ParserError
|
|
145
141
|
[body, nil, request_id]
|
|
146
142
|
end
|
|
147
143
|
|
data/lib/tos/signer.rb
CHANGED
|
@@ -1,17 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require "openssl"
|
|
4
|
-
require "
|
|
5
|
-
require "uri"
|
|
6
|
-
require "cgi"
|
|
4
|
+
require "erb"
|
|
7
5
|
|
|
8
6
|
module TOS
|
|
9
|
-
# Signs HTTP requests for the TOS object service using
|
|
10
|
-
#
|
|
11
|
-
# service name and a slightly different signed-header policy: by default
|
|
12
|
-
# only `content-type` and any `x-tos-*` headers are signed).
|
|
13
|
-
#
|
|
14
|
-
# Reference: https://github.com/volcengine/ve-tos-golang-sdk/blob/master/tos/sign_v4.go
|
|
7
|
+
# Signs HTTP requests for the TOS object service using TOS4-HMAC-SHA256
|
|
8
|
+
# (AWS SigV4 with the "tos" service name)
|
|
15
9
|
class Signer
|
|
16
10
|
ALGORITHM = "TOS4-HMAC-SHA256"
|
|
17
11
|
SERVICE = "tos"
|
|
@@ -32,153 +26,98 @@ module TOS
|
|
|
32
26
|
# Sign a request and return a Hash of headers to add (Authorization,
|
|
33
27
|
# X-Tos-Date, X-Tos-Content-Sha256, plus X-Tos-Security-Token when STS
|
|
34
28
|
# credentials are used).
|
|
35
|
-
#
|
|
36
|
-
# @param method [String] HTTP method, uppercased (e.g. "PUT")
|
|
37
|
-
# @param path [String] URI path starting with "/" (e.g. "/bucket/key")
|
|
38
|
-
# @param query [Hash{String=>String,Array<String>}] query params
|
|
39
|
-
# @param headers [Hash] existing request headers (case-insensitive keys)
|
|
40
|
-
# @param body [String, nil] request body — may be nil for GET/HEAD/DELETE
|
|
41
|
-
# @param now [Time] override for testing
|
|
42
29
|
def sign_headers(method:, path:, query: {}, headers: {}, body: nil, now: Time.now.utc)
|
|
43
30
|
datetime = now.strftime("%Y%m%dT%H%M%SZ")
|
|
44
|
-
date = datetime[0, 8]
|
|
45
31
|
content_sha = body ? sha256_hex(body) : EMPTY_SHA256
|
|
46
32
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
33
|
+
out = headers.dup
|
|
34
|
+
out["X-Tos-Date"] = datetime
|
|
35
|
+
out["X-Tos-Content-Sha256"] = content_sha
|
|
36
|
+
out["X-Tos-Security-Token"] = @credentials.security_token if @credentials.security_token
|
|
51
37
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
method: method.to_s.upcase,
|
|
56
|
-
path: path,
|
|
57
|
-
query: query,
|
|
58
|
-
signed_headers: signed_headers,
|
|
59
|
-
content_sha: content_sha
|
|
60
|
-
)
|
|
61
|
-
|
|
62
|
-
credential_scope = "#{date}/#{@region}/#{SERVICE}/#{REQUEST}"
|
|
63
|
-
string_to_sign = [ALGORITHM, datetime, credential_scope, sha256_hex(canonical_request)].join("\n")
|
|
64
|
-
|
|
65
|
-
signing_key = derive_signing_key(date)
|
|
66
|
-
signature = hmac_hex(signing_key, string_to_sign)
|
|
67
|
-
|
|
68
|
-
authorization = build_authorization_header(signed_headers, credential_scope, signature)
|
|
69
|
-
working_headers["Authorization"] = authorization
|
|
70
|
-
|
|
71
|
-
working_headers
|
|
38
|
+
signed = signed_headers(out)
|
|
39
|
+
out["Authorization"] = authorization(method, path, query, signed, content_sha, datetime)
|
|
40
|
+
out
|
|
72
41
|
end
|
|
73
42
|
|
|
74
43
|
# Build a presigned URL with all signing material in the query string.
|
|
75
|
-
# Returns a fully-qualified URL.
|
|
76
44
|
def presign(method:, host:, path:, query: {}, expires_in: 3600, now: Time.now.utc)
|
|
77
45
|
datetime = now.strftime("%Y%m%dT%H%M%SZ")
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
query_with_sig["X-Tos-Date"] = datetime
|
|
85
|
-
query_with_sig["X-Tos-Expires"] = expires_in.to_s
|
|
86
|
-
query_with_sig["X-Tos-SignedHeaders"] = ""
|
|
87
|
-
query_with_sig["X-Tos-Security-Token"] = @credentials.security_token if @credentials.security_token
|
|
88
|
-
|
|
89
|
-
canonical_request = build_canonical_request(
|
|
90
|
-
method: method.to_s.upcase,
|
|
91
|
-
path: path,
|
|
92
|
-
query: query_with_sig,
|
|
93
|
-
signed_headers: [],
|
|
94
|
-
content_sha: UNSIGNED_PAYLOAD
|
|
46
|
+
params = query.merge(
|
|
47
|
+
"X-Tos-Algorithm" => ALGORITHM,
|
|
48
|
+
"X-Tos-Credential" => "#{@credentials.access_key_id}/#{credential_scope(datetime[0, 8])}",
|
|
49
|
+
"X-Tos-Date" => datetime,
|
|
50
|
+
"X-Tos-Expires" => expires_in.to_s,
|
|
51
|
+
"X-Tos-SignedHeaders" => "",
|
|
95
52
|
)
|
|
53
|
+
params["X-Tos-Security-Token"] = @credentials.security_token if @credentials.security_token
|
|
96
54
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
query_with_sig["X-Tos-Signature"] = hmac_hex(signing_key, string_to_sign)
|
|
100
|
-
|
|
101
|
-
"https://#{host}#{path}?#{encode_query(query_with_sig)}"
|
|
55
|
+
params["X-Tos-Signature"] = sign(method, path, params, [], UNSIGNED_PAYLOAD, datetime)
|
|
56
|
+
"https://#{host}#{path}?#{encode_query(params)}"
|
|
102
57
|
end
|
|
103
58
|
|
|
104
59
|
private
|
|
105
60
|
|
|
106
|
-
# Default policy
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
61
|
+
# Default policy: sign `host`, `content-type`, and any `x-tos-*` header.
|
|
62
|
+
def signed_headers(headers)
|
|
63
|
+
headers
|
|
64
|
+
.map { |k, v| [k.to_s.downcase, normalize_value(v)] }
|
|
65
|
+
.select { |k, _| k == "host" || k == "content-type" || k.start_with?("x-tos-") }
|
|
66
|
+
.sort
|
|
67
|
+
end
|
|
112
68
|
|
|
113
|
-
|
|
114
|
-
|
|
69
|
+
def authorization(method, path, query, signed, content_sha, datetime)
|
|
70
|
+
header_list = signed.map(&:first).join(";")
|
|
71
|
+
signature = sign(method, path, query, signed, content_sha, datetime)
|
|
72
|
+
"#{ALGORITHM} Credential=#{@credentials.access_key_id}/#{credential_scope(datetime[0, 8])}," \
|
|
73
|
+
"SignedHeaders=#{header_list},Signature=#{signature}"
|
|
115
74
|
end
|
|
116
75
|
|
|
117
|
-
def
|
|
118
|
-
|
|
119
|
-
|
|
76
|
+
def sign(method, path, query, signed, content_sha, datetime)
|
|
77
|
+
canonical = canonical_request(method, path, query, signed, content_sha)
|
|
78
|
+
string_to_sign = [ALGORITHM, datetime, credential_scope(datetime[0, 8]), sha256_hex(canonical)].join("\n")
|
|
79
|
+
hmac_hex(signing_key(datetime[0, 8]), string_to_sign)
|
|
80
|
+
end
|
|
120
81
|
|
|
82
|
+
def canonical_request(method, path, query, signed, content_sha)
|
|
121
83
|
[
|
|
122
|
-
method,
|
|
84
|
+
method.to_s.upcase,
|
|
123
85
|
encode_path(path),
|
|
124
86
|
encode_query(query),
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
content_sha
|
|
87
|
+
signed.map { |k, v| "#{k}:#{v}\n" }.join,
|
|
88
|
+
signed.map(&:first).join(";"),
|
|
89
|
+
content_sha,
|
|
128
90
|
].join("\n")
|
|
129
91
|
end
|
|
130
92
|
|
|
131
|
-
def
|
|
132
|
-
|
|
133
|
-
"#{ALGORITHM} Credential=#{@credentials.access_key_id}/#{credential_scope}," \
|
|
134
|
-
"SignedHeaders=#{header_list},Signature=#{signature}"
|
|
93
|
+
def credential_scope(date)
|
|
94
|
+
"#{date}/#{@region}/#{SERVICE}/#{REQUEST}"
|
|
135
95
|
end
|
|
136
96
|
|
|
137
|
-
def
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
hmac(
|
|
97
|
+
def signing_key(date)
|
|
98
|
+
key = hmac(@credentials.secret_access_key, date)
|
|
99
|
+
key = hmac(key, @region)
|
|
100
|
+
key = hmac(key, SERVICE)
|
|
101
|
+
hmac(key, REQUEST)
|
|
142
102
|
end
|
|
143
103
|
|
|
144
104
|
def normalize_value(value)
|
|
145
105
|
Array(value).map { |v| v.to_s.split(/\s+/).join(" ") }.join(",")
|
|
146
106
|
end
|
|
147
107
|
|
|
148
|
-
# RFC 3986 percent-encoding for the URI path. Slashes are preserved.
|
|
149
108
|
def encode_path(path)
|
|
150
|
-
path.split("/", -1).map { |
|
|
109
|
+
path.split("/", -1).map { |seg| ERB::Util.url_encode(seg) }.join("/")
|
|
151
110
|
end
|
|
152
111
|
|
|
153
|
-
# Sort by key, then by value, encode and join with `&`.
|
|
154
112
|
def encode_query(query)
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
end
|
|
160
|
-
end
|
|
161
|
-
pairs.sort.map { |k, v| "#{k}=#{v}" }.join("&")
|
|
162
|
-
end
|
|
163
|
-
|
|
164
|
-
def uri_escape(str)
|
|
165
|
-
# URI.encode_www_form_component encodes space as "+", which is wrong for
|
|
166
|
-
# SigV4. Use a manual escape that matches Go's url.QueryEscape rules
|
|
167
|
-
# (which percent-encodes everything except unreserved chars), but preserve
|
|
168
|
-
# nothing else.
|
|
169
|
-
str.b.gsub(/([^A-Za-z0-9\-_.~])/) { format("%%%02X", $1.ord) }
|
|
170
|
-
end
|
|
171
|
-
|
|
172
|
-
def sha256_hex(data)
|
|
173
|
-
OpenSSL::Digest::SHA256.hexdigest(data.to_s)
|
|
174
|
-
end
|
|
175
|
-
|
|
176
|
-
def hmac(key, data)
|
|
177
|
-
OpenSSL::HMAC.digest("SHA256", key, data)
|
|
113
|
+
query.flat_map { |k, vs| Array(vs).map { |v| [ERB::Util.url_encode(k.to_s), ERB::Util.url_encode(v.to_s)] } }
|
|
114
|
+
.sort
|
|
115
|
+
.map { |k, v| "#{k}=#{v}" }
|
|
116
|
+
.join("&")
|
|
178
117
|
end
|
|
179
118
|
|
|
180
|
-
def
|
|
181
|
-
|
|
182
|
-
|
|
119
|
+
def sha256_hex(data) = OpenSSL::Digest::SHA256.hexdigest(data.to_s)
|
|
120
|
+
def hmac(key, data) = OpenSSL::HMAC.digest("SHA256", key, data)
|
|
121
|
+
def hmac_hex(key, data) = OpenSSL::HMAC.hexdigest("SHA256", key, data)
|
|
183
122
|
end
|
|
184
123
|
end
|
data/lib/tos/version.rb
CHANGED
data/lib/tos.rb
CHANGED
|
@@ -10,7 +10,7 @@ require_relative "tos/response"
|
|
|
10
10
|
require_relative "tos/client"
|
|
11
11
|
require_relative "tos/bucket"
|
|
12
12
|
|
|
13
|
-
#
|
|
13
|
+
# Ruby SDK for Volcengine TOS (Torch Object Storage).
|
|
14
14
|
#
|
|
15
15
|
# client = TOS::Client.new(
|
|
16
16
|
# access_key_id: ENV["TOS_ACCESS_KEY_ID"],
|
data/ve-tos-ruby-sdk.gemspec
CHANGED
|
@@ -16,9 +16,6 @@ Gem::Specification.new do |spec|
|
|
|
16
16
|
spec.files = Dir["lib/**/*.rb", "README.md", "LICENSE", "ve-tos-ruby-sdk.gemspec"]
|
|
17
17
|
spec.require_paths = ["lib"]
|
|
18
18
|
|
|
19
|
-
# rexml is no longer a default gem under Bundler-managed contexts.
|
|
20
|
-
spec.add_dependency "rexml", ">= 3.2"
|
|
21
|
-
|
|
22
19
|
spec.add_development_dependency "rspec", "~> 3.12"
|
|
23
20
|
spec.add_development_dependency "webmock", "~> 3.18"
|
|
24
21
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ve-tos-ruby-sdk
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Renny
|
|
@@ -9,20 +9,6 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
-
- !ruby/object:Gem::Dependency
|
|
13
|
-
name: rexml
|
|
14
|
-
requirement: !ruby/object:Gem::Requirement
|
|
15
|
-
requirements:
|
|
16
|
-
- - ">="
|
|
17
|
-
- !ruby/object:Gem::Version
|
|
18
|
-
version: '3.2'
|
|
19
|
-
type: :runtime
|
|
20
|
-
prerelease: false
|
|
21
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
-
requirements:
|
|
23
|
-
- - ">="
|
|
24
|
-
- !ruby/object:Gem::Version
|
|
25
|
-
version: '3.2'
|
|
26
12
|
- !ruby/object:Gem::Dependency
|
|
27
13
|
name: rspec
|
|
28
14
|
requirement: !ruby/object:Gem::Requirement
|