|
| 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 |
0 commit comments