tilia-http 4.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +19 -0
- data/.rspec +1 -0
- data/.rubocop.yml +35 -0
- data/.simplecov +4 -0
- data/.travis.yml +3 -0
- data/CHANGELOG.sabre.md +235 -0
- data/CONTRIBUTING.md +25 -0
- data/Gemfile +18 -0
- data/Gemfile.lock +69 -0
- data/LICENSE +27 -0
- data/LICENSE.sabre +27 -0
- data/README.md +68 -0
- data/Rakefile +17 -0
- data/examples/asyncclient.rb +45 -0
- data/examples/basicauth.rb +39 -0
- data/examples/client.rb +20 -0
- data/examples/reverseproxy.rb +39 -0
- data/examples/stringify.rb +37 -0
- data/lib/tilia/http/auth/abstract_auth.rb +51 -0
- data/lib/tilia/http/auth/aws.rb +191 -0
- data/lib/tilia/http/auth/basic.rb +43 -0
- data/lib/tilia/http/auth/bearer.rb +37 -0
- data/lib/tilia/http/auth/digest.rb +187 -0
- data/lib/tilia/http/auth.rb +12 -0
- data/lib/tilia/http/client.rb +452 -0
- data/lib/tilia/http/client_exception.rb +15 -0
- data/lib/tilia/http/client_http_exception.rb +37 -0
- data/lib/tilia/http/http_exception.rb +21 -0
- data/lib/tilia/http/message.rb +241 -0
- data/lib/tilia/http/message_decorator_trait.rb +183 -0
- data/lib/tilia/http/message_interface.rb +154 -0
- data/lib/tilia/http/request.rb +235 -0
- data/lib/tilia/http/request_decorator.rb +160 -0
- data/lib/tilia/http/request_interface.rb +126 -0
- data/lib/tilia/http/response.rb +164 -0
- data/lib/tilia/http/response_decorator.rb +58 -0
- data/lib/tilia/http/response_interface.rb +36 -0
- data/lib/tilia/http/sapi.rb +165 -0
- data/lib/tilia/http/url_util.rb +70 -0
- data/lib/tilia/http/util.rb +51 -0
- data/lib/tilia/http/version.rb +9 -0
- data/lib/tilia/http.rb +416 -0
- data/test/http/auth/aws_test.rb +189 -0
- data/test/http/auth/basic_test.rb +60 -0
- data/test/http/auth/bearer_test.rb +47 -0
- data/test/http/auth/digest_test.rb +141 -0
- data/test/http/client_mock.rb +101 -0
- data/test/http/client_test.rb +331 -0
- data/test/http/message_decorator_test.rb +67 -0
- data/test/http/message_test.rb +163 -0
- data/test/http/request_decorator_test.rb +87 -0
- data/test/http/request_test.rb +132 -0
- data/test/http/response_decorator_test.rb +28 -0
- data/test/http/response_test.rb +38 -0
- data/test/http/sapi_mock.rb +12 -0
- data/test/http/sapi_test.rb +133 -0
- data/test/http/url_util_test.rb +155 -0
- data/test/http/util_test.rb +186 -0
- data/test/http_test.rb +102 -0
- data/test/test_helper.rb +6 -0
- data/tilia-http.gemspec +18 -0
- metadata +192 -0
data/lib/tilia/http.rb
ADDED
@@ -0,0 +1,416 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'date'
|
3
|
+
|
4
|
+
# Namespace for Tilia library
|
5
|
+
module Tilia
|
6
|
+
# Load active support core extensions
|
7
|
+
require 'active_support'
|
8
|
+
require 'active_support/core_ext'
|
9
|
+
|
10
|
+
# Char detecting functions
|
11
|
+
require 'rchardet'
|
12
|
+
|
13
|
+
# Rack for IO handling with server
|
14
|
+
require 'rack'
|
15
|
+
|
16
|
+
# HTTP handling
|
17
|
+
require 'typhoeus'
|
18
|
+
|
19
|
+
# Tilia libraries
|
20
|
+
require 'tilia/event'
|
21
|
+
require 'tilia/uri'
|
22
|
+
|
23
|
+
# Namespace of the Tilia::Xml library
|
24
|
+
# A collection of useful helpers for parsing or generating various HTTP
|
25
|
+
# headers.
|
26
|
+
module Http
|
27
|
+
require 'tilia/http/auth'
|
28
|
+
require 'tilia/http/http_exception'
|
29
|
+
require 'tilia/http/client_exception'
|
30
|
+
require 'tilia/http/client_http_exception'
|
31
|
+
require 'tilia/http/client'
|
32
|
+
require 'tilia/http/message_decorator_trait'
|
33
|
+
require 'tilia/http/message_interface'
|
34
|
+
require 'tilia/http/message'
|
35
|
+
require 'tilia/http/request_interface'
|
36
|
+
require 'tilia/http/request_decorator'
|
37
|
+
require 'tilia/http/request'
|
38
|
+
require 'tilia/http/response_interface'
|
39
|
+
require 'tilia/http/response_decorator'
|
40
|
+
require 'tilia/http/response'
|
41
|
+
require 'tilia/http/sapi'
|
42
|
+
require 'tilia/http/url_util'
|
43
|
+
require 'tilia/http/util'
|
44
|
+
require 'tilia/http/version'
|
45
|
+
|
46
|
+
# Parses a HTTP date-string.
|
47
|
+
#
|
48
|
+
# This method returns false if the date is invalid.
|
49
|
+
#
|
50
|
+
# The following formats are supported:
|
51
|
+
# Sun, 06 Nov 1994 08:49:37 GMT ; IMF-fixdate
|
52
|
+
# Sunday, 06-Nov-94 08:49:37 GMT ; obsolete RFC 850 format
|
53
|
+
# Sun Nov 6 08:49:37 1994 ; ANSI C's asctime format
|
54
|
+
#
|
55
|
+
# See:
|
56
|
+
# http://tools.ietf.org/html/rfc7231#section-7.1.1.1
|
57
|
+
#
|
58
|
+
# @param [String] date_string
|
59
|
+
# @return bool|DateTime
|
60
|
+
def self.parse_date(date_string)
|
61
|
+
return false unless date_string
|
62
|
+
|
63
|
+
# Only the format is checked, valid ranges are checked by strtotime below
|
64
|
+
month = '(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)'
|
65
|
+
weekday = '(Monday|Tuesday|Wednesday|Thursday|Friday|Saturday|Sunday)'
|
66
|
+
wkday = '(Mon|Tue|Wed|Thu|Fri|Sat|Sun)'
|
67
|
+
time = '([0-1]\d|2[0-3])(\:[0-5]\d){2}'
|
68
|
+
date3 = month + ' ([12]\d|3[01]| [1-9])'
|
69
|
+
date2 = '(0[1-9]|[12]\d|3[01])\-' + month + '\-\d{2}'
|
70
|
+
# 4-digit year cannot begin with 0 - unix timestamp begins in 1970
|
71
|
+
date1 = '(0[1-9]|[12]\d|3[01]) ' + month + ' [1-9]\d{3}'
|
72
|
+
|
73
|
+
# ANSI C's asctime() format
|
74
|
+
# 4-digit year cannot begin with 0 - unix timestamp begins in 1970
|
75
|
+
asctime_date = wkday + ' ' + date3 + ' ' + time + ' [1-9]\d{3}'
|
76
|
+
# RFC 850, obsoleted by RFC 1036
|
77
|
+
rfc850_date = weekday + ', ' + date2 + ' ' + time + ' GMT'
|
78
|
+
# RFC 822, updated by RFC 1123
|
79
|
+
rfc1123_date = wkday + ', ' + date1 + ' ' + time + ' GMT'
|
80
|
+
# allowed date formats by RFC 2616
|
81
|
+
http_date = "(#{rfc1123_date}|#{rfc850_date}|#{asctime_date})"
|
82
|
+
|
83
|
+
# allow for space around the string and strip it
|
84
|
+
date_string.strip!
|
85
|
+
|
86
|
+
return false unless date_string =~ /^#{http_date}$/
|
87
|
+
|
88
|
+
date = Time.zone.parse date_string
|
89
|
+
|
90
|
+
# Ruby does not accept ANSI + GMT
|
91
|
+
date += date.utc_offset.seconds unless date_string.index('GMT')
|
92
|
+
|
93
|
+
# Correct 2 digit years
|
94
|
+
if date.year < 100
|
95
|
+
date_string.gsub!(
|
96
|
+
format('-%02i', date.year),
|
97
|
+
format('-%04i', Time.now.year.div(100) * 100 + date.year)
|
98
|
+
)
|
99
|
+
date = Time.zone.parse(date_string)
|
100
|
+
if date > (Time.now + 1.month)
|
101
|
+
date = Time.zone.parse(
|
102
|
+
date.to_s.gsub(
|
103
|
+
format('%04i', date.year),
|
104
|
+
format('%04i', date.year - 100)
|
105
|
+
)
|
106
|
+
)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
date
|
111
|
+
end
|
112
|
+
|
113
|
+
# Transforms a DateTime object to a valid HTTP/1.1 Date header value
|
114
|
+
#
|
115
|
+
# @param DateTime date_time
|
116
|
+
# @return [String]
|
117
|
+
def self.to_date(date_time)
|
118
|
+
# We need to clone it, as we don't want to affect the existing
|
119
|
+
# DateTime.
|
120
|
+
date_time.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
121
|
+
end
|
122
|
+
|
123
|
+
# This function can be used to aid with content negotiation.
|
124
|
+
#
|
125
|
+
# It takes 2 arguments, the accept_header_value, which usually comes from
|
126
|
+
# an Accept header, and available_options, which contains an array of
|
127
|
+
# items that the server can support.
|
128
|
+
#
|
129
|
+
# The result of this function will be the 'best possible option'. If no
|
130
|
+
# best possible option could be found, null is returned.
|
131
|
+
#
|
132
|
+
# When it's null you can according to the spec either return a default, or
|
133
|
+
# you can choose to emit 406 Not Acceptable.
|
134
|
+
#
|
135
|
+
# The method also accepts sending 'null' for the accept_header_value,
|
136
|
+
# implying that no accept header was sent.
|
137
|
+
#
|
138
|
+
# @param [String, nil] accept_header_value
|
139
|
+
# @param array available_options
|
140
|
+
# @return [String, nil]
|
141
|
+
def self.negotiate_content_type(accept_header_value, available_options)
|
142
|
+
unless accept_header_value
|
143
|
+
# Grabbing the first in the list.
|
144
|
+
return available_options[0]
|
145
|
+
end
|
146
|
+
|
147
|
+
proposals = accept_header_value.split(',').map { |m| parse_mime_type(m) }
|
148
|
+
|
149
|
+
options = available_options.map { |m| parse_mime_type(m) }
|
150
|
+
|
151
|
+
last_quality = 0
|
152
|
+
last_specificity = 0
|
153
|
+
last_option_index = 0
|
154
|
+
last_choice = nil
|
155
|
+
|
156
|
+
proposals.each do |proposal|
|
157
|
+
# Ignoring broken values.
|
158
|
+
next if proposal.nil?
|
159
|
+
|
160
|
+
# If the quality is lower we don't have to bother comparing.
|
161
|
+
next if proposal['quality'] < last_quality
|
162
|
+
|
163
|
+
options.each_with_index do |option, option_index|
|
164
|
+
if proposal['type'] != '*' && proposal['type'] != option['type']
|
165
|
+
# no match on type.
|
166
|
+
next
|
167
|
+
end
|
168
|
+
if proposal['subType'] != '*' && proposal['subType'] != option['subType']
|
169
|
+
# no match on subtype.
|
170
|
+
next
|
171
|
+
end
|
172
|
+
|
173
|
+
# Any parameters appearing on the options must appear on
|
174
|
+
# proposals.
|
175
|
+
flow = true
|
176
|
+
option['parameters'].each do |param_name, param_value|
|
177
|
+
flow = false unless proposal['parameters'].key?(param_name)
|
178
|
+
flow = false unless param_value == proposal['parameters'][param_name]
|
179
|
+
end
|
180
|
+
next unless flow
|
181
|
+
|
182
|
+
# If we got here, we have a match on parameters, type and
|
183
|
+
# subtype. We need to calculate a score for how specific the
|
184
|
+
# match was.
|
185
|
+
specificity = (proposal['type'] != '*' ? 20 : 0) +
|
186
|
+
(proposal['subType'] != '*' ? 10 : 0) +
|
187
|
+
(option['parameters'].size)
|
188
|
+
|
189
|
+
# Does this entry win?
|
190
|
+
next unless (proposal['quality'] > last_quality) ||
|
191
|
+
(proposal['quality'] == last_quality && specificity > last_specificity) ||
|
192
|
+
(proposal['quality'] == last_quality && specificity == last_specificity && option_index < last_option_index)
|
193
|
+
|
194
|
+
last_quality = proposal['quality']
|
195
|
+
last_specificity = specificity
|
196
|
+
last_option_index = option_index
|
197
|
+
last_choice = available_options[option_index]
|
198
|
+
end
|
199
|
+
end
|
200
|
+
last_choice
|
201
|
+
end
|
202
|
+
|
203
|
+
# Parses the Prefer header, as defined in RFC7240.
|
204
|
+
#
|
205
|
+
# Input can be given as a single header value (string) or multiple headers
|
206
|
+
# (array of string).
|
207
|
+
#
|
208
|
+
# This method will return a key.value array with the various Prefer
|
209
|
+
# parameters.
|
210
|
+
#
|
211
|
+
# Prefer: return=minimal will result in:
|
212
|
+
#
|
213
|
+
# [ 'return' => 'minimal' ]
|
214
|
+
#
|
215
|
+
# Prefer: foo, wait=10 will result in:
|
216
|
+
#
|
217
|
+
# [ 'foo' => true, 'wait' => '10']
|
218
|
+
#
|
219
|
+
# This method also supports the formats from older drafts of RFC7240, and
|
220
|
+
# it will automatically map them to the new values, as the older values
|
221
|
+
# are still pretty common.
|
222
|
+
#
|
223
|
+
# Parameters are currently discarded. There's no known prefer value that
|
224
|
+
# uses them.
|
225
|
+
#
|
226
|
+
# @param [String, Array<String>] header
|
227
|
+
# @return array
|
228
|
+
def self.parse_prefer(input)
|
229
|
+
token = '[!#$%&\'*+\-.^_`~A-Za-z0-9]+'
|
230
|
+
|
231
|
+
# Work in progress
|
232
|
+
word = '(?: [a-zA-Z0-9]+ | "[a-zA-Z0-9]*" )'
|
233
|
+
|
234
|
+
pattern = /
|
235
|
+
^
|
236
|
+
(?<name> #{token}) # Prefer property name
|
237
|
+
\s* # Optional space
|
238
|
+
(?: = \s* # Prefer property value
|
239
|
+
(?<value> #{word})
|
240
|
+
)?
|
241
|
+
(?: \s* ; (?: .*))? # Prefer parameters (ignored)
|
242
|
+
$
|
243
|
+
/x
|
244
|
+
|
245
|
+
output = {}
|
246
|
+
header_values(input).each do |value|
|
247
|
+
match = pattern.match(value)
|
248
|
+
next unless match
|
249
|
+
|
250
|
+
# Mapping old values to their new counterparts
|
251
|
+
case match['name']
|
252
|
+
when 'return-asynch'
|
253
|
+
output['respond-async'] = true
|
254
|
+
when 'return-representation'
|
255
|
+
output['return'] = 'representation'
|
256
|
+
when 'return-minimal'
|
257
|
+
output['return'] = 'minimal'
|
258
|
+
when 'strict'
|
259
|
+
output['handling'] = 'strict'
|
260
|
+
when 'lenient'
|
261
|
+
output['handling'] = 'lenient'
|
262
|
+
else
|
263
|
+
if match['value']
|
264
|
+
value = match['value'].gsub(/^"*|"*$/, '')
|
265
|
+
else
|
266
|
+
value = true
|
267
|
+
end
|
268
|
+
|
269
|
+
output[match['name'].downcase] = value.blank? ? true : value
|
270
|
+
end
|
271
|
+
end
|
272
|
+
output
|
273
|
+
end
|
274
|
+
|
275
|
+
# This method splits up headers into all their individual values.
|
276
|
+
#
|
277
|
+
# A HTTP header may have more than one header, such as this:
|
278
|
+
# Cache-Control: private, no-store
|
279
|
+
#
|
280
|
+
# Header values are always split with a comma.
|
281
|
+
#
|
282
|
+
# You can pass either a string, or an array. The resulting value is always
|
283
|
+
# an array with each spliced value.
|
284
|
+
#
|
285
|
+
# If the second headers argument is set, this value will simply be merged
|
286
|
+
# in. This makes it quicker to merge an old list of values with a new set.
|
287
|
+
#
|
288
|
+
# @param [String, Array<String>] values
|
289
|
+
# @param [String, Array<String>] values2
|
290
|
+
# @return [String][]
|
291
|
+
def self.header_values(values, values2 = nil)
|
292
|
+
values = [values] unless values.is_a?(Array)
|
293
|
+
if values2
|
294
|
+
values2 = [values2] unless values2.is_a?(Array)
|
295
|
+
values.concat(values2)
|
296
|
+
end
|
297
|
+
|
298
|
+
result = []
|
299
|
+
values.each do |l1|
|
300
|
+
l1.split(',').each do |l2|
|
301
|
+
result << l2.strip
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
result
|
306
|
+
end
|
307
|
+
|
308
|
+
# Parses a mime-type and splits it into:
|
309
|
+
#
|
310
|
+
# 1. type
|
311
|
+
# 2. subtype
|
312
|
+
# 3. quality
|
313
|
+
# 4. parameters
|
314
|
+
#
|
315
|
+
# @param [String] str
|
316
|
+
# @return array
|
317
|
+
def self.parse_mime_type(str)
|
318
|
+
parameters = {}
|
319
|
+
# If no q= parameter appears, then quality = 1.
|
320
|
+
quality = 1
|
321
|
+
|
322
|
+
parts = str.split(';')
|
323
|
+
|
324
|
+
# The first part is the mime-type.
|
325
|
+
mime_type = parts.shift
|
326
|
+
|
327
|
+
mime_type = mime_type.strip.split('/')
|
328
|
+
if mime_type.size != 2
|
329
|
+
# Illegal value
|
330
|
+
return nil
|
331
|
+
end
|
332
|
+
(type, sub_type) = mime_type
|
333
|
+
|
334
|
+
parts.each do |part|
|
335
|
+
part = part.strip
|
336
|
+
equal = part.index('=')
|
337
|
+
if !equal.nil? && equal > 0
|
338
|
+
(part_name, part_value) = part.split('=', 2)
|
339
|
+
else
|
340
|
+
part_name = part
|
341
|
+
part_value = nil
|
342
|
+
end
|
343
|
+
|
344
|
+
# The quality parameter, if it appears, also marks the end of
|
345
|
+
# the parameter list. Anything after the q= counts as an
|
346
|
+
# 'accept extension' and could introduce new semantics in
|
347
|
+
# content-negotation.
|
348
|
+
if part_name != 'q'
|
349
|
+
parameters[part_name] = part
|
350
|
+
else
|
351
|
+
quality = part_value.to_f
|
352
|
+
break; # Stop parsing parts
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
{
|
357
|
+
'type' => type,
|
358
|
+
'subType' => sub_type,
|
359
|
+
'quality' => quality,
|
360
|
+
'parameters' => parameters
|
361
|
+
}
|
362
|
+
end
|
363
|
+
|
364
|
+
# Encodes the path of a url.
|
365
|
+
#
|
366
|
+
# slashes (/) are treated as path-separators.
|
367
|
+
#
|
368
|
+
# @param [String] path
|
369
|
+
# @return [String]
|
370
|
+
def self.encode_path(path)
|
371
|
+
path.gsub(%r{([^A-Za-z0-9_\-\.~\(\)\/:@])}) do |m|
|
372
|
+
m.bytes.inject('') do |str, byte|
|
373
|
+
str << "%#{format('%02x', byte.ord)}"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
|
378
|
+
# Encodes a 1 segment of a path
|
379
|
+
#
|
380
|
+
# Slashes are considered part of the name, and are encoded as %2f
|
381
|
+
#
|
382
|
+
# @param [String] path_segment
|
383
|
+
# @return [String]
|
384
|
+
def self.encode_path_segment(path_segment)
|
385
|
+
path_segment.gsub(/([^A-Za-z0-9_\-\.~\(\):@])/) do |m|
|
386
|
+
m.bytes.inject('') do |str, byte|
|
387
|
+
str << "%#{format('%02x', byte.ord)}"
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|
391
|
+
|
392
|
+
# Decodes a url-encoded path
|
393
|
+
#
|
394
|
+
# @param [String] path
|
395
|
+
# @return [String]
|
396
|
+
def self.decode_path(path)
|
397
|
+
decode_path_segment(path)
|
398
|
+
end
|
399
|
+
|
400
|
+
# Decodes a url-encoded path segment
|
401
|
+
#
|
402
|
+
# @param [String] path
|
403
|
+
# @return [String]
|
404
|
+
def self.decode_path_segment(path)
|
405
|
+
path = URI.unescape(path)
|
406
|
+
cd = CharDet.detect(path)
|
407
|
+
|
408
|
+
# Best solution I could find ...
|
409
|
+
if cd['encoding'] =~ /(?:windows|iso)/i
|
410
|
+
path = path.encode('UTF-8', cd['encoding'])
|
411
|
+
end
|
412
|
+
|
413
|
+
path
|
414
|
+
end
|
415
|
+
end
|
416
|
+
end
|
@@ -0,0 +1,189 @@
|
|
1
|
+
require 'digest'
|
2
|
+
require 'base64'
|
3
|
+
require 'openssl'
|
4
|
+
require 'test_helper'
|
5
|
+
|
6
|
+
module Tilia
|
7
|
+
module Http
|
8
|
+
class AWSTest < Minitest::Test
|
9
|
+
REALM = 'SabreDAV unittest'
|
10
|
+
|
11
|
+
def setup
|
12
|
+
@response = Response.new
|
13
|
+
@request = Request.new
|
14
|
+
@auth = Auth::Aws.new(REALM, @request, @response)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Generates an HMAC-SHA1 signature
|
18
|
+
#
|
19
|
+
# @param [String] key
|
20
|
+
# @param [String] message
|
21
|
+
# @return [String]
|
22
|
+
def hmacsha1(key, message)
|
23
|
+
# Built in in Ruby
|
24
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha1'), key, message)
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_no_header
|
28
|
+
@request.method = 'GET'
|
29
|
+
result = @auth.init
|
30
|
+
|
31
|
+
refute(result, 'No AWS Authorization header was supplied, so we should have gotten false')
|
32
|
+
|
33
|
+
assert_equal(Auth::Aws::ERR_NOAWSHEADER, @auth.error_code)
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_incorrect_content_md5
|
37
|
+
access_key = 'accessKey'
|
38
|
+
secret_key = 'secretKey'
|
39
|
+
|
40
|
+
@request.method = 'GET'
|
41
|
+
@request.update_headers(
|
42
|
+
'Authorization' => "AWS #{access_key}:sig",
|
43
|
+
'Content-MD5' => 'garbage'
|
44
|
+
)
|
45
|
+
@request.url = '/'
|
46
|
+
|
47
|
+
@auth.init
|
48
|
+
result = @auth.validate(secret_key)
|
49
|
+
|
50
|
+
refute(result)
|
51
|
+
assert_equal(Auth::Aws::ERR_MD5CHECKSUMWRONG, @auth.error_code)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_no_date
|
55
|
+
access_key = 'accessKey'
|
56
|
+
secret_key = 'secretKey'
|
57
|
+
content = 'thisisthebody'
|
58
|
+
content_md5 = Base64.strict_encode64(Digest::MD5.digest(content))
|
59
|
+
|
60
|
+
@request.method = 'POST'
|
61
|
+
@request.update_headers(
|
62
|
+
'Authorization' => "AWS #{access_key}:sig",
|
63
|
+
'Content-MD5' => content_md5
|
64
|
+
)
|
65
|
+
@request.body = content
|
66
|
+
|
67
|
+
@auth.init
|
68
|
+
result = @auth.validate(secret_key)
|
69
|
+
|
70
|
+
refute(result)
|
71
|
+
assert_equal(Auth::Aws::ERR_INVALIDDATEFORMAT, @auth.error_code)
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_future_date
|
75
|
+
access_key = 'accessKey'
|
76
|
+
secret_key = 'secretKey'
|
77
|
+
content = 'thisisthebody'
|
78
|
+
content_md5 = Base64.strict_encode64(Digest::MD5.digest(content))
|
79
|
+
|
80
|
+
date = Time.zone.now + 20.minutes
|
81
|
+
date = date.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
82
|
+
|
83
|
+
@request.method = 'POST'
|
84
|
+
@request.update_headers(
|
85
|
+
'Authorization' => "AWS #{access_key}:sig",
|
86
|
+
'Content-MD5' => content_md5,
|
87
|
+
'Date' => date
|
88
|
+
)
|
89
|
+
|
90
|
+
@request.body = content
|
91
|
+
|
92
|
+
@auth.init
|
93
|
+
result = @auth.validate(secret_key)
|
94
|
+
|
95
|
+
refute(result)
|
96
|
+
assert_equal(Auth::Aws::ERR_REQUESTTIMESKEWED, @auth.error_code)
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_past_date
|
100
|
+
access_key = 'accessKey'
|
101
|
+
secret_key = 'secretKey'
|
102
|
+
content = 'thisisthebody'
|
103
|
+
content_md5 = Base64.strict_encode64(Digest::MD5.digest(content))
|
104
|
+
|
105
|
+
date = Time.zone.now - 20.minutes
|
106
|
+
date = date.to_time.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
107
|
+
|
108
|
+
@request.method = 'POST'
|
109
|
+
@request.update_headers(
|
110
|
+
'Authorization' => "AWS #{access_key}:sig",
|
111
|
+
'Content-MD5' => content_md5,
|
112
|
+
'Date' => date
|
113
|
+
)
|
114
|
+
|
115
|
+
@request.body = content
|
116
|
+
|
117
|
+
@auth.init
|
118
|
+
result = @auth.validate(secret_key)
|
119
|
+
|
120
|
+
refute(result)
|
121
|
+
assert_equal(Auth::Aws::ERR_REQUESTTIMESKEWED, @auth.error_code)
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_incorrect_signature
|
125
|
+
access_key = 'accessKey'
|
126
|
+
secret_key = 'secretKey'
|
127
|
+
content = 'thisisthebody'
|
128
|
+
content_md5 = Base64.strict_encode64(Digest::MD5.digest(content))
|
129
|
+
|
130
|
+
date = Time.zone.now
|
131
|
+
date = date.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
132
|
+
|
133
|
+
@request.url = '/'
|
134
|
+
@request.method = 'POST'
|
135
|
+
@request.update_headers(
|
136
|
+
'Authorization' => "AWS #{access_key}:sig",
|
137
|
+
'Content-MD5' => content_md5,
|
138
|
+
'X-amz-date' => date
|
139
|
+
)
|
140
|
+
@request.body = content
|
141
|
+
|
142
|
+
@auth.init
|
143
|
+
result = @auth.validate(secret_key)
|
144
|
+
|
145
|
+
refute(result)
|
146
|
+
assert_equal(Auth::Aws::ERR_INVALIDSIGNATURE, @auth.error_code)
|
147
|
+
end
|
148
|
+
|
149
|
+
def test_valid_request
|
150
|
+
access_key = 'accessKey'
|
151
|
+
secret_key = 'secretKey'
|
152
|
+
content = 'thisisthebody'
|
153
|
+
content_md5 = Base64.strict_encode64(Digest::MD5.digest(content))
|
154
|
+
|
155
|
+
date = Time.zone.now
|
156
|
+
date = date.utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
|
157
|
+
|
158
|
+
sig = Base64.strict_encode64(
|
159
|
+
hmacsha1(
|
160
|
+
secret_key,
|
161
|
+
"POST\n#{content_md5}\n\n#{date}\nx-amz-date:#{date}\n/evert"
|
162
|
+
)
|
163
|
+
)
|
164
|
+
|
165
|
+
@request.url = '/evert'
|
166
|
+
@request.method = 'POST'
|
167
|
+
@request.update_headers(
|
168
|
+
'Authorization' => "AWS #{access_key}:#{sig}",
|
169
|
+
'Content-MD5' => content_md5,
|
170
|
+
'X-amz-date' => date
|
171
|
+
)
|
172
|
+
|
173
|
+
@request.body = content
|
174
|
+
|
175
|
+
@auth.init
|
176
|
+
result = @auth.validate(secret_key)
|
177
|
+
|
178
|
+
assert(result, "Signature did not validate, got errorcode #{@auth.error_code}")
|
179
|
+
assert_equal(access_key, @auth.access_key)
|
180
|
+
end
|
181
|
+
|
182
|
+
def test401
|
183
|
+
@auth.require_login
|
184
|
+
header = @response.header('WWW-Authenticate') =~ /^AWS$/
|
185
|
+
assert(header, 'The WWW-Authenticate response didn\'t match our pattern')
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'base64'
|
2
|
+
require 'test_helper'
|
3
|
+
|
4
|
+
module Tilia
|
5
|
+
module Http
|
6
|
+
class BasicTest < Minitest::Test
|
7
|
+
def test_get_credentials
|
8
|
+
request = Request.new(
|
9
|
+
'GET',
|
10
|
+
'/',
|
11
|
+
'Authorization' => "Basic #{Base64.strict_encode64('user:pass:bla')}"
|
12
|
+
)
|
13
|
+
|
14
|
+
basic = Auth::Basic.new('Dagger', request, Response.new)
|
15
|
+
|
16
|
+
assert_equal(['user', 'pass:bla'], basic.credentials)
|
17
|
+
end
|
18
|
+
|
19
|
+
def test_get_invalid_credentials_colon_missing
|
20
|
+
request = Request.new(
|
21
|
+
'GET',
|
22
|
+
'/',
|
23
|
+
'Authorization' => "Basic #{Base64.strict_encode64('userpass')}"
|
24
|
+
)
|
25
|
+
|
26
|
+
basic = Auth::Basic.new('Dagger', request, Response.new)
|
27
|
+
|
28
|
+
assert_nil(basic.credentials)
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_credentials_noheader
|
32
|
+
request = Request.new('GET', '/', {})
|
33
|
+
basic = Auth::Basic.new('Dagger', request, Response.new)
|
34
|
+
|
35
|
+
assert_nil(basic.credentials)
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_credentials_not_basic
|
39
|
+
request = Request.new(
|
40
|
+
'GET',
|
41
|
+
'/',
|
42
|
+
'Authorization' => "QBasic #{Base64.strict_encode64('user:pass:bla')}"
|
43
|
+
)
|
44
|
+
basic = Auth::Basic.new('Dagger', request, Response.new)
|
45
|
+
|
46
|
+
assert_nil(basic.credentials)
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_require_login
|
50
|
+
response = Response.new
|
51
|
+
basic = Auth::Basic.new('Dagger', Request.new, response)
|
52
|
+
|
53
|
+
basic.require_login
|
54
|
+
|
55
|
+
assert_equal('Basic realm="Dagger"', response.header('WWW-Authenticate'))
|
56
|
+
assert_equal(401, response.status)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
module Tilia
|
4
|
+
module Http
|
5
|
+
class BearerTest < Minitest::Test
|
6
|
+
def test_get_token
|
7
|
+
request = Request.new(
|
8
|
+
'GET',
|
9
|
+
'/',
|
10
|
+
'Authorization' => 'Bearer 12345'
|
11
|
+
)
|
12
|
+
|
13
|
+
bearer = Auth::Bearer.new('Dagger', request, Response.new)
|
14
|
+
|
15
|
+
assert_equal('12345', bearer.token)
|
16
|
+
end
|
17
|
+
|
18
|
+
def test_get_credentials_noheader
|
19
|
+
request = Request.new('GET', '/', {})
|
20
|
+
bearer = Auth::Bearer.new('Dagger', request, Response.new)
|
21
|
+
|
22
|
+
assert_nil(bearer.token)
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_get_credentials_not_bearer
|
26
|
+
request = Request.new(
|
27
|
+
'GET',
|
28
|
+
'/',
|
29
|
+
'Authorization' => 'QBearer 12345'
|
30
|
+
)
|
31
|
+
bearer = Auth::Bearer.new('Dagger', request, Response.new)
|
32
|
+
|
33
|
+
assert_nil(bearer.token)
|
34
|
+
end
|
35
|
+
|
36
|
+
def test_require_login
|
37
|
+
response = Response.new
|
38
|
+
bearer = Auth::Bearer.new('Dagger', Request.new, response)
|
39
|
+
|
40
|
+
bearer.require_login
|
41
|
+
|
42
|
+
assert_equal('Bearer realm="Dagger"', response.header('WWW-Authenticate'))
|
43
|
+
assert_equal(401, response.status)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|