Module: MAuth::Signable

Included in:
Request, Response
Defined in:
lib/mauth/request_and_response.rb

Overview

module which composes a string to sign.

includer must provide

  • SIGNATURE_COMPONENTS OR SIGNATURE_COMPONENTS_V2 constant - array of keys to get from

  • #attributes_for_signing

  • #merge_headers (takes a Hash of headers; returns an instance of includer’s own class whose headers have been updated with the argument headers)

Instance Method Summary collapse

Instance Method Details

#attributes_for_signingObject



128
129
130
# File 'lib/mauth/request_and_response.rb', line 128

def attributes_for_signing
  @attributes_for_signing
end

#initialize(attributes_for_signing) ⇒ Object



124
125
126
# File 'lib/mauth/request_and_response.rb', line 124

def initialize(attributes_for_signing)
  @attributes_for_signing = attributes_for_signing
end

#normalize_path(path) ⇒ Object

Addressable::URI.parse(path).normalize.to_s.squeeze(‘/’)



92
93
94
95
96
97
98
99
100
101
# File 'lib/mauth/request_and_response.rb', line 92

def normalize_path(path)
  return if path.nil?

  # Addressable::URI.normalize_path normalizes `.` and `..` in path
  #   i.e. /./example => /example ; /example/.. => /
  # String#squeeze removes duplicated slahes i.e. /// => /
  # String#gsub normalizes percent encoding to uppercase i.e. %cf%80 => %CF%80
  Addressable::URI.normalize_path(path).squeeze('/')
    .gsub(/%[a-f0-9]{2}/, &:upcase)
end

#string_to_sign_v1(more_attributes) ⇒ Object

the string to sign for V1 protocol will be (where LF is line feed character) for requests:

string_to_sign =
  http_verb + <LF> +
  resource_url_path (no host, port or query string; first "/" is included) + <LF> +
  request_body + <LF> +
  app_uuid + <LF> +
  current_seconds_since_epoch

for responses:

string_to_sign =
  status_code_string + <LF> +
  response_body + <LF> +
  app_uuid + <LF> +
  current_seconds_since_epoch


34
35
36
37
38
39
40
41
42
43
44
45
46
# File 'lib/mauth/request_and_response.rb', line 34

def string_to_sign_v1(more_attributes)
  attributes_for_signing = self.attributes_for_signing.merge(more_attributes)
  missing_attributes = self.class::SIGNATURE_COMPONENTS.select do |key|
    !attributes_for_signing.key?(key) || attributes_for_signing[key].nil?
  end
  missing_attributes.delete(:body) # body may be omitted
  if missing_attributes.any?
    raise(UnableToSignError,
      "Missing required attributes to sign: #{missing_attributes.inspect}\non object to sign: #{inspect}")
  end

  self.class::SIGNATURE_COMPONENTS.map { |k| attributes_for_signing[k].to_s }.join("\n")
end

#string_to_sign_v2(override_attrs) ⇒ Object

the string to sign for V2 protocol will be (where LF is line feed character) for requests:

string_to_sign =
  http_verb + <LF> +
  resource_url_path (no host, port or query string; first "/" is included) + <LF> +
  request_body_digest + <LF> +
  app_uuid + <LF> +
  current_seconds_since_epoch + <LF> +
  encoded_query_params

for responses:

string_to_sign =
  status_code_string + <LF> +
  response_body_digest + <LF> +
  app_uuid + <LF> +
  current_seconds_since_epoch


64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/mauth/request_and_response.rb', line 64

def string_to_sign_v2(override_attrs)
  attrs_with_overrides = attributes_for_signing.merge(override_attrs)

  # memoization of body_digest to avoid hashing three times when we call
  # string_to_sign_v2 three times in client#signature_valid_v2!
  # note that if :body is nil we hash an empty string ('')
  attrs_with_overrides[:body_digest] ||= OpenSSL::Digest.hexdigest('SHA512', attrs_with_overrides[:body] || '')
  attrs_with_overrides[:encoded_query_params] =
    unescape_encode_query_string(attrs_with_overrides[:query_string] || '')
  attrs_with_overrides[:request_url] = normalize_path(attrs_with_overrides[:request_url])

  missing_attributes = self.class::SIGNATURE_COMPONENTS_V2.reject do |key|
    attrs_with_overrides[key]
  end

  missing_attributes.delete(:body_digest) # body may be omitted
  missing_attributes.delete(:encoded_query_params) # query_string may be omitted
  if missing_attributes.any?
    raise(UnableToSignError,
      "Missing required attributes to sign: #{missing_attributes.inspect}\non object to sign: #{inspect}")
  end

  self.class::SIGNATURE_COMPONENTS_V2.map do |k|
    attrs_with_overrides[k].to_s.dup.force_encoding('UTF-8')
  end.join("\n")
end

#unescape_encode_query_string(q_string) ⇒ Object

sorts query string parameters by codepoint, uri encodes keys and values, and rejoins parameters into a query string



105
106
107
108
109
110
111
112
# File 'lib/mauth/request_and_response.rb', line 105

def unescape_encode_query_string(q_string)
  q_string.split('&').map do |part|
    k, _eq, v = part.partition('=')
    [CGI.unescape(k), CGI.unescape(v)]
  end.sort.map do |k, v| # rubocop:disable Style/MultilineBlockChain
    "#{uri_escape(k)}=#{uri_escape(v)}"
  end.join('&')
end

#uri_escape(string) ⇒ Object

percent encodes special characters, preserving character encoding. encodes space as ‘%20’ does not encode A-Z, a-z, 0-9, hyphen ( - ), underscore ( _ ), period ( . ), or tilde ( ~ ) NOTE the CGI.escape spec changed in 2.5 to not escape tildes. we gsub tilde encoding back to tildes to account for older Rubies



120
121
122
# File 'lib/mauth/request_and_response.rb', line 120

def uri_escape(string)
  CGI.escape(string).gsub(/\+|%7E/, '+' => '%20', '%7E' => '~')
end