escher 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/escher.rb +55 -28
  2. metadata +20 -12
  3. checksums.yaml +0 -7
data/lib/escher.rb CHANGED
@@ -2,69 +2,95 @@ require 'time'
2
2
  require 'uri'
3
3
  require 'digest'
4
4
 
5
+ class EscherError < RuntimeError
6
+ end
7
+
5
8
  module Escher
6
- VERSION = '0.0.1'
9
+ VERSION = '0.0.2'
7
10
 
8
11
  def self.default_options
9
12
  {:auth_header_name => 'X-Ems-Auth', :date_header_name => 'X-Ems-Date', :vendor_prefix => 'EMS'}
10
13
  end
11
14
 
12
- def self.validate_request(method, url, body, headers, options = {})
15
+ def self.validate_request(method, request_uri, body, headers, key_db, accepted_credentials, current_time = Time.now, options = {})
13
16
 
14
17
  options = default_options.merge(options)
15
- auth_header = get_header(options[:auth_header_name], headers)
18
+ host = get_header('host', headers)
16
19
  date = get_header(options[:date_header_name], headers)
20
+ auth_header = get_header(options[:auth_header_name], headers)
17
21
 
18
22
  algo, api_key_id, short_date, credential_scope, signed_headers, signature = parse_auth_header auth_header, options[:vendor_prefix]
19
23
 
20
- api_secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'
24
+ raise EscherError, 'Host header is not signed' unless signed_headers.include? 'host'
25
+ raise EscherError, 'Date header is not signed' unless signed_headers.include? options[:date_header_name].downcase
26
+ raise EscherError, 'Invalid request date' unless short_date(date) == short_date && within_range(current_time, date)
27
+ # TODO validate host header
28
+ raise EscherError, 'Invalid credentials' unless credential_scope == accepted_credentials
29
+
30
+ api_secret = key_db[api_key_id]
21
31
 
22
- signature == generate_signature(algo, api_secret, body, credential_scope, date, headers, method, signed_headers, url, options[:vendor_prefix], options[:auth_header_name], options[:date_header_name])
32
+ signature == generate_signature(algo, api_secret, body, credential_scope.join('/'), date, headers, method, signed_headers, host, request_uri, options[:vendor_prefix], options[:auth_header_name], options[:date_header_name])
33
+ end
34
+
35
+ def self.short_date(date)
36
+ long_date(date)[0..7]
37
+ end
38
+
39
+ def self.within_range(current_time, date)
40
+ (current_time - 900 .. current_time + 900).cover?(Time.parse date)
23
41
  end
24
42
 
25
43
  def self.get_header(header_name, headers)
26
- (headers.detect { |header| header[0].downcase == header_name.downcase })[1]
44
+ header = (headers.detect { |header| header[0].downcase == header_name.downcase })
45
+ raise EscherError, "Missing header: #{header_name.downcase}" unless header
46
+ header[1]
27
47
  end
28
48
 
29
49
  def self.parse_auth_header(auth_header, vendor_prefix)
30
- m = /#{vendor_prefix.upcase}-HMAC-(?<algo>[A-Z0-9\,]+) Credential=(?<credentials>[A-Za-z0-9\/\-_]+), SignedHeaders=(?<signed_headers>[A-Za-z\-;]+), Signature=(?<signature>[0-9a-f]+)$/
50
+ m = /#{vendor_prefix.upcase}-HMAC-(?<algo>[A-Z0-9\,]+) Credential=(?<api_key_id>[A-Za-z0-9\-_]+)\/(?<short_date>[0-9]{8})\/(?<credentials>[A-Za-z0-9\-_\/]+), SignedHeaders=(?<signed_headers>[A-Za-z\-;]+), Signature=(?<signature>[0-9a-f]+)$/
31
51
  .match auth_header
52
+ raise EscherError, 'Malformed authorization header' unless m && m['credentials']
32
53
  [
33
54
  m['algo'],
34
- ] + m['credentials'].split('/', 3) + [
55
+ m['api_key_id'],
56
+ m['short_date'],
57
+ m['credentials'].split('/'),
35
58
  m['signed_headers'].split(';'),
36
59
  m['signature'],
37
60
  ]
38
61
  end
39
62
 
40
- def self.get_auth_header(client, method, url, body, headers, headers_to_sign, date = Time.now.utc.rfc2822, algo = 'SHA256', options = {})
63
+ def self.generate_auth_header(client, method, host, request_uri, body, headers, headers_to_sign, date = Time.now.utc.rfc2822, algo = 'SHA256', options = {})
41
64
  options = default_options.merge options
