lib/api_signature/builder.rb in api_signature-0.1.5 vs lib/api_signature/builder.rb in api_signature-1.0.0

- old
+ new

@@ -1,51 +1,119 @@ # frozen_string_literal: true -require 'active_support/hash_with_indifferent_access' -require 'ostruct' +require 'uri' module ApiSignature class Builder - OPTIONS_KEYS = [ - :access_key, :secret, :request_method, :path, :timestamp - ].freeze + attr_reader :settings - delegate(*OPTIONS_KEYS, to: :@settings) - delegate :expired?, to: :signature_generator + SPLITTER = "\n" - def initialize(settings = {}) - settings = HashWithIndifferentAccess.new(settings) + def initialize(settings = {}, unsigned_headers = []) + @settings = settings + @unsigned_headers = unsigned_headers + end - settings['timestamp'] ||= Time.now.utc.to_i.to_s - settings['request_method'] = (settings['request_method'] || settings['method']).upcase + def http_method + @http_method ||= extract_http_method + end - @settings = OpenStruct.new(settings.select { |k, _v| OPTIONS_KEYS.include?(k.to_sym) }) + def uri + @uri ||= extract_uri end + def host + @host ||= extract_host_from_uri + end + def headers - { - 'X-Access-Key' => options[:access_key], - 'X-Timestamp' => options[:timestamp], - 'X-Signature' => signature - } + @headers ||= Utils.normalize_keys(settings[:headers]) end - def options - { - timestamp: timestamp, - request_method: request_method, - path: path, - access_key: access_key + def datetime + @datetime ||= extract_datetime + end + + def date + @date ||= datetime.to_s.scan(/\d/).take(8).join + end + + def content_sha256 + @content_sha256 ||= Utils.sha256_hexdigest(body) + end + + def body + @body ||= (settings[:body] || '') + end + + def build_sign_headers(apply_checksum_header = false) + @sign_headers = { + 'host' => host, + 'x-datetime' => datetime } + @sign_headers['x-content-sha256'] = content_sha256 if apply_checksum_header + @sign_headers end - def signature - @signature ||= signature_generator.generate_signature(secret) + def full_headers + @full_headers ||= merge_sign_with_origin_headers end + def signed_headers + @signed_headers ||= full_headers.reject { |key, _value| @unsigned_headers.include?(key) } + end + + def signed_headers_names + @signed_headers_names ||= signed_headers.keys.sort.join(';') + end + + def canonical_request(path) + [ + http_method, + path, + Utils.normalized_querystring(uri.query), + canonical_headers + SPLITTER, + signed_headers_names, + content_sha256 + ].join(SPLITTER) + end + private - def signature_generator - @signature_generator ||= Generator.new(options) + def extract_http_method + raise ArgumentError, 'missing required option :http_method' unless settings[:http_method] + + settings[:http_method].to_s.upcase + end + + def extract_uri + raise ArgumentError, 'missing required option :url' unless settings[:url] + + URI.parse(settings[:url].to_s) + end + + def extract_host_from_uri + if Utils.standard_port?(uri) + uri.host + else + "#{uri.host}:#{uri.port}" + end + end + + def extract_datetime + headers['x-datetime'] || Time.now.utc.strftime(ApiSignature.configuration.datetime_format) + end + + def merge_sign_with_origin_headers + raise ArgumentError, 'missing required variable sign_headers' unless @sign_headers + + # merge so we do not modify given headers hash + headers.merge(@sign_headers) + end + + def canonical_headers + signed_headers.sort_by(&:first) + .map { |k, v| "#{k}:#{Utils.canonical_header_value(v.to_s)}" } + .join(SPLITTER) end end end