# frozen_string_literal: true require "set" require "openssl" require "faraday" require "active_support/all" module Aliyun module Connector class RPCClient attr_accessor :endpoint, :api_version, :access_key_id, :access_key_secret, :security_token, :codes, :opts, :verbose # 对象初始化属性 def initialize(config, verbose = false) # 参数校验 validate config # 相关属性 self.endpoint = config[:endpoint] self.api_version = config[:api_version] self.access_key_id = config[:access_key_id] || Aliyun.access_key_id self.access_key_secret = config[:access_key_secret] || Aliyun.access_key_secret self.security_token = config[:security_token] self.opts = config[:opts] || {} self.verbose = verbose.instance_of?(TrueClass) && verbose self.codes = Set.new [200, "200", "OK", "Success"] self.codes.merge config[:codes] if config[:codes] end # 通用请求接口 def request(action:, params: {}, opts: {}) opts = self.opts.merge(opts) action = action.upcase_first if opts[:format_action] params = format_params(params) unless opts[:format_params] defaults = default_params params = { Action: action }.merge(defaults).merge(params) method = (opts[:method] || "GET").upcase normalized = normalize(params) canonicalized = canonicalize(normalized) string_to_sign = "#{method}&#{encode('/')}&#{encode(canonicalized)}" secret = "#{self.access_key_secret}&" signature = Base64.encode64(OpenSSL::HMAC.digest("sha1", secret, string_to_sign)).strip normalized.push(["Signature", encode(signature)]) # 转换为 query 样式 querystring = canonicalize(normalized) # 特殊处理 POST uri = opts[:method] == "POST" ? "/" : "/?#{querystring}" # 初始化会话 response = connection.send(method.downcase, uri) do |request| if opts[:method] == "POST" request.headers["Content-Type"] = "application/x-www-form-urlencoded" request.body = querystring end request.headers["User-Agent"] = DEFAULT_UA end # 解析接口响应 response_body = JSON.parse(response.body) if response_body["Code"] && !response_body["Code"].to_s.empty? && !self.codes.include?(response_body["Code"]) raise StandardError, "Code: #{response_body['Code']}, Message: #{response_body['Message']}, URL: #{uri}" end # 返回数据 response_body end private def connection(adapter = Faraday.default_adapter) Faraday.new(url: self.endpoint) { |f| f.adapter adapter } end # 设置缺省参数 def default_params default_params = { "Format" => "JSON", "SignatureMethod" => "HMAC-SHA1", "SignatureNonce" => SecureRandom.hex(16), "SignatureVersion" => "1.0", "Timestamp" => Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"), "AccessKeyId" => self.access_key_id, "Version" => self.api_version, } default_params["SecurityToken"] = self.security_token if self.security_token default_params end # 消息签名需要 def encode(string) ERB::Util.url_encode(string) end # 转换 HASH key 样式 def format_params(param_hash) param_hash.keys.each { |key| param_hash[(key.to_s.upcase_first).to_sym] = param_hash.delete key } param_hash end def replace_repeat_list(target, key, repeat) repeat.each_with_index do |item, index| if item && item.instance_of?(Hash) item.each_key { |k| target["#{key}.#{index.next}.#{k}"] = item[k] } else target["#{key}.#{index.next}"] = item end end target end def flat_params(params) target = {} params.each do |key, value| if value.instance_of?(Array) replace_repeat_list(target, key, value) else target[key.to_s] = value end end target end def normalize(params) flat_params(params).sort.to_h.map { |key, value| [encode(key), encode(value)] } end def canonicalize(normalized) normalized.map { |element| "#{element.first}=#{element.last}" }.join("&") end def validate(config = {}) config.with_indifferent_access raise ArgumentError, 'must pass "config"' unless config raise ArgumentError, 'must pass "config[:endpoint]"' unless config[:endpoint] unless config[:endpoint].match?(/^http[s]?:/i) raise ArgumentError, '"config.endpoint" must starts with \'https://\' or \'http://\'.' end raise ArgumentError, 'must pass "config[:api_version]"' unless config[:api_version] unless config[:access_key_id] || Aliyun.access_key_id raise ArgumentError, 'must pass "config[:access_key_id]" or define "Aliyun.access_key_id"' end unless config[:access_key_secret] || Aliyun.access_key_secret raise ArgumentError, 'must pass "config[:access_key_secret]" or define "Aliyun.access_key_secret"' end end end end end