42
- signature = generate_signature(algo, client[:api_secret], body, client[:credential_scope], date, headers, method, headers_to_sign, url, options[:vendor_prefix], options[:auth_header_name], options[:date_header_name])
43
- "#{algo_id(options[:vendor_prefix], algo)} Credential=#{client[:api_key_id]}/#{long_date(date)[0..7]}/#{client[:credential_scope]}, SignedHeaders=#{headers_to_sign.uniq.join ';'}, Signature=#{signature}"
65
+ signature = generate_signature(algo, client[:api_secret], body, credential_scope_as_string(client), date, headers, method, headers_to_sign, host, request_uri, options[:vendor_prefix], options[:auth_header_name], options[:date_header_name])
66
+ "#{algo_id(options[:vendor_prefix], algo)} Credential=#{client[:api_key_id]}/#{short_date(date)}/#{credential_scope_as_string(client)}, SignedHeaders=#{headers_to_sign.uniq.join ';'}, Signature=#{signature}"
67
+ end
68
+
69
+ def self.credential_scope_as_string(client)
70
+ client[:credential_scope].join '/'
44
71
  end
45
72
 
46
- def self.generate_signature(algo, api_secret, body, credential_scope, date, headers, method, signed_headers, url, vendor_prefix, auth_header_name, date_header_name)
47
- canonicalized_request = canonicalize method, url, body, date, headers, signed_headers, algo, auth_header_name, date_header_name
73
+ def self.generate_signature(algo, api_secret, body, credential_scope, date, headers, method, signed_headers, host, request_uri, vendor_prefix, auth_header_name, date_header_name)
74
+ canonicalized_request = canonicalize method, host, request_uri, body, date, headers, signed_headers, algo, auth_header_name, date_header_name
48
75
  string_to_sign = get_string_to_sign credential_scope, canonicalized_request, date, vendor_prefix, algo
49
- signing_key = calculate_signing_key(api_secret, date, vendor_prefix, credential_scope, algo)
50
- signature = calculate_signature(algo, signing_key, string_to_sign)
76
+ signing_key = calculate_signing_key api_secret, date, vendor_prefix, credential_scope, algo
77
+ calculate_signature algo, signing_key, string_to_sign
51
78
  end
52
79
 
53
80
  def self.calculate_signature(algo, signing_key, string_to_sign)
54
81
  Digest::HMAC.hexdigest(string_to_sign, signing_key, create_algo(algo))
55
82
  end
56
83
 
57
- def self.canonicalize(method, url, body, date, headers, headers_to_sign, algo, auth_header_name, date_header_name)
58
- url, query = url.split '?', 2 # URI#parse cannot parse unicode characters in query string TODO use Adressable
59
- uri = URI.parse(url)
84
+ def self.canonicalize(method, host, request_uri, body, date, headers, headers_to_sign, algo, auth_header_name, date_header_name)
85
+ path, query = request_uri.split '?', 2
60
86
 
61
87
  ([
62
88
  method.upcase,
63
- canonicalize_path(uri),
89
+ canonicalize_path(path),
64
90
  canonicalize_query(query),
65
- ] + canonicalize_headers(date, uri, headers, auth_header_name, date_header_name) + [
91
+ ] + canonicalize_headers(date, host, headers, auth_header_name, date_header_name) + [
66
92
  '',
67
- (headers_to_sign | %w(date host)).join(';'),
93
+ (headers_to_sign | [date_header_name.downcase, 'host']).join(';'),
68
94
  request_body_hash(body, algo)
69
95
  ]).join "\n"
70
96
  end
@@ -88,7 +114,7 @@ module Escher
88
114
  when 'SHA512'
89
115
  return Digest::SHA512
90
116
  else
91
- raise('Unidentified hash algorithm')
117
+ raise EscherError, 'Unidentified hash algorithm'
92
118
  end
93
119
  end
94
120
 
@@ -102,20 +128,21 @@ module Escher
102
128
 
103
129
  def self.calculate_signing_key(api_secret, date, vendor_prefix, credential_scope, algo)
104
130
  signing_key = vendor_prefix + api_secret
105
- for data in [long_date(date)[0..7]] + credential_scope.split('/') do
131
+ for data in [short_date(date)] + credential_scope.split('/') do
106
132
  signing_key = Digest::HMAC.digest(data, signing_key, create_algo(algo))
107
133
  end
108
134
  signing_key
109
135
  end
110
136
 
111
- def self.canonicalize_path(uri)
112
- path = uri.path
137
+ def self.canonicalize_path(path)
113
138
  while path.gsub!(%r{([^/]+)/\.\./?}) { |match| $1 == '..' ? match : '' } do end
114
- path = path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
139
+ path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
115
140
  end
116
141
 
117
- def self.canonicalize_headers(date, uri, raw_headers, auth_header_name, date_header_name)
118
- collect_headers(raw_headers, auth_header_name).merge({date_header_name.downcase => [date], 'host' => [uri.host]}).map { |k, v| k + ':' + (v.sort_by { |x| x }).join(',').gsub(/\s+/, ' ').strip }
142
+ def self.canonicalize_headers(date, host, raw_headers, auth_header_name, date_header_name)
143
+ collect_headers(raw_headers, auth_header_name).merge({date_header_name.downcase => [date], 'host' => [host]})
144
+ .sort
145
+ .map { |k, v| k + ':' + (v.sort_by { |x| x }).join(',').gsub(/\s+/, ' ').strip }
119
146
  end
120
147
 
121
148
  def self.collect_headers(raw_headers, auth_header_name)
metadata CHANGED
@@ -1,7 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: escher
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.0.2
5
+ prerelease:
5
6
  platform: ruby
6
7
  authors:
7
8
  - Andras Barthazi
@@ -13,43 +14,49 @@ dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: rspec
15
16
  requirement: !ruby/object:Gem::Requirement
17
+ none: false
16
18
  requirements:
17
- - - '>='
19
+ - - ~>
18
20
  - !ruby/object:Gem::Version
19
21
  version: '0'
20
22
  type: :development
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
23
26
  requirements:
24
- - - '>='
27
+ - - ~>
25
28
  - !ruby/object:Gem::Version
26
29
  version: '0'
27
30
  - !ruby/object:Gem::Dependency
28
31
  name: rake
29
32
  requirement: !ruby/object:Gem::Requirement
33
+ none: false
30
34
  requirements:
31
- - - '>='
35
+ - - ~>
32
36
  - !ruby/object:Gem::Version
33
37
  version: '0'
34
38
  type: :development
35
39
  prerelease: false
36
40
  version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
37
42
  requirements:
38
- - - '>='
43
+ - - ~>
39
44
  - !ruby/object:Gem::Version
40
45
  version: '0'
41
46
  - !ruby/object:Gem::Dependency
42
47
  name: codeclimate-test-reporter
43
48
  requirement: !ruby/object:Gem::Requirement
49
+ none: false
44
50
  requirements:
45
- - - '>='
51
+ - - ~>
46
52
  - !ruby/object:Gem::Version
47
53
  version: '0'
48
54
  type: :development
49
55
  prerelease: false
50
56
  version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
51
58
  requirements:
52
- - - '>='
59
+ - - ~>
53
60
  - !ruby/object:Gem::Version
54
61
  version: '0'
55
62
  description: For Emarsys API
@@ -62,25 +69,26 @@ files:
62
69
  homepage: http://emarsys.com
63
70
  licenses:
64
71
  - MIT
65
- metadata: {}
66
72
  post_install_message:
67
73
  rdoc_options: []
68
74
  require_paths:
69
75
  - lib
70
76
  required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
71
78
  requirements:
72
- - - '>='
79
+ - - ! '>='
73
80
  - !ruby/object:Gem::Version
74
81
  version: '0'
75
82
  required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
76
84
  requirements:
77
- - - '>='
85
+ - - ! '>='
78
86
  - !ruby/object:Gem::Version
79
87
  version: '0'
80
88
  requirements: []
81
89
  rubyforge_project:
82
- rubygems_version: 2.0.7
90
+ rubygems_version: 1.8.23
83
91
  signing_key:
84
- specification_version: 4
92
+ specification_version: 3
85
93
  summary: Escher - Emarsys request signing library
86
94
  test_files: []
checksums.yaml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- SHA1:
3
- metadata.gz: 981338fcbb72266308f87b15a3d9bc95a26d78cd
4
- data.tar.gz: 8523d44bcaabc7165ce430ab469e0684fec5980d
5
- SHA512:
6
- metadata.gz: 6f74fc90792b502604a30b1056b2d3fd9ee13f7ec00982ec45b1aa75da387d7c69b5a51790734cbe8f124c36434f09ca407a3be1d24f14b67aa581ad3d2470ee
7
- data.tar.gz: ee4a26a105765b9ed62e5feeb0aab7e6392b8cfba21dad5765e3c376d77bb3d86c4a0b4b8915dc6b23d99d81f44ee9779f68710b3c2bb043b9a5d62bd88f02ca