escher 0.0.1 → 0.0.2
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.
- data/lib/escher.rb +55 -28
- metadata +20 -12
- 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.
|
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,
|
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
|
-
|
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
|
-
|
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,
|
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 })
|
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
|
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
|
-
|
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.
|
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
|
43
|
-
"#{algo_id(options[:vendor_prefix], algo)} Credential=#{client[:api_key_id]}/#{
|
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,
|
47
|
-
canonicalized_request = canonicalize method,
|
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
|
50
|
-
|
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,
|
58
|
-
|
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(
|
89
|
+
canonicalize_path(path),
|
64
90
|
canonicalize_query(query),
|
65
|
-
] + canonicalize_headers(date,
|
91
|
+
] + canonicalize_headers(date, host, headers, auth_header_name, date_header_name) + [
|
66
92
|
'',
|
67
|
-
(headers_to_sign |
|
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
|
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 [
|
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(
|
112
|
-
path = uri.path
|
137
|
+
def self.canonicalize_path(path)
|
113
138
|
while path.gsub!(%r{([^/]+)/\.\./?}) { |match| $1 == '..' ? match : '' } do end
|
114
|
-
path
|
139
|
+
path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
|
115
140
|
end
|
116
141
|
|
117
|
-
def self.canonicalize_headers(date,
|
118
|
-
collect_headers(raw_headers, auth_header_name).merge({date_header_name.downcase => [date], 'host' => [
|
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.
|
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:
|
90
|
+
rubygems_version: 1.8.23
|
83
91
|
signing_key:
|
84
|
-
specification_version:
|
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
|