ve-tos-ruby-sdk 0.1.1 → 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 -115
- data/lib/tos/version.rb +1 -1
- data/ve-tos-ruby-sdk.gemspec +0 -2
- 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,15 +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).
|
|
7
|
+
# Signs HTTP requests for the TOS object service using TOS4-HMAC-SHA256
|
|
8
|
+
# (AWS SigV4 with the "tos" service name)
|
|
13
9
|
class Signer
|
|
14
10
|
ALGORITHM = "TOS4-HMAC-SHA256"
|
|
15
11
|
SERVICE = "tos"
|
|
@@ -30,153 +26,98 @@ module TOS
|
|
|
30
26
|
# Sign a request and return a Hash of headers to add (Authorization,
|
|
31
27
|
# X-Tos-Date, X-Tos-Content-Sha256, plus X-Tos-Security-Token when STS
|
|
32
28
|
# credentials are used).
|
|
33
|
-
#
|
|
34
|
-
# @param method [String] HTTP method, uppercased (e.g. "PUT")
|
|
35
|
-
# @param path [String] URI path starting with "/" (e.g. "/bucket/key")
|
|
36
|
-
# @param query [Hash{String=>String,Array<String>}] query params
|
|
37
|
-
# @param headers [Hash] existing request headers (case-insensitive keys)
|
|
38
|
-
# @param body [String, nil] request body — may be nil for GET/HEAD/DELETE
|
|
39
|
-
# @param now [Time] override for testing
|
|
40
29
|
def sign_headers(method:, path:, query: {}, headers: {}, body: nil, now: Time.now.utc)
|
|
41
30
|
datetime = now.strftime("%Y%m%dT%H%M%SZ")
|
|
42
|
-
date = datetime[0, 8]
|
|
43
31
|
content_sha = body ? sha256_hex(body) : EMPTY_SHA256
|
|
44
32
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
|
49
37
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
method: method.to_s.upcase,
|
|
54
|
-
path: path,
|
|
55
|
-
query: query,
|
|
56
|
-
signed_headers: signed_headers,
|
|
57
|
-
content_sha: content_sha
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
credential_scope = "#{date}/#{@region}/#{SERVICE}/#{REQUEST}"
|
|
61
|
-
string_to_sign = [ALGORITHM, datetime, credential_scope, sha256_hex(canonical_request)].join("\n")
|
|
62
|
-
|
|
63
|
-
signing_key = derive_signing_key(date)
|
|
64
|
-
signature = hmac_hex(signing_key, string_to_sign)
|
|
65
|
-
|
|
66
|
-
authorization = build_authorization_header(signed_headers, credential_scope, signature)
|
|
67
|
-
working_headers["Authorization"] = authorization
|
|
68
|
-
|
|
69
|
-
working_headers
|
|
38
|
+
signed = signed_headers(out)
|
|
39
|
+
out["Authorization"] = authorization(method, path, query, signed, content_sha, datetime)
|
|
40
|
+
out
|
|
70
41
|
end
|
|
71
42
|
|
|
72
43
|
# Build a presigned URL with all signing material in the query string.
|
|
73
|
-
# Returns a fully-qualified URL.
|
|
74
44
|
def presign(method:, host:, path:, query: {}, expires_in: 3600, now: Time.now.utc)
|
|
75
45
|
datetime = now.strftime("%Y%m%dT%H%M%SZ")
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
query_with_sig["X-Tos-Date"] = datetime
|
|
83
|
-
query_with_sig["X-Tos-Expires"] = expires_in.to_s
|
|
84
|
-
query_with_sig["X-Tos-SignedHeaders"] = ""
|
|
85
|
-
query_with_sig["X-Tos-Security-Token"] = @credentials.security_token if @credentials.security_token
|
|
86
|
-
|
|
87
|
-
canonical_request = build_canonical_request(
|
|
88
|
-
method: method.to_s.upcase,
|
|
89
|
-
path: path,
|
|
90
|
-
query: query_with_sig,
|
|
91
|
-
signed_headers: [],
|
|
92
|
-
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" => "",
|
|
93
52
|
)
|
|
53
|
+
params["X-Tos-Security-Token"] = @credentials.security_token if @credentials.security_token
|
|
94
54
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
query_with_sig["X-Tos-Signature"] = hmac_hex(signing_key, string_to_sign)
|
|
98
|
-
|
|
99
|
-
"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)}"
|
|
100
57
|
end
|
|
101
58
|
|
|
102
59
|
private
|
|
103
60
|
|
|
104
|
-
# Default policy
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
110
68
|
|
|
111
|
-
|
|
112
|
-
|
|
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}"
|
|
113
74
|
end
|
|
114
75
|
|
|
115
|
-
def
|
|
116
|
-
|
|
117
|
-
|
|
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
|
|
118
81
|
|
|
82
|
+
def canonical_request(method, path, query, signed, content_sha)
|
|
119
83
|
[
|
|
120
|
-
method,
|
|
84
|
+
method.to_s.upcase,
|
|
121
85
|
encode_path(path),
|
|
122
86
|
encode_query(query),
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
content_sha
|
|
87
|
+
signed.map { |k, v| "#{k}:#{v}\n" }.join,
|
|
88
|
+
signed.map(&:first).join(";"),
|
|
89
|
+
content_sha,
|
|
126
90
|
].join("\n")
|
|
127
91
|
end
|
|
128
92
|
|
|
129
|
-
def
|
|
130
|
-
|
|
131
|
-
"#{ALGORITHM} Credential=#{@credentials.access_key_id}/#{credential_scope}," \
|
|
132
|
-
"SignedHeaders=#{header_list},Signature=#{signature}"
|
|
93
|
+
def credential_scope(date)
|
|
94
|
+
"#{date}/#{@region}/#{SERVICE}/#{REQUEST}"
|
|
133
95
|
end
|
|
134
96
|
|
|
135
|
-
def
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
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)
|
|
140
102
|
end
|
|
141
103
|
|
|
142
104
|
def normalize_value(value)
|
|
143
105
|
Array(value).map { |v| v.to_s.split(/\s+/).join(" ") }.join(",")
|
|
144
106
|
end
|
|
145
107
|
|
|
146
|
-
# RFC 3986 percent-encoding for the URI path. Slashes are preserved.
|
|
147
108
|
def encode_path(path)
|
|
148
|
-
path.split("/", -1).map { |
|
|
109
|
+
path.split("/", -1).map { |seg| ERB::Util.url_encode(seg) }.join("/")
|
|
149
110
|
end
|
|
150
111
|
|
|
151
|
-
# Sort by key, then by value, encode and join with `&`.
|
|
152
112
|
def encode_query(query)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
pairs.sort.map { |k, v| "#{k}=#{v}" }.join("&")
|
|
160
|
-
end
|
|
161
|
-
|
|
162
|
-
def uri_escape(str)
|
|
163
|
-
# URI.encode_www_form_component encodes space as "+", which is wrong for
|
|
164
|
-
# SigV4. Use a manual escape that matches Go's url.QueryEscape rules
|
|
165
|
-
# (which percent-encodes everything except unreserved chars), but preserve
|
|
166
|
-
# nothing else.
|
|
167
|
-
str.b.gsub(/([^A-Za-z0-9\-_.~])/) { format("%%%02X", $1.ord) }
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
def sha256_hex(data)
|
|
171
|
-
OpenSSL::Digest::SHA256.hexdigest(data.to_s)
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
def hmac(key, data)
|
|
175
|
-
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("&")
|
|
176
117
|
end
|
|
177
118
|
|
|
178
|
-
def
|
|
179
|
-
|
|
180
|
-
|
|
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)
|
|
181
122
|
end
|
|
182
123
|
end
|
data/lib/tos/version.rb
CHANGED
data/ve-tos-ruby-sdk.gemspec
CHANGED
|
@@ -16,8 +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
|
-
spec.add_dependency "rexml", ">= 3.2"
|
|
20
|
-
|
|
21
19
|
spec.add_development_dependency "rspec", "~> 3.12"
|
|
22
20
|
spec.add_development_dependency "webmock", "~> 3.18"
|
|
23
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
|