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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9a94a5896f975a89ca93bcad502a1aee316647af303b9279358f9690c01b76a6
4
- data.tar.gz: c99e79d75684d386839cdc1f8f540259b035ca7039b35ce8912ca997e86965bc
3
+ metadata.gz: 95bc0ad985868c8d32fa03105d98e288a7b0deb98d2ad148a359019a6da12a28
4
+ data.tar.gz: b40de0e450ed402f29090100918f5ebae9494f981f1cd71c2ec1824d47073bce
5
5
  SHA512:
6
- metadata.gz: a80ba4f769d55a776dbe0dac3dab068ee253bd9305123e246929d70e21ca619ab144697aaf9c4fa3703686df1ac9603ba300d043d4b42daea80b0a51cff0e8aa
7
- data.tar.gz: 2f15c21d6faa7a0e898461d6ae7106cecb4cfd58da4542cb263f5beddea16a325732a2d2fdb4cced6cc94e138104d79e18df97dae6b980b0fadcbf6378b2632d
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 = build_delete_payload(keys, quiet)
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/xml", "Content-MD5" => content_md5 },
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
- doc = REXML::Document.new(res.body)
100
- root = doc.root
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: truncated,
108
- next_continuation_token: next_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
- doc = REXML::Document.new(body)
140
- code = doc.root&.elements&.[]("Code")&.text
141
- message = doc.root&.elements&.[]("Message")&.text || body
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 "time"
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 the
10
- # TOS4-HMAC-SHA256 scheme (analogous to AWS SigV4, but with the "tos"
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
- working_headers = headers.dup
46
- working_headers["X-Tos-Date"] = datetime
47
- working_headers["X-Tos-Content-Sha256"] = content_sha
48
- working_headers["X-Tos-Security-Token"] = @credentials.security_token if @credentials.security_token
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
- signed_headers = filter_signed_headers(working_headers)
51
-
52
- canonical_request = build_canonical_request(
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
- date = datetime[0, 8]
77
- credential_scope = "#{date}/#{@region}/#{SERVICE}/#{REQUEST}"
78
-
79
- query_with_sig = query.dup
80
- query_with_sig["X-Tos-Algorithm"] = ALGORITHM
81
- query_with_sig["X-Tos-Credential"] = "#{@credentials.access_key_id}/#{credential_scope}"
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
- string_to_sign = [ALGORITHM, datetime, credential_scope, sha256_hex(canonical_request)].join("\n")
96
- signing_key = derive_signing_key(date)
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 mirrors the Go SDK: only `content-type` (lower-case) and
105
- # any `x-tos-*` header is signed. `host` is intentionally NOT signed.
106
- def filter_signed_headers(headers)
107
- headers.each_with_object([]) do |(name, value), acc|
108
- key = name.to_s.downcase
109
- next unless key == "content-type" || key.start_with?("x-tos-")
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
- acc << [key, normalize_value(value)]
112
- end.sort_by(&:first)
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 build_canonical_request(method:, path:, query:, signed_headers:, content_sha:)
116
- canonical_headers = signed_headers.map { |k, v| "#{k}:#{v}\n" }.join
117
- header_list = signed_headers.map(&:first).join(";")
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
- canonical_headers,
124
- header_list,
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 build_authorization_header(signed_headers, credential_scope, signature)
130
- header_list = signed_headers.map(&:first).join(";")
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 derive_signing_key(date)
136
- k_date = hmac(@credentials.secret_access_key, date)
137
- k_region = hmac(k_date, @region)
138
- k_service = hmac(k_region, SERVICE)
139
- hmac(k_service, REQUEST)
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 { |segment| uri_escape(segment) }.join("/")
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
- 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("&")
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 hmac_hex(key, data)
179
- OpenSSL::HMAC.hexdigest("SHA256", key, data)
180
- end
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TOS
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
@@ -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.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