Skip to content

Commit d33bb6b

Browse files
committed
Improve User-Agent
1 parent fe12c9c commit d33bb6b

File tree

11 files changed

+561
-536
lines changed

11 files changed

+561
-536
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
.PHONY: test install build deploy
22

33
test:
4-
rspec --format doc
4+
bundle exec rspec --format doc --exclude-pattern spec/**/*_integration_spec.rb
5+
bundle exec rspec --format doc -P spec/**/*_integration_spec.rb
56

67
deploy:
78
$(eval VERSION := $(shell cat lib/aliyunsdkcore.rb | grep 'VERSION = ' | cut -d\" -f2))

aliyunsdkcore.gemspec

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ Gem::Specification.new do |s|
2424
s.add_dependency 'faraday', '~> 0.15.4'
2525
s.add_dependency 'activesupport', '>= 4.0.0'
2626

27-
s.add_development_dependency "simplecov"
28-
s.add_development_dependency "rspec"
27+
s.add_development_dependency "simplecov", ">= 0.16.1"
28+
s.add_development_dependency "rspec", ">= 3.8.0"
2929
s.add_development_dependency "codecov", ">= 0.1.10"
3030
end

lib/aliyunsdkcore.rb

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1-
require_relative './rpc_client'
2-
require_relative './roa_client'
1+
require 'aliyunsdkcore/rpc_client'
2+
require 'aliyunsdkcore/roa_client'
33

44
module AliyunSDKCore
55
VERSION = "0.0.7"
6+
DEFAULT_UA = "AlibabaCloud (#{Gem::Platform.local.os}; " +
7+
"#{Gem::Platform.local.cpu}) Ruby/#{RUBY_VERSION} Core/#{VERSION}"
68
end
9+
10+
RPCClient = AliyunSDKCore::RPCClient
11+
ROAClient = AliyunSDKCore::ROAClient

lib/aliyunsdkcore/roa_client.rb

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
require 'faraday'
2+
require 'securerandom'
3+
require 'active_support/all'
4+
5+
def present?(obj)
6+
obj.respond_to?(:empty?) ? !obj.empty? : obj
7+
end
8+
9+
module AliyunSDKCore
10+
11+
class ROAClient
12+
13+
attr_accessor :endpoint, :api_version, :access_key_id,
14+
:access_key_secret, :security_token, :hostname, :opts
15+
16+
def initialize(config)
17+
validate config
18+
19+
self.endpoint = config[:endpoint]
20+
self.api_version = config[:api_version]
21+
self.access_key_id = config[:access_key_id]
22+
self.access_key_secret = config[:access_key_secret]
23+
self.security_token = config[:security_token]
24+
end
25+
26+
def request(method:, uri:, params: {}, body: {}, headers: {}, options: {})
27+
28+
mix_headers = default_headers.merge(headers)
29+
30+
response = connection.send(method.downcase) do |request|
31+
request.url uri, params
32+
if present?(body)
33+
request_body = body.to_json
34+
request.body = request_body
35+
mix_headers['content-md5'] = Digest::MD5.base64digest request_body
36+
mix_headers['content-length'] = request_body.length.to_s
37+
end
38+
string2sign = string_to_sign(method, uri, mix_headers, params)
39+
mix_headers.merge!(authorization: authorization(string2sign))
40+
mix_headers.each { |key, value| request.headers[key] = value }
41+
end
42+
43+
return response if options.has_key? :raw_body
44+
45+
response_content_type = response.headers['Content-Type'] || ''
46+
if response_content_type.start_with?('application/json')
47+
if response.status >= 400
48+
result = JSON.parse(response.body)
49+
raise StandardError, "code: #{response.status}, #{result['Message']} requestid: #{result['RequestId']}"
50+
end
51+
end
52+
53+
if response_content_type.start_with?('text/xml')
54+
result = Hash.from_xml(response.body)
55+
raise ACSError, result['Error'] if result['Error']
56+
end
57+
58+
response
59+
end
60+
61+
def connection(adapter = Faraday.default_adapter)
62+
Faraday.new(:url => self.endpoint) { |faraday| faraday.adapter adapter }
63+
end
64+
65+
def get(uri: '', headers: {}, params: {}, options: {})
66+
request(method: :get, uri: uri, params: params, body: {}, headers: headers, options: options)
67+
end
68+
69+
def post(uri: '', headers: {}, params: {}, body: {}, options: {})
70+
request(method: :get, uri: uri, params: params, body: body, headers: headers, options: options)
71+
end
72+
73+
def put(uri: '', headers: {}, params: {}, body: {}, options: {})
74+
request(method: :get, uri: uri, params: params, body: body, headers: headers, options: options)
75+
end
76+
77+
def delete(uri: '', headers: {}, params: {}, options: {})
78+
request(method: :get, uri: uri, params: params, body: {}, headers: headers, options: options)
79+
end
80+
81+
def default_headers
82+
default_headers = {
83+
'accept' => 'application/json',
84+
'date' => Time.now.httpdate,
85+
'host' => URI(self.endpoint).host,
86+
'x-acs-signature-nonce' => SecureRandom.hex(16),
87+
'x-acs-signature-method' => 'HMAC-SHA1',
88+
'x-acs-signature-version' => '1.0',
89+
'x-acs-version' => self.api_version,
90+
'x-sdk-client' => "RUBY(#{RUBY_VERSION})", # FIXME: 如何获取Gem的名称和版本号
91+
'user-agent' => DEFAULT_UA
92+
}
93+
if self.security_token
94+
default_headers.merge!(
95+
'x-acs-accesskey-id' => self.access_key_id,
96+
'x-acs-security-token' => self.security_token
97+
)
98+
end
99+
default_headers
100+
end
101+
102+
private
103+
104+
def string_to_sign(method, uri, headers, query = {})
105+
header_string = [
106+
method,
107+
headers['accept'],
108+
headers['content-md5'] || '',
109+
headers['content-type'] || '',
110+
headers['date'],
111+
].join("\n")
112+
"#{header_string}\n#{canonicalized_headers(headers)}#{canonicalized_resource(uri, query)}"
113+
end
114+
115+
def canonicalized_headers(headers)
116+
headers.keys.select { |key| key.to_s.start_with? 'x-acs-' }
117+
.sort.map { |key| "#{key}:#{headers[key].strip}\n" }.join
118+
end
119+
120+
def canonicalized_resource(uri, query_hash = {})
121+
query_string = query_hash.map { |key, value| "#{key}=#{value}" }.join('&')
122+
query_string.empty? ? uri : "#{uri}?#{query_string}"
123+
end
124+
125+
def authorization(string_to_sign)
126+
"acs #{self.access_key_id}:#{signature(string_to_sign)}"
127+
end
128+
129+
def signature(string_to_sign)
130+
Base64.encode64(OpenSSL::HMAC.digest('sha1', self.access_key_secret, string_to_sign)).strip
131+
end
132+
133+
def validate(config)
134+
raise ArgumentError, 'must pass "config"' unless config
135+
raise ArgumentError, 'must pass "config[:endpoint]"' unless config[:endpoint]
136+
unless config[:endpoint].start_with?('http://') || config[:endpoint].start_with?('https://')
137+
raise ArgumentError, '"config.endpoint" must starts with \'https://\' or \'http://\'.'
138+
end
139+
raise ArgumentError, 'must pass "config[:api_version]"' unless config[:api_version]
140+
raise ArgumentError, 'must pass "config[:access_key_id]"' unless config[:access_key_id]
141+
raise ArgumentError, 'must pass "config[:access_key_secret]"' unless config[:access_key_secret]
142+
end
143+
144+
class ACSError < StandardError
145+
146+
attr_accessor :code
147+
148+
def initialize(error)
149+
self.code = error['Code']
150+
message = error['Message']
151+
host_id = error['HostId']
152+
request_id = error['RequestId']
153+
super("#{message} host_id: #{host_id}, request_id: #{request_id}")
154+
end
155+
156+
end
157+
158+
end
159+
end

lib/aliyunsdkcore/rpc_client.rb

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
require 'set'
2+
require 'openssl'
3+
require 'faraday'
4+
require 'active_support/all'
5+
6+
# Converts just the first character to uppercase.
7+
#
8+
# upcase_first('what a Lovely Day') # => "What a Lovely Day"
9+
# upcase_first('w') # => "W"
10+
# upcase_first('') # => ""
11+
def upcase_first(string)
12+
string.length > 0 ? string[0].upcase.concat(string[1..-1]) : ""
13+
end
14+
15+
module AliyunSDKCore
16+
17+
class RPCClient
18+
19+
attr_accessor :endpoint, :api_version, :access_key_id, :access_key_secret, :security_token, :codes, :opts, :verbose
20+
21+
def initialize(config, verbose = false)
22+
23+
validate config
24+
25+
self.endpoint = config[:endpoint]
26+
self.api_version = config[:api_version]
27+
self.access_key_id = config[:access_key_id]
28+
self.access_key_secret = config[:access_key_secret]
29+
self.security_token = config[:security_token]
30+
self.opts = config[:opts] || {}
31+
self.verbose = verbose.instance_of?(TrueClass) && verbose
32+
self.codes = Set.new [200, '200', 'OK', 'Success']
33+
self.codes.merge config[:codes] if config[:codes]
34+
end
35+
36+
def request(action:, params: {}, opts: {})
37+
opts = self.opts.merge(opts)
38+
action = upcase_first(action) if opts[:format_action]
39+
params = format_params(params) unless opts[:format_params]
40+
defaults = default_params
41+
params = { Action: action }.merge(defaults).merge(params)
42+
method = (opts[:method] || 'GET').upcase
43+
normalized = normalize(params)
44+
canonicalized = canonicalize(normalized)
45+
string_to_sign = "#{method}&#{encode('/')}&#{encode(canonicalized)}"
46+
key = self.access_key_secret + '&'
47+
signature = Base64.encode64(OpenSSL::HMAC.digest('sha1', key, string_to_sign)).strip
48+
normalized.push(['Signature', encode(signature)])
49+
50+
querystring = canonicalize(normalized)
51+
52+
uri = opts[:method] == 'POST' ? '/' : "/?#{querystring}"
53+
54+
response = connection.send(method.downcase, uri) do |request|
55+
request.headers['User-Agent'] = DEFAULT_UA
56+
if opts[:method] == 'POST'
57+
request.headers['Content-Type'] = 'application/x-www-form-urlencoded'
58+
request.body = querystring
59+
end
60+
end
61+
62+
response_body = JSON.parse(response.body)
63+
if response_body['Code'] && !self.codes.include?(response_body['Code'])
64+
raise StandardError, "#{response_body['Message']}, URL: #{uri}"
65+
end
66+
67+
response_body
68+
end
69+
70+
private
71+
72+
def connection(adapter = Faraday.default_adapter)
73+
Faraday.new(:url => self.endpoint) { |faraday| faraday.adapter adapter }
74+
end
75+
76+
def default_params
77+
default_params = {
78+
'Format' => 'JSON',
79+
'SignatureMethod' => 'HMAC-SHA1',
80+
'SignatureNonce' => SecureRandom.hex(16),
81+
'SignatureVersion' => '1.0',
82+
'Timestamp' => Time.now.utc.strftime('%Y-%m-%dT%H:%M:%SZ'),
83+
'AccessKeyId' => self.access_key_id,
84+
'Version' => self.api_version,
85+
}
86+
default_params.merge!('SecurityToken' => self.security_token) if self.security_token
87+
default_params
88+
end
89+
90+
def encode(string)
91+
encoded = CGI.escape string
92+
encoded.gsub(/[\+]/, '%20')
93+
end
94+
95+
def format_params(param_hash)
96+
param_hash.keys.each { |key| param_hash[upcase_first(key.to_s).to_sym] = param_hash.delete key }
97+
param_hash
98+
end
99+
100+
def replace_repeat_list(target, key, repeat)
101+
repeat.each_with_index do |item, index|
102+
if item && item.instance_of?(Hash)
103+
item.each_key { |k| target["#{key}.#{index.next}.#{k}"] = item[k] }
104+
else
105+
target["#{key}.#{index.next}"] = item
106+
end
107+
end
108+
target
109+
end
110+
111+
def flat_params(params)
112+
target = {}
113+
params.each do |key, value|
114+
if value.instance_of?(Array)
115+
replace_repeat_list(target, key, value)
116+
else
117+
target[key.to_s] = value
118+
end
119+
end
120+
target
121+
end
122+
123+
def normalize(params)
124+
flat_params(params)
125+
.sort
126+
.to_h
127+
.map { |key, value| [encode(key), encode(value)] }
128+
end
129+
130+
def canonicalize(normalized)
131+
normalized.map { |element| "#{element.first}=#{element.last}" }.join('&')
132+
end
133+
134+
def validate(config)
135+
raise ArgumentError, 'must pass "config"' unless config
136+
raise ArgumentError, 'must pass "config[:endpoint]"' unless config[:endpoint]
137+
unless config[:endpoint].start_with?('http://') || config[:endpoint].start_with?('https://')
138+
raise ArgumentError, '"config.endpoint" must starts with \'https://\' or \'http://\'.'
139+
end
140+
raise ArgumentError, 'must pass "config[:api_version]"' unless config[:api_version]
141+
raise ArgumentError, 'must pass "config[:access_key_id]"' unless config[:access_key_id]
142+
raise ArgumentError, 'must pass "config[:access_key_secret]"' unless config[:access_key_secret]
143+
end
144+
end
145+
end

0 commit comments

Comments
 (0)