tilia-http 4.1.0
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 +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
|