tilia-http 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +19 -0
  3. data/.rspec +1 -0
  4. data/.rubocop.yml +35 -0
  5. data/.simplecov +4 -0
  6. data/.travis.yml +3 -0
  7. data/CHANGELOG.sabre.md +235 -0
  8. data/CONTRIBUTING.md +25 -0
  9. data/Gemfile +18 -0
  10. data/Gemfile.lock +69 -0
  11. data/LICENSE +27 -0
  12. data/LICENSE.sabre +27 -0
  13. data/README.md +68 -0
  14. data/Rakefile +17 -0
  15. data/examples/asyncclient.rb +45 -0
  16. data/examples/basicauth.rb +39 -0
  17. data/examples/client.rb +20 -0
  18. data/examples/reverseproxy.rb +39 -0
  19. data/examples/stringify.rb +37 -0
  20. data/lib/tilia/http/auth/abstract_auth.rb +51 -0
  21. data/lib/tilia/http/auth/aws.rb +191 -0
  22. data/lib/tilia/http/auth/basic.rb +43 -0
  23. data/lib/tilia/http/auth/bearer.rb +37 -0
  24. data/lib/tilia/http/auth/digest.rb +187 -0
  25. data/lib/tilia/http/auth.rb +12 -0
  26. data/lib/tilia/http/client.rb +452 -0
  27. data/lib/tilia/http/client_exception.rb +15 -0
  28. data/lib/tilia/http/client_http_exception.rb +37 -0
  29. data/lib/tilia/http/http_exception.rb +21 -0
  30. data/lib/tilia/http/message.rb +241 -0
  31. data/lib/tilia/http/message_decorator_trait.rb +183 -0
  32. data/lib/tilia/http/message_interface.rb +154 -0
  33. data/lib/tilia/http/request.rb +235 -0
  34. data/lib/tilia/http/request_decorator.rb +160 -0
  35. data/lib/tilia/http/request_interface.rb +126 -0
  36. data/lib/tilia/http/response.rb +164 -0
  37. data/lib/tilia/http/response_decorator.rb +58 -0
  38. data/lib/tilia/http/response_interface.rb +36 -0
  39. data/lib/tilia/http/sapi.rb +165 -0
  40. data/lib/tilia/http/url_util.rb +70 -0
  41. data/lib/tilia/http/util.rb +51 -0
  42. data/lib/tilia/http/version.rb +9 -0
  43. data/lib/tilia/http.rb +416 -0
  44. data/test/http/auth/aws_test.rb +189 -0
  45. data/test/http/auth/basic_test.rb +60 -0
  46. data/test/http/auth/bearer_test.rb +47 -0
  47. data/test/http/auth/digest_test.rb +141 -0
  48. data/test/http/client_mock.rb +101 -0
  49. data/test/http/client_test.rb +331 -0
  50. data/test/http/message_decorator_test.rb +67 -0
  51. data/test/http/message_test.rb +163 -0
  52. data/test/http/request_decorator_test.rb +87 -0
  53. data/test/http/request_test.rb +132 -0
  54. data/test/http/response_decorator_test.rb +28 -0
  55. data/test/http/response_test.rb +38 -0
  56. data/test/http/sapi_mock.rb +12 -0
  57. data/test/http/sapi_test.rb +133 -0
  58. data/test/http/url_util_test.rb +155 -0
  59. data/test/http/util_test.rb +186 -0
  60. data/test/http_test.rb +102 -0
  61. data/test/test_helper.rb +6 -0
  62. data/tilia-http.gemspec +18 -0
  63. 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