escher 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/escher.gemspec +5 -1
- data/lib/escher/auth.rb +341 -0
- data/lib/escher/request/base.rb +29 -0
- data/lib/escher/request/factory.rb +24 -0
- data/lib/escher/request/hash_request.rb +67 -0
- data/lib/escher/request/legacy_request.rb +157 -0
- data/lib/escher/request/rack_request.rb +42 -0
- data/lib/escher/version.rb +2 -2
- data/lib/escher.rb +6 -2
- data/spec/escher/auth_spec.rb +408 -0
- data/spec/escher/request/factory_spec.rb +20 -0
- data/spec/escher/request/hash_request_spec.rb +121 -0
- data/spec/escher/request/rack_request_spec.rb +114 -0
- metadata +45 -21
- data/lib/escher/base.rb +0 -324
- data/lib/escher/request.rb +0 -83
- data/spec/escher_spec.rb +0 -358
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a08450345302a7c0e0b1d789ef7a23850151f234
|
4
|
+
data.tar.gz: ff3bfb71fba27c558bc3abd9e96331dc8bd1c354
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2b68ae2a96d4ccfa0ad1fbbb2966056e316872e140ee984cd8d38eef474faee7c7b4f81b0e33ea654fce0aaa7f3785589fc53b8200b7ad24eb629b58d8ff5138
|
7
|
+
data.tar.gz: 8ada366ab04fc7895aca5026dcb8a2fc08603cdd604e74979ecb1ad238436b64fc9a07d23fd1d53b23f4c7bdf7b37c3c672abe7205c647ebf10acbd496a84686
|
data/escher.gemspec
CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
|
|
10
10
|
spec.email = ["andras.barthazi@emarsys.com"]
|
11
11
|
spec.summary = %q{Library for HTTP request signing (Ruby implementation)}
|
12
12
|
spec.description = %q{Escher helps you creating secure HTTP requests (for APIs) by signing HTTP(s) requests.}
|
13
|
-
spec.homepage = "
|
13
|
+
spec.homepage = "http://escherauth.io/"
|
14
14
|
spec.license = "MIT"
|
15
15
|
|
16
16
|
spec.files = `git ls-files -z`.split("\x0")
|
@@ -18,9 +18,13 @@ Gem::Specification.new do |spec|
|
|
18
18
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
19
|
spec.require_paths = ["lib"]
|
20
20
|
|
21
|
+
spec.required_ruby_version = '>= 1.9'
|
22
|
+
|
21
23
|
spec.add_development_dependency "bundler", "~> 1.6"
|
22
24
|
spec.add_development_dependency "rake", "~> 10"
|
23
25
|
spec.add_development_dependency "rspec", "~> 2"
|
24
26
|
|
27
|
+
spec.add_development_dependency "rack"
|
28
|
+
|
25
29
|
spec.add_runtime_dependency "addressable", "~> 2.3"
|
26
30
|
end
|
data/lib/escher/auth.rb
ADDED
@@ -0,0 +1,341 @@
|
|
1
|
+
module Escher
|
2
|
+
class Auth
|
3
|
+
|
4
|
+
def initialize(credential_scope, options = {})
|
5
|
+
@credential_scope = credential_scope
|
6
|
+
@algo_prefix = options[:algo_prefix] || 'ESR'
|
7
|
+
@vendor_key = options[:vendor_key] || 'Escher'
|
8
|
+
@hash_algo = options[:hash_algo] || 'SHA256'
|
9
|
+
@current_time = options[:current_time] || Time.now
|
10
|
+
@auth_header_name = options[:auth_header_name] || 'X-Escher-Auth'
|
11
|
+
@date_header_name = options[:date_header_name] || 'X-Escher-Date'
|
12
|
+
@clock_skew = options[:clock_skew] || 900
|
13
|
+
@algo = create_algo
|
14
|
+
@algo_id = @algo_prefix + '-HMAC-' + @hash_algo
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
def sign!(req, client, headers_to_sign = [])
|
20
|
+
headers_to_sign |= [@date_header_name.downcase, 'host']
|
21
|
+
|
22
|
+
request = wrap_request req
|
23
|
+
raise EscherError, 'Missing header: Host' unless request.has_header? 'host'
|
24
|
+
|
25
|
+
request.set_header(@date_header_name, format_date_for_header) unless request.has_header? @date_header_name
|
26
|
+
|
27
|
+
signature = generate_signature(client[:api_secret], request.body, request.headers, request.method, headers_to_sign, request.path, request.query_values)
|
28
|
+
request.set_header(@auth_header_name, "#{@algo_id} Credential=#{client[:api_key_id]}/#{short_date(@current_time)}/#{@credential_scope}, SignedHeaders=#{prepare_headers_to_sign headers_to_sign}, Signature=#{signature}")
|
29
|
+
|
30
|
+
request.request
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
def is_valid?(*args)
|
36
|
+
begin
|
37
|
+
authenticate(*args)
|
38
|
+
return true
|
39
|
+
rescue
|
40
|
+
return false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
def authenticate(req, key_db)
|
47
|
+
request = wrap_request req
|
48
|
+
method = request.method
|
49
|
+
body = request.body
|
50
|
+
headers = request.headers
|
51
|
+
path = request.path
|
52
|
+
query_parts = request.query_values
|
53
|
+
|
54
|
+
signature_from_query = get_signing_param('Signature', query_parts)
|
55
|
+
|
56
|
+
(['Host'] + (signature_from_query ? [] : [@auth_header_name, @date_header_name])).each do |header|
|
57
|
+
raise EscherError, 'Missing header: ' + header unless request.header header
|
58
|
+
end
|
59
|
+
|
60
|
+
if method == 'GET' && signature_from_query
|
61
|
+
raw_date = get_signing_param('Date', query_parts)
|
62
|
+
algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires = get_auth_parts_from_query(query_parts)
|
63
|
+
|
64
|
+
body = 'UNSIGNED-PAYLOAD'
|
65
|
+
query_parts.delete [query_key_for('Signature'), signature]
|
66
|
+
query_parts = query_parts.map { |k, v| [uri_decode(k), uri_decode(v)] }
|
67
|
+
else
|
68
|
+
raw_date = request.header @date_header_name
|
69
|
+
auth_header = request.header @auth_header_name
|
70
|
+
algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires = get_auth_parts_from_header(auth_header)
|
71
|
+
end
|
72
|
+
|
73
|
+
date = Time.parse(raw_date)
|
74
|
+
api_secret = key_db[api_key_id]
|
75
|
+
|
76
|
+
raise EscherError, 'Invalid API key' unless api_secret
|
77
|
+
raise EscherError, 'Only SHA256 and SHA512 hash algorithms are allowed' unless %w(SHA256 SHA512).include?(algorithm)
|
78
|
+
raise EscherError, 'Invalid request date' unless short_date(date) == short_date
|
79
|
+
raise EscherError, 'The request date is not within the accepted time range' unless is_date_within_range?(date, expires)
|
80
|
+
raise EscherError, 'Invalid credentials' unless credential_scope == @credential_scope
|
81
|
+
raise EscherError, 'Host header is not signed' unless signed_headers.include? 'host'
|
82
|
+
raise EscherError, 'Only the host header should be signed' if signature_from_query && signed_headers != ['host']
|
83
|
+
raise EscherError, 'Date header is not signed' if !signature_from_query && !signed_headers.include?(@date_header_name.downcase)
|
84
|
+
|
85
|
+
escher = reconfig(algorithm, credential_scope, date)
|
86
|
+
expected_signature = escher.generate_signature(api_secret, body, headers, method, signed_headers, path, query_parts)
|
87
|
+
raise EscherError, 'The signatures do not match' unless signature == expected_signature
|
88
|
+
api_key_id
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
def reconfig(algorithm, credential_scope, date)
|
94
|
+
self.class.new(
|
95
|
+
credential_scope,
|
96
|
+
algo_prefix: @algo_prefix,
|
97
|
+
vendor_key: @vendor_key,
|
98
|
+
hash_algo: algorithm,
|
99
|
+
auth_header_name: @auth_header_name,
|
100
|
+
date_header_name: @date_header_name,
|
101
|
+
current_time: date
|
102
|
+
)
|
103
|
+
end
|
104
|
+
|
105
|
+
|
106
|
+
|
107
|
+
def generate_signed_url(url_to_sign, client, expires = 86400)
|
108
|
+
uri = Addressable::URI.parse(url_to_sign)
|
109
|
+
host = uri.host
|
110
|
+
path = uri.path
|
111
|
+
query_parts = (uri.query || '')
|
112
|
+
.split('&', -1)
|
113
|
+
.map { |pair| pair.split('=', -1) }
|
114
|
+
.map { |k, v| (k.include? ' ') ? [k.str(/\S+/), ''] : [k, v] }
|
115
|
+
|
116
|
+
headers = [['host', host]]
|
117
|
+
headers_to_sign = ['host']
|
118
|
+
body = 'UNSIGNED-PAYLOAD'
|
119
|
+
query_parts += [
|
120
|
+
['Algorithm', @algo_id],
|
121
|
+
['Credentials', "#{client[:api_key_id]}/#{short_date(@current_time)}/#{@credential_scope}"],
|
122
|
+
['Date', long_date(@current_time)],
|
123
|
+
['Expires', expires.to_s],
|
124
|
+
['SignedHeaders', headers_to_sign.join(';')],
|
125
|
+
].map { |k, v| query_pair(k, v) }
|
126
|
+
|
127
|
+
signature = generate_signature(client[:api_secret], body, headers, 'GET', headers_to_sign, path, query_parts)
|
128
|
+
query_parts_with_signature = (query_parts.map { |k, v| [uri_encode(k), uri_encode(v)] } << query_pair('Signature', signature))
|
129
|
+
|
130
|
+
uri.scheme + '://' + host + path + '?' + query_parts_with_signature.map { |k, v| k + '=' + v }.join('&')
|
131
|
+
end
|
132
|
+
|
133
|
+
|
134
|
+
|
135
|
+
def query_pair(k, v)
|
136
|
+
[query_key_for(k), v]
|
137
|
+
end
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
def query_key_for(key)
|
142
|
+
"X-#{@vendor_key}-#{key}"
|
143
|
+
end
|
144
|
+
|
145
|
+
|
146
|
+
|
147
|
+
def get_signing_param(key, query_parts)
|
148
|
+
the_param = (query_parts.detect { |param| param[0] === query_key_for(key) })
|
149
|
+
the_param ? uri_decode(the_param[1]) : nil
|
150
|
+
end
|
151
|
+
|
152
|
+
|
153
|
+
|
154
|
+
def get_auth_parts_from_header(auth_header)
|
155
|
+
m = /#{@algo_prefix}-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]+)$/
|
156
|
+
.match auth_header
|
157
|
+
raise EscherError, 'Malformed authorization header' unless m && m['credentials']
|
158
|
+
return m['algo'], m['api_key_id'], m['short_date'], m['credentials'], m['signed_headers'].split(';'), m['signature'], 0
|
159
|
+
end
|
160
|
+
|
161
|
+
|
162
|
+
|
163
|
+
def get_auth_parts_from_query(query_parts)
|
164
|
+
expires = get_signing_param('Expires', query_parts).to_i
|
165
|
+
api_key_id, short_date, credential_scope = get_signing_param('Credentials', query_parts).split('/', 3)
|
166
|
+
signed_headers = get_signing_param('SignedHeaders', query_parts).split ';'
|
167
|
+
algorithm = parse_algo(get_signing_param('Algorithm', query_parts))
|
168
|
+
signature = get_signing_param('Signature', query_parts)
|
169
|
+
return algorithm, api_key_id, short_date, credential_scope, signed_headers, signature, expires
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
|
174
|
+
def generate_signature(api_secret, body, headers, method, signed_headers, path, query_parts)
|
175
|
+
canonicalized_request = canonicalize(method, path, query_parts, body, headers, signed_headers.uniq)
|
176
|
+
string_to_sign = get_string_to_sign(canonicalized_request)
|
177
|
+
|
178
|
+
signing_key = Digest::HMAC.digest(short_date(@current_time), @algo_prefix + api_secret, @algo)
|
179
|
+
@credential_scope.split('/').each { |data|
|
180
|
+
signing_key = Digest::HMAC.digest(data, signing_key, @algo)
|
181
|
+
}
|
182
|
+
|
183
|
+
Digest::HMAC.hexdigest(string_to_sign, signing_key, @algo)
|
184
|
+
end
|
185
|
+
|
186
|
+
|
187
|
+
|
188
|
+
def format_date_for_header
|
189
|
+
@date_header_name.downcase == 'date' ? @current_time.utc.rfc2822.sub('-0000', 'GMT') : long_date(@current_time)
|
190
|
+
end
|
191
|
+
|
192
|
+
|
193
|
+
|
194
|
+
def canonicalize(method, path, query_parts, body, headers, headers_to_sign)
|
195
|
+
[
|
196
|
+
method,
|
197
|
+
canonicalize_path(path),
|
198
|
+
canonicalize_query(query_parts),
|
199
|
+
canonicalize_headers(headers, headers_to_sign).join("\n"),
|
200
|
+
'',
|
201
|
+
prepare_headers_to_sign(headers_to_sign),
|
202
|
+
@algo.new.hexdigest(body)
|
203
|
+
].join "\n"
|
204
|
+
end
|
205
|
+
|
206
|
+
|
207
|
+
|
208
|
+
def prepare_headers_to_sign(headers_to_sign)
|
209
|
+
headers_to_sign.sort.uniq.join(';')
|
210
|
+
end
|
211
|
+
|
212
|
+
|
213
|
+
|
214
|
+
def parse_uri(request_uri)
|
215
|
+
path, query = request_uri.split '?', 2
|
216
|
+
return path, (query || '')
|
217
|
+
.split('&', -1)
|
218
|
+
.map { |pair| pair.split('=', -1) }
|
219
|
+
.map { |k, v| (k.include? ' ') ? [k.str(/\S+/), ''] : [k, v] }
|
220
|
+
end
|
221
|
+
|
222
|
+
|
223
|
+
|
224
|
+
def get_string_to_sign(canonicalized_request)
|
225
|
+
[
|
226
|
+
@algo_id,
|
227
|
+
long_date(@current_time),
|
228
|
+
short_date(@current_time) + '/' + @credential_scope,
|
229
|
+
@algo.new.hexdigest(canonicalized_request)
|
230
|
+
].join("\n")
|
231
|
+
end
|
232
|
+
|
233
|
+
|
234
|
+
|
235
|
+
def create_algo
|
236
|
+
case @hash_algo
|
237
|
+
when 'SHA256'
|
238
|
+
@algo = Digest::SHA2.new(256)
|
239
|
+
when 'SHA512'
|
240
|
+
@algo = Digest::SHA2.new(512)
|
241
|
+
else
|
242
|
+
raise EscherError, 'Unidentified hash algorithm'
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
|
247
|
+
|
248
|
+
def long_date(date)
|
249
|
+
date.utc.strftime('%Y%m%dT%H%M%SZ')
|
250
|
+
end
|
251
|
+
|
252
|
+
|
253
|
+
|
254
|
+
def short_date(date)
|
255
|
+
date.utc.strftime('%Y%m%d')
|
256
|
+
end
|
257
|
+
|
258
|
+
|
259
|
+
|
260
|
+
def is_date_within_range?(request_date, expires)
|
261
|
+
(request_date - @clock_skew .. request_date + expires + @clock_skew).cover? @current_time
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
|
266
|
+
def parse_algo(algorithm)
|
267
|
+
m = /^#{@algo_prefix}-HMAC-(?<algo>[A-Z0-9\,]+)$/.match(algorithm)
|
268
|
+
m && m['algo']
|
269
|
+
end
|
270
|
+
|
271
|
+
|
272
|
+
|
273
|
+
def canonicalize_path(path)
|
274
|
+
while path.gsub!(%r{([^/]+)/\.\./?}) { |match| $1 == '..' ? match : '' } do
|
275
|
+
end
|
276
|
+
path.gsub(%r{/\./}, '/').sub(%r{/\.\z}, '/').gsub(/\/+/, '/')
|
277
|
+
end
|
278
|
+
|
279
|
+
|
280
|
+
|
281
|
+
def canonicalize_headers(raw_headers, headers_to_sign)
|
282
|
+
headers = {}
|
283
|
+
raw_headers.each do |raw_header|
|
284
|
+
if raw_header[0].downcase != @auth_header_name.downcase
|
285
|
+
if headers[raw_header[0].downcase]
|
286
|
+
headers[raw_header[0].downcase] << raw_header[1]
|
287
|
+
else
|
288
|
+
headers[raw_header[0].downcase] = [raw_header[1]]
|
289
|
+
end
|
290
|
+
end
|
291
|
+
end
|
292
|
+
headers
|
293
|
+
.sort
|
294
|
+
.select { |h| headers_to_sign.include?(h[0]) }
|
295
|
+
.map { |k, v| k + ':' + v.map { |piece| normalize_white_spaces piece }.join(',') }
|
296
|
+
end
|
297
|
+
|
298
|
+
|
299
|
+
|
300
|
+
def normalize_white_spaces(value)
|
301
|
+
value.strip.split('"', -1).map.with_index { |piece, index|
|
302
|
+
is_inside_of_quotes = (index % 2 === 1)
|
303
|
+
is_inside_of_quotes ? piece : piece.gsub(/\s+/, ' ')
|
304
|
+
}.join '"'
|
305
|
+
end
|
306
|
+
|
307
|
+
|
308
|
+
|
309
|
+
def canonicalize_query(query_parts)
|
310
|
+
query_parts
|
311
|
+
.map { |k, v| uri_encode(k.gsub('+', ' ')) + '=' + uri_encode(v || '') }
|
312
|
+
.sort.join '&'
|
313
|
+
end
|
314
|
+
|
315
|
+
|
316
|
+
|
317
|
+
def uri_encode(component)
|
318
|
+
Addressable::URI.encode_component(component, Addressable::URI::CharacterClasses::UNRESERVED)
|
319
|
+
end
|
320
|
+
|
321
|
+
|
322
|
+
|
323
|
+
def uri_decode(component)
|
324
|
+
Addressable::URI.unencode_component(component)
|
325
|
+
end
|
326
|
+
|
327
|
+
|
328
|
+
|
329
|
+
private
|
330
|
+
|
331
|
+
def wrap_request(request)
|
332
|
+
Escher::Request::Factory.from_request request
|
333
|
+
end
|
334
|
+
|
335
|
+
end
|
336
|
+
|
337
|
+
|
338
|
+
class EscherError < RuntimeError
|
339
|
+
end
|
340
|
+
|
341
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Escher
|
2
|
+
module Request
|
3
|
+
class Base
|
4
|
+
|
5
|
+
attr_reader :request
|
6
|
+
|
7
|
+
|
8
|
+
|
9
|
+
def initialize(request)
|
10
|
+
@request = request
|
11
|
+
end
|
12
|
+
|
13
|
+
|
14
|
+
|
15
|
+
def has_header?(name)
|
16
|
+
not header(name).nil?
|
17
|
+
end
|
18
|
+
|
19
|
+
|
20
|
+
|
21
|
+
def header(name)
|
22
|
+
header = headers.find { |(header_name, _)| header_name.downcase == name.downcase }
|
23
|
+
return nil if header.nil?
|
24
|
+
header[1]
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
require_relative 'hash_request'
|
4
|
+
require_relative 'legacy_request'
|
5
|
+
require_relative 'rack_request'
|
6
|
+
|
7
|
+
module Escher
|
8
|
+
module Request
|
9
|
+
class Factory
|
10
|
+
|
11
|
+
def self.from_request(request)
|
12
|
+
case request
|
13
|
+
when Hash
|
14
|
+
HashRequest.new request
|
15
|
+
when lambda { |request| request.class.ancestors.map(&:to_s).include? "Rack::Request" }
|
16
|
+
RackRequest.new request
|
17
|
+
else
|
18
|
+
Escher::Request::LegacyRequest.new request
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Escher
|
2
|
+
module Request
|
3
|
+
class HashRequest < Base
|
4
|
+
|
5
|
+
# Based on the example in RFC 3986, but scheme, user, password,
|
6
|
+
# host, port and fragment support removed, only path and query left
|
7
|
+
URI_REGEXP = /^([^?#]*)(\?(.*))?$/
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
def initialize(request)
|
12
|
+
super request
|
13
|
+
@uri = parse_uri request[:uri]
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
|
18
|
+
def headers
|
19
|
+
request[:headers].map { |(header_name, value)| [header_name.gsub('_', '-'), value] }
|
20
|
+
end
|
21
|
+
|
22
|
+
|
23
|
+
|
24
|
+
def set_header(name, value)
|
25
|
+
request[:headers] ||= []
|
26
|
+
request[:headers] << [name, value] unless has_header? name
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
def method
|
32
|
+
request[:method]
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
def body
|
38
|
+
request[:body] or ''
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
|
43
|
+
def path
|
44
|
+
@uri.path
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
def query_values
|
50
|
+
@uri.query_values(Array) or []
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def parse_uri(uri)
|
58
|
+
uri.match URI_REGEXP do |match_data|
|
59
|
+
Addressable::URI.new({:path => match_data[1],
|
60
|
+
:query => match_data[3]})
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Escher
|
2
|
+
module Request
|
3
|
+
class LegacyRequest
|
4
|
+
|
5
|
+
# based on the example in RFC 3986, but scheme, user, password,
|
6
|
+
# host, port and fraement support removed, only path and query left
|
7
|
+
URIREGEX = /^([^?#]*)(\?(.*))?$/
|
8
|
+
|
9
|
+
|
10
|
+
|
11
|
+
def initialize(request)
|
12
|
+
@request = request
|
13
|
+
prepare_request_uri
|
14
|
+
prepare_request_headers
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
def prepare_request_uri
|
20
|
+
case @request.class.to_s
|
21
|
+
when 'Hash'
|
22
|
+
uri = @request[:uri]
|
23
|
+
else
|
24
|
+
uri = @request.uri
|
25
|
+
end
|
26
|
+
fragments = uri.scan(URIREGEX)[0]
|
27
|
+
@request_uri = Addressable::URI.new({
|
28
|
+
:path => fragments[0],
|
29
|
+
:query => fragments[2],
|
30
|
+
})
|
31
|
+
raise "Invalid request URI: #{@request_uri}" unless @request_uri
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
|
36
|
+
def prepare_request_headers
|
37
|
+
@request_headers = []
|
38
|
+
case @request.class.to_s
|
39
|
+
when 'Hash'
|
40
|
+
@request_headers = @request[:headers]
|
41
|
+
when 'Sinatra::Request' # TODO: not working yet
|
42
|
+
@request.env.each { |key, value|
|
43
|
+
if key.downcase[0, 5] == "http_"
|
44
|
+
@request_headers += [[key[5..-1].gsub("_", "-"), value]]
|
45
|
+
end
|
46
|
+
}
|
47
|
+
when 'WEBrick::HTTPRequest'
|
48
|
+
@request.header.each { |key, values|
|
49
|
+
values.each { |value|
|
50
|
+
@request_headers += [[key, value]]
|
51
|
+
}
|
52
|
+
}
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
def request
|
59
|
+
@request
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
|
64
|
+
def headers
|
65
|
+
@request_headers
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
|
70
|
+
def set_header(key, value)
|
71
|
+
found = false
|
72
|
+
@request_headers.each { |header|
|
73
|
+
if not found and header[0].downcase == key.downcase
|
74
|
+
header[1] = value
|
75
|
+
found = true
|
76
|
+
end
|
77
|
+
}
|
78
|
+
unless found
|
79
|
+
@request_headers += [[key, value]]
|
80
|
+
end
|
81
|
+
case @request.class.to_s
|
82
|
+
when 'Hash'
|
83
|
+
@request[:headers] = @request_headers
|
84
|
+
else
|
85
|
+
@request[key] = value
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
|
91
|
+
def has_header?(key)
|
92
|
+
@request_headers.each { |header|
|
93
|
+
if header[0].downcase == key.downcase
|
94
|
+
return true
|
95
|
+
end
|
96
|
+
}
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
|
100
|
+
|
101
|
+
|
102
|
+
def method
|
103
|
+
case @request.class.to_s
|
104
|
+
when 'Hash'
|
105
|
+
@request[:method]
|
106
|
+
else
|
107
|
+
@request.request_method
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
|
112
|
+
|
113
|
+
# TODO: create a test for empty body (= nil)
|
114
|
+
def body
|
115
|
+
case @request.class.to_s
|
116
|
+
when 'Hash'
|
117
|
+
@request[:body] || ''
|
118
|
+
else
|
119
|
+
@request.body || ''
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
|
124
|
+
|
125
|
+
def host
|
126
|
+
@request_headers.each { |header|
|
127
|
+
if header[0].downcase == key.downcase
|
128
|
+
return header[1]
|
129
|
+
end
|
130
|
+
}
|
131
|
+
case @request.class.to_s
|
132
|
+
when 'Hash'
|
133
|
+
@request[:host]
|
134
|
+
else
|
135
|
+
begin
|
136
|
+
@request.host
|
137
|
+
rescue
|
138
|
+
""
|
139
|
+
end
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
|
144
|
+
|
145
|
+
def path
|
146
|
+
@request_uri.path
|
147
|
+
end
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
def query_values
|
152
|
+
@request_uri.query_values(Array) || []
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Escher
|
2
|
+
module Request
|
3
|
+
class RackRequest < Base
|
4
|
+
|
5
|
+
def headers
|
6
|
+
request.env.
|
7
|
+
select { |header_name, _| header_name.start_with? "HTTP_" }.
|
8
|
+
map { |header_name, value| [header_name[5..-1].tr('_', '-'), value] }
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
|
13
|
+
def method
|
14
|
+
request.request_method
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
|
19
|
+
def body
|
20
|
+
request.body or ''
|
21
|
+
end
|
22
|
+
|
23
|
+
|
24
|
+
|
25
|
+
def path
|
26
|
+
request.env['REQUEST_PATH']
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
|
31
|
+
def query_values
|
32
|
+
Addressable::URI.new(:query => request.env['QUERY_STRING']).query_values(Array) or []
|
33
|
+
end
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
def set_header(header_name, value)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/escher/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = '0.
|
1
|
+
module Escher
|
2
|
+
VERSION = '0.3.1'
|
3
3
|
end
|