escher 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []