escher 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|