escher 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. checksums.yaml +7 -0
  2. data/lib/escher.rb +154 -0
  3. metadata +86 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 981338fcbb72266308f87b15a3d9bc95a26d78cd
4
+ data.tar.gz: 8523d44bcaabc7165ce430ab469e0684fec5980d
5
+ SHA512:
6
+ metadata.gz: 6f74fc90792b502604a30b1056b2d3fd9ee13f7ec00982ec45b1aa75da387d7c69b5a51790734cbe8f124c36434f09ca407a3be1d24f14b67aa581ad3d2470ee
7
+ data.tar.gz: ee4a26a105765b9ed62e5feeb0aab7e6392b8cfba21dad5765e3c376d77bb3d86c4a0b4b8915dc6b23d99d81f44ee9779f68710b3c2bb043b9a5d62bd88f02ca
data/lib/escher.rb ADDED
@@ -0,0 +1,154 @@
1
+ require 'time'
2
+ require 'uri'
3
+ require 'digest'
4
+
5
+ module Escher
6
+ VERSION = '0.0.1'
7
+
8
+ def self.default_options
9
+ {:auth_header_name => 'X-Ems-Auth', :date_header_name => 'X-Ems-Date', :vendor_prefix => 'EMS'}
10
+ end
11
+
12
+ def self.validate_request(method, url, body, headers, options = {})
13
+
14
+ options = default_options.merge(options)
15
+ auth_header = get_header(options[:auth_header_name], headers)
16
+ date = get_header(options[:date_header_name], headers)
17
+
18
+ algo, api_key_id, short_date, credential_scope, signed_headers, signature = parse_auth_header auth_header, options[:vendor_prefix]
19
+
20
+ api_secret = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'
21
+
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])
23
+ end
24
+
25
+ def self.get_header(header_name, headers)
26
+ (headers.detect { |header| header[0].downcase == header_name.downcase })[1]
27
+ end
28
+
29
+ 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]+)$/
31
+ .match auth_header
32
+ [
33
+ m['algo'],
34
+ ] + m['credentials'].split('/', 3) + [
35
+ m['signed_headers'].split(';'),
36
+ m['signature'],
37
+ ]
38
+ end
39
+
40
+ def self.get_auth_header(client, method, url, body, headers, headers_to_sign, date = Time.now.utc.rfc2822, algo = 'SHA256', options = {})
41
+ 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}"
44
+ end
45
+
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
48
+ 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)
51
+ end
52
+
53
+ def self.calculate_signature(algo, signing_key, string_to_sign)
54
+ Digest::HMAC.hexdigest(string_to_sign, signing_key, create_algo(algo))
55
+ end
56
+
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)
60
+
61
+ ([
62
+ method.upcase,
63
+ canonicalize_path(uri),
64
+ canonicalize_query(query),
65
+ ] + canonicalize_headers(date, uri, headers, auth_header_name, date_header_name) + [
66
+ '',
67
+ (headers_to_sign | %w(date host)).join(';'),
68
+ request_body_hash(body, algo)
69
+ ]).join "\n"
70
+ end
71
+
72
+ # TODO: extract algo creation
73
+ def self.get_string_to_sign(credential_scope, canonicalized_request, date, prefix, algo)
74
+ date = long_date(date)
75
+ lines = [
76
+ algo_id(prefix, algo),
77
+ date,
78
+ date[0..7] + '/' + credential_scope,
79
+ create_algo(algo).new.hexdigest(canonicalized_request)
80
+ ]
81
+ lines.join "\n"
82
+ end
83
+
84
+ def self.create_algo(algo)
85
+ case algo.upcase
86
+ when 'SHA256'
87
+ return Digest::SHA256
88
+ when 'SHA512'
89
+ return Digest::SHA512
90
+ else
91
+ raise('Unidentified hash algorithm')
92
+ end
93
+ end
94
+
95
+ def self.long_date(date)
96
+ Time.parse(date).utc.strftime("%Y%m%dT%H%M%SZ")
97
+ end
98
+
99
+ def self.algo_id(prefix, algo)
100
+ prefix + '-HMAC-' + algo
101
+ end
102
+
103
+ def self.calculate_signing_key(api_secret, date, vendor_prefix, credential_scope, algo)
104
+ signing_key = vendor_prefix + api_secret
105
+ for data in [long_date(date)[0..7]] + credential_scope.split('/') do
106
+ signing_key = Digest::HMAC.digest(data, signing_key, create_algo(algo))
107
+ end
108
+ signing_key
109
+ end
110
+
111
+ def self.canonicalize_path(uri)
112
+ path = uri.path
113
+ while path.gsub!(%r{([^/]+)/\.\./?}) { |match| $1 == '..' ? match : '' } do end
114
+ path = path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
115
+ end
116
+
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 }
119
+ end
120
+
121
+ def self.collect_headers(raw_headers, auth_header_name)
122
+ headers = {}
123
+ raw_headers.each { |raw_header|
124
+ if raw_header[0].downcase != auth_header_name.downcase then
125
+ if headers[raw_header[0].downcase] then
126
+ headers[raw_header[0].downcase] << raw_header[1]
127
+ else
128
+ headers[raw_header[0].downcase] = [raw_header[1]]
129
+ end
130
+ end
131
+ }
132
+ headers
133
+ end
134
+
135
+ def self.request_body_hash(body, algo)
136
+ create_algo(algo).new.hexdigest body
137
+ end
138
+
139
+ def self.canonicalize_query(query)
140
+ query = query || ''
141
+ query.split('&', -1)
142
+ .map { |pair| k, v = pair.split('=', -1)
143
+ if k.include? ' ' then
144
+ [k.str(/\S+/), '']
145
+ else
146
+ [k, v]
147
+ end }
148
+ .map { |pair|
149
+ k, v = pair;
150
+ URI::encode(k.gsub('+', ' ')) + '=' + URI::encode(v || '')
151
+ }
152
+ .sort.join '&'
153
+ end
154
+ end
metadata ADDED
@@ -0,0 +1,86 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: escher
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Andras Barthazi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-08-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rspec
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: codeclimate-test-reporter
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '>='
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: For Emarsys API
56
+ email: andras.barthazi@emarsys.com
57
+ executables: []
58
+ extensions: []
59
+ extra_rdoc_files: []
60
+ files:
61
+ - lib/escher.rb
62
+ homepage: http://emarsys.com
63
+ licenses:
64
+ - MIT
65
+ metadata: {}
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - '>='
73
+ - !ruby/object:Gem::Version
74
+ version: '0'
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - '>='
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubyforge_project:
82
+ rubygems_version: 2.0.7
83
+ signing_key:
84
+ specification_version: 4
85
+ summary: Escher - Emarsys request signing library
86
+ test_files: []