ve-tos-ruby-sdk 0.1.1 → 0.1.3
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 +59 -114
- 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: 7d28faa1c3a6f13531292e60ccee65d3679f26f685094171bb4c67bc5395c344
|
|
4
|
+
data.tar.gz: 9b34853fcbde7301fde520242a3678530e97b426b60b982e230312397f1ce327
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0b7f5c1c0342f5eeb82250e873a2240656686dcd16a325c0ba34e8db1630c4c002fd416607b5752b711d5246a66005b162f5d2e771000d5dba992ad6ac6d0520
|
|
7
|
+
data.tar.gz: b36e9449e1bc7c4573908d660138226ab969eb58d2a1e53484a9544611f4df06f31db219dfa60e933b99232d230f091396416b3c18292a56d62cf0e73e331fa1
|
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,102 @@ 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 { |
|
|
149
|
-
end
|
|
150
|
-
|
|
151
|
-
# Sort by key, then by value, encode and join with `&`.
|
|
152
|
-
def encode_query(query)
|
|
153
|
-
pairs = []
|
|
154
|
-
query.each do |key, values|
|
|
155
|
-
Array(values).each do |value|
|
|
156
|
-
pairs << [uri_escape(key.to_s), uri_escape(value.to_s)]
|
|
157
|
-
end
|
|
158
|
-
end
|
|
159
|
-
pairs.sort.map { |k, v| "#{k}=#{v}" }.join("&")
|
|
109
|
+
path.split("/", -1).map { |seg| ERB::Util.url_encode(percent_decode(seg)) }.join("/")
|
|
160
110
|
end
|
|
161
111
|
|
|
162
|
-
def
|
|
163
|
-
|
|
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) }
|
|
112
|
+
def percent_decode(seg)
|
|
113
|
+
seg.b.gsub(/%([0-9A-Fa-f]{2})/) { $1.to_i(16).chr }.force_encoding("UTF-8")
|
|
168
114
|
end
|
|
169
115
|
|
|
170
|
-
def
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
OpenSSL::HMAC.digest("SHA256", key, data)
|
|
116
|
+
def encode_query(query)
|
|
117
|
+
query.flat_map { |k, vs| Array(vs).map { |v| [ERB::Util.url_encode(k.to_s), ERB::Util.url_encode(v.to_s)] } }
|
|
118
|
+
.sort
|
|
119
|
+
.map { |k, v| "#{k}=#{v}" }
|
|
120
|
+
.join("&")
|
|
176
121
|
end
|
|
177
122
|
|
|
178
|
-
def
|
|
179
|
-
|
|
180
|
-
|
|
123
|
+
def sha256_hex(data) = OpenSSL::Digest::SHA256.hexdigest(data.to_s)
|
|
124
|
+
def hmac(key, data) = OpenSSL::HMAC.digest("SHA256", key, data)
|
|
125
|
+
def hmac_hex(key, data) = OpenSSL::HMAC.hexdigest("SHA256", key, data)
|
|
181
126
|
end
|
|
182
127
|
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.3
|
|
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
|