escher 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/lib/escher.rb +154 -0
- 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: []
|