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.
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