rack 3.0.11 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rack/utils.rb CHANGED
@@ -6,6 +6,7 @@ require 'fileutils'
6
6
  require 'set'
7
7
  require 'tempfile'
8
8
  require 'time'
9
+ require 'cgi/escape'
9
10
 
10
11
  require_relative 'query_parser'
11
12
  require_relative 'mime'
@@ -85,15 +86,6 @@ module Rack
85
86
  self.default_query_parser = self.default_query_parser.new_depth_limit(v)
86
87
  end
87
88
 
88
- def self.key_space_limit
89
- warn("`Rack::Utils.key_space_limit` is deprecated as this value no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
90
- 65536
91
- end
92
-
93
- def self.key_space_limit=(v)
94
- warn("`Rack::Utils.key_space_limit=` is deprecated and no longer has an effect. It will be removed in Rack 3.1", uplevel: 1)
95
- end
96
-
97
89
  if defined?(Process::CLOCK_MONOTONIC)
98
90
  def clock_time
99
91
  Process.clock_gettime(Process::CLOCK_MONOTONIC)
@@ -184,21 +176,8 @@ module Rack
184
176
  matches&.first
185
177
  end
186
178
 
187
- ESCAPE_HTML = {
188
- "&" => "&",
189
- "<" => "&lt;",
190
- ">" => "&gt;",
191
- "'" => "&#x27;",
192
- '"' => "&quot;",
193
- "/" => "&#x2F;"
194
- }
195
-
196
- ESCAPE_HTML_PATTERN = Regexp.union(*ESCAPE_HTML.keys)
197
-
198
179
  # Escape ampersands, brackets and quotes to their HTML/XML entities.
199
- def escape_html(string)
200
- string.to_s.gsub(ESCAPE_HTML_PATTERN){|c| ESCAPE_HTML[c] }
201
- end
180
+ define_method(:escape_html, CGI.method(:escapeHTML))
202
181
 
203
182
  def select_best_encoding(available_encodings, accept_encoding)
204
183
  # http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
@@ -252,21 +231,6 @@ module Rack
252
231
  end
253
232
  end
254
233
 
255
- def add_cookie_to_header(header, key, value)
256
- warn("add_cookie_to_header is deprecated and will be removed in Rack 3.1", uplevel: 1)
257
-
258
- case header
259
- when nil, ''
260
- return set_cookie_header(key, value)
261
- when String
262
- [header, set_cookie_header(key, value)]
263
- when Array
264
- header + [set_cookie_header(key, value)]
265
- else
266
- raise ArgumentError, "Unrecognized cookie header value. Expected String, Array, or nil, got #{header.inspect}"
267
- end
268
- end
269
-
270
234
  # :call-seq:
271
235
  # parse_cookies(env) -> hash
272
236
  #
@@ -280,6 +244,20 @@ module Rack
280
244
  parse_cookies_header env[HTTP_COOKIE]
281
245
  end
282
246
 
247
+ # A valid cookie key according to RFC2616.
248
+ # A <cookie-name> can be any US-ASCII characters, except control characters, spaces, or tabs. It also must not contain a separator character like the following: ( ) < > @ , ; : \ " / [ ] ? = { }.
249
+ VALID_COOKIE_KEY = /\A[!#$%&'*+\-\.\^_`|~0-9a-zA-Z]+\z/.freeze
250
+ private_constant :VALID_COOKIE_KEY
251
+
252
+ private def escape_cookie_key(key)
253
+ if key =~ VALID_COOKIE_KEY
254
+ key
255
+ else
256
+ warn "Cookie key #{key.inspect} is not valid according to RFC2616; it will be escaped. This behaviour is deprecated and will be removed in a future version of Rack.", uplevel: 2
257
+ escape(key)
258
+ end
259
+ end
260
+
283
261
  # :call-seq:
284
262
  # set_cookie_header(key, value) -> encoded string
285
263
  #
@@ -306,7 +284,7 @@ module Rack
306
284
  def set_cookie_header(key, value)
307
285
  case value
308
286
  when Hash
309
- key = escape(key) unless value[:escape_key] == false
287
+ key = escape_cookie_key(key) unless value[:escape_key] == false
310
288
  domain = "; domain=#{value[:domain]}" if value[:domain]
311
289
  path = "; path=#{value[:path]}" if value[:path]
312
290
  max_age = "; max-age=#{value[:max_age]}" if value[:max_age]
@@ -318,23 +296,24 @@ module Rack
318
296
  when false, nil
319
297
  nil
320
298
  when :none, 'None', :None
321
- '; SameSite=None'
299
+ '; samesite=none'
322
300
  when :lax, 'Lax', :Lax
323
- '; SameSite=Lax'
301
+ '; samesite=lax'
324
302
  when true, :strict, 'Strict', :Strict
325
- '; SameSite=Strict'
303
+ '; samesite=strict'
326
304
  else
327
- raise ArgumentError, "Invalid SameSite value: #{value[:same_site].inspect}"
305
+ raise ArgumentError, "Invalid :same_site value: #{value[:same_site].inspect}"
328
306
  end
307
+ partitioned = "; partitioned" if value[:partitioned]
329
308
  value = value[:value]
330
309
  else
331
- key = escape(key)
310
+ key = escape_cookie_key(key)
332
311
  end
333
312
 
334
313
  value = [value] unless Array === value
335
314
 
336
315
  return "#{key}=#{value.map { |v| escape v }.join('&')}#{domain}" \
337
- "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}"
316
+ "#{path}#{max_age}#{expires}#{secure}#{httponly}#{same_site}#{partitioned}"
338
317
  end
339
318
 
340
319
  # :call-seq:
@@ -375,24 +354,12 @@ module Rack
375
354
  set_cookie_header(key, value.merge(max_age: '0', expires: Time.at(0), value: ''))
376
355
  end
377
356
 
378
- def make_delete_cookie_header(header, key, value)
379
- warn("make_delete_cookie_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
380
-
381
- delete_set_cookie_header!(header, key, value)
382
- end
383
-
384
357
  def delete_cookie_header!(headers, key, value = {})
385
358
  headers[SET_COOKIE] = delete_set_cookie_header!(headers[SET_COOKIE], key, value)
386
359
 
387
360
  return nil
388
361
  end
389
362
 
390
- def add_remove_cookie_to_header(header, key, value = {})
391
- warn("add_remove_cookie_to_header is deprecated and will be removed in Rack 3.1, use delete_set_cookie_header! instead", uplevel: 1)
392
-
393
- delete_set_cookie_header!(header, key, value)
394
- end
395
-
396
363
  # :call-seq:
397
364
  # delete_set_cookie_header!(header, key, value = {}) -> header value
398
365
  #
@@ -435,6 +402,8 @@ module Rack
435
402
 
436
403
  def get_byte_ranges(http_range, size)
437
404
  # See <http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35>
405
+ # Ignore Range when file size is 0 to avoid a 416 error.
406
+ return nil if size.zero?
438
407
  return nil unless http_range && http_range =~ /bytes=([^;]+)/
439
408
  ranges = []
440
409
  $1.split(/,\s*/).each do |range_spec|
@@ -517,39 +486,12 @@ module Rack
517
486
  end
518
487
  end
519
488
 
520
- # A wrapper around Headers
521
- # header when set.
522
- #
523
- # @api private
524
- class HeaderHash < Hash # :nodoc:
525
- def self.[](headers)
526
- warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
527
- if headers.is_a?(Headers) && !headers.frozen?
528
- return headers
529
- end
530
-
531
- new_headers = Headers.new
532
- headers.each{|k,v| new_headers[k] = v}
533
- new_headers
534
- end
535
-
536
- def self.new(hash = {})
537
- warn "Rack::Utils::HeaderHash is deprecated and will be removed in Rack 3.1, switch to Rack::Headers", uplevel: 1
538
- headers = Headers.new
539
- hash.each{|k,v| headers[k] = v}
540
- headers
541
- end
542
-
543
- def self.allocate
544
- raise TypeError, "cannot allocate HeaderHash"
545
- end
546
- end
547
-
548
489
  # Every standard HTTP code mapped to the appropriate message.
549
490
  # Generated with:
550
- # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv | \
551
- # ruby -ne 'm = /^(\d{3}),(?!Unassigned|\(Unused\))([^,]+)/.match($_) and \
552
- # puts "#{m[1]} => \x27#{m[2].strip}\x27,"'
491
+ # curl -s https://www.iana.org/assignments/http-status-codes/http-status-codes-1.csv \
492
+ # | ruby -rcsv -e "puts CSV.parse(STDIN, headers: true) \
493
+ # .reject {|v| v['Description'] == 'Unassigned' or v['Description'].include? '(' } \
494
+ # .map {|v| %Q/#{v['Value']} => '#{v['Description']}'/ }.join(','+?\n)"
553
495
  HTTP_STATUS_CODES = {
554
496
  100 => 'Continue',
555
497
  101 => 'Switching Protocols',
@@ -571,7 +513,6 @@ module Rack
571
513
  303 => 'See Other',
572
514
  304 => 'Not Modified',
573
515
  305 => 'Use Proxy',
574
- 306 => '(Unused)',
575
516
  307 => 'Temporary Redirect',
576
517
  308 => 'Permanent Redirect',
577
518
  400 => 'Bad Request',
@@ -587,13 +528,13 @@ module Rack
587
528
  410 => 'Gone',
588
529
  411 => 'Length Required',
589
530
  412 => 'Precondition Failed',
590
- 413 => 'Payload Too Large',
531
+ 413 => 'Content Too Large',
591
532
  414 => 'URI Too Long',
592
533
  415 => 'Unsupported Media Type',
593
534
  416 => 'Range Not Satisfiable',
594
535
  417 => 'Expectation Failed',
595
536
  421 => 'Misdirected Request',
596
- 422 => 'Unprocessable Entity',
537
+ 422 => 'Unprocessable Content',
597
538
  423 => 'Locked',
598
539
  424 => 'Failed Dependency',
599
540
  425 => 'Too Early',
@@ -601,7 +542,7 @@ module Rack
601
542
  428 => 'Precondition Required',
602
543
  429 => 'Too Many Requests',
603
544
  431 => 'Request Header Fields Too Large',
604
- 451 => 'Unavailable for Legal Reasons',
545
+ 451 => 'Unavailable For Legal Reasons',
605
546
  500 => 'Internal Server Error',
606
547
  501 => 'Not Implemented',
607
548
  502 => 'Bad Gateway',
@@ -611,8 +552,6 @@ module Rack
611
552
  506 => 'Variant Also Negotiates',
612
553
  507 => 'Insufficient Storage',
613
554
  508 => 'Loop Detected',
614
- 509 => 'Bandwidth Limit Exceeded',
615
- 510 => 'Not Extended',
616
555
  511 => 'Network Authentication Required'
617
556
  }
618
557
 
@@ -620,12 +559,34 @@ module Rack
620
559
  STATUS_WITH_NO_ENTITY_BODY = Hash[((100..199).to_a << 204 << 304).product([true])]
621
560
 
622
561
  SYMBOL_TO_STATUS_CODE = Hash[*HTTP_STATUS_CODES.map { |code, message|
623
- [message.downcase.gsub(/\s|-|'/, '_').to_sym, code]
562
+ [message.downcase.gsub(/\s|-/, '_').to_sym, code]
624
563
  }.flatten]
625
564
 
565
+ OBSOLETE_SYMBOLS_TO_STATUS_CODES = {
566
+ payload_too_large: 413,
567
+ unprocessable_entity: 422,
568
+ bandwidth_limit_exceeded: 509,
569
+ not_extended: 510
570
+ }.freeze
571
+ private_constant :OBSOLETE_SYMBOLS_TO_STATUS_CODES
572
+
573
+ OBSOLETE_SYMBOL_MAPPINGS = {
574
+ payload_too_large: :content_too_large,
575
+ unprocessable_entity: :unprocessable_content
576
+ }.freeze
577
+ private_constant :OBSOLETE_SYMBOL_MAPPINGS
578
+
626
579
  def status_code(status)
627
580
  if status.is_a?(Symbol)
628
- SYMBOL_TO_STATUS_CODE.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
581
+ SYMBOL_TO_STATUS_CODE.fetch(status) do
582
+ fallback_code = OBSOLETE_SYMBOLS_TO_STATUS_CODES.fetch(status) { raise ArgumentError, "Unrecognized status code #{status.inspect}" }
583
+ message = "Status code #{status.inspect} is deprecated and will be removed in a future version of Rack."
584
+ if canonical_symbol = OBSOLETE_SYMBOL_MAPPINGS[status]
585
+ message = "#{message} Please use #{canonical_symbol.inspect} instead."
586
+ end
587
+ warn message, uplevel: 1
588
+ fallback_code
589
+ end
629
590
  else
630
591
  status.to_i
631
592
  end
data/lib/rack/version.rb CHANGED
@@ -12,20 +12,7 @@
12
12
  # so it should be enough just to <tt>require 'rack'</tt> in your code.
13
13
 
14
14
  module Rack
15
- # The Rack protocol version number implemented.
16
- VERSION = [1, 3].freeze
17
- deprecate_constant :VERSION
18
-
19
- VERSION_STRING = "1.3".freeze
20
- deprecate_constant :VERSION_STRING
21
-
22
- # The Rack protocol version number implemented.
23
- def self.version
24
- warn "Rack.version is deprecated and will be removed in Rack 3.1!", uplevel: 1
25
- VERSION
26
- end
27
-
28
- RELEASE = "3.0.11"
15
+ RELEASE = "3.1.0"
29
16
 
30
17
  # Return the Rack release as a dotted string.
31
18
  def self.release
data/lib/rack.rb CHANGED
@@ -15,23 +15,21 @@ require_relative 'rack/version'
15
15
  require_relative 'rack/constants'
16
16
 
17
17
  module Rack
18
- autoload :Builder, "rack/builder"
18
+ autoload :BadRequest, "rack/bad_request"
19
19
  autoload :BodyProxy, "rack/body_proxy"
20
+ autoload :Builder, "rack/builder"
20
21
  autoload :Cascade, "rack/cascade"
21
- autoload :Chunked, "rack/chunked"
22
22
  autoload :CommonLogger, "rack/common_logger"
23
23
  autoload :ConditionalGet, "rack/conditional_get"
24
24
  autoload :Config, "rack/config"
25
25
  autoload :ContentLength, "rack/content_length"
26
26
  autoload :ContentType, "rack/content_type"
27
+ autoload :Deflater, "rack/deflater"
28
+ autoload :Directory, "rack/directory"
27
29
  autoload :ETag, "rack/etag"
28
30
  autoload :Events, "rack/events"
29
- autoload :File, "rack/file"
30
31
  autoload :Files, "rack/files"
31
- autoload :Deflater, "rack/deflater"
32
- autoload :Directory, "rack/directory"
33
32
  autoload :ForwardRequest, "rack/recursive"
34
- autoload :Handler, "rack/handler"
35
33
  autoload :Head, "rack/head"
36
34
  autoload :Headers, "rack/headers"
37
35
  autoload :Lint, "rack/lint"
@@ -40,32 +38,28 @@ module Rack
40
38
  autoload :MediaType, "rack/media_type"
41
39
  autoload :MethodOverride, "rack/method_override"
42
40
  autoload :Mime, "rack/mime"
41
+ autoload :MockRequest, "rack/mock_request"
42
+ autoload :MockResponse, "rack/mock_response"
43
+ autoload :Multipart, "rack/multipart"
43
44
  autoload :NullLogger, "rack/null_logger"
44
45
  autoload :QueryParser, "rack/query_parser"
45
46
  autoload :Recursive, "rack/recursive"
46
47
  autoload :Reloader, "rack/reloader"
48
+ autoload :Request, "rack/request"
49
+ autoload :Response, "rack/response"
47
50
  autoload :RewindableInput, "rack/rewindable_input"
48
51
  autoload :Runtime, "rack/runtime"
49
52
  autoload :Sendfile, "rack/sendfile"
50
- autoload :Server, "rack/server"
51
53
  autoload :ShowExceptions, "rack/show_exceptions"
52
54
  autoload :ShowStatus, "rack/show_status"
53
55
  autoload :Static, "rack/static"
54
56
  autoload :TempfileReaper, "rack/tempfile_reaper"
55
57
  autoload :URLMap, "rack/urlmap"
56
58
  autoload :Utils, "rack/utils"
57
- autoload :Multipart, "rack/multipart"
58
-
59
- autoload :MockRequest, "rack/mock_request"
60
- autoload :MockResponse, "rack/mock_response"
61
-
62
- autoload :Request, "rack/request"
63
- autoload :Response, "rack/response"
64
59
 
65
60
  module Auth
66
61
  autoload :Basic, "rack/auth/basic"
67
- autoload :AbstractRequest, "rack/auth/abstract/request"
68
62
  autoload :AbstractHandler, "rack/auth/abstract/handler"
69
- autoload :Digest, "rack/auth/digest"
63
+ autoload :AbstractRequest, "rack/auth/abstract/request"
70
64
  end
71
65
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.11
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Leah Neukirchen
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2024-05-09 00:00:00.000000000 Z
11
+ date: 2024-06-11 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: minitest
@@ -89,15 +89,10 @@ files:
89
89
  - lib/rack/auth/abstract/handler.rb
90
90
  - lib/rack/auth/abstract/request.rb
91
91
  - lib/rack/auth/basic.rb
92
- - lib/rack/auth/digest.rb
93
- - lib/rack/auth/digest/md5.rb
94
- - lib/rack/auth/digest/nonce.rb
95
- - lib/rack/auth/digest/params.rb
96
- - lib/rack/auth/digest/request.rb
92
+ - lib/rack/bad_request.rb
97
93
  - lib/rack/body_proxy.rb
98
94
  - lib/rack/builder.rb
99
95
  - lib/rack/cascade.rb
100
- - lib/rack/chunked.rb
101
96
  - lib/rack/common_logger.rb
102
97
  - lib/rack/conditional_get.rb
103
98
  - lib/rack/config.rb
@@ -108,7 +103,6 @@ files:
108
103
  - lib/rack/directory.rb
109
104
  - lib/rack/etag.rb
110
105
  - lib/rack/events.rb
111
- - lib/rack/file.rb
112
106
  - lib/rack/files.rb
113
107
  - lib/rack/head.rb
114
108
  - lib/rack/headers.rb
@@ -164,7 +158,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
164
158
  - !ruby/object:Gem::Version
165
159
  version: '0'
166
160
  requirements: []
167
- rubygems_version: 3.5.3
161
+ rubygems_version: 3.5.9
168
162
  signing_key:
169
163
  specification_version: 4
170
164
  summary: A modular Ruby webserver interface.
@@ -1 +0,0 @@
1
- require_relative '../digest'
@@ -1 +0,0 @@
1
- require_relative '../digest'
@@ -1 +0,0 @@
1
- require_relative '../digest'
@@ -1 +0,0 @@
1
- require_relative '../digest'
@@ -1,256 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'abstract/handler'
4
- require_relative 'abstract/request'
5
- require 'digest/md5'
6
- require 'base64'
7
-
8
- module Rack
9
- warn "Rack::Auth::Digest is deprecated and will be removed in Rack 3.1", uplevel: 1
10
-
11
- module Auth
12
- module Digest
13
- # Rack::Auth::Digest::Nonce is the default nonce generator for the
14
- # Rack::Auth::Digest::MD5 authentication handler.
15
- #
16
- # +private_key+ needs to set to a constant string.
17
- #
18
- # +time_limit+ can be optionally set to an integer (number of seconds),
19
- # to limit the validity of the generated nonces.
20
-
21
- class Nonce
22
-
23
- class << self
24
- attr_accessor :private_key, :time_limit
25
- end
26
-
27
- def self.parse(string)
28
- new(*Base64.decode64(string).split(' ', 2))
29
- end
30
-
31
- def initialize(timestamp = Time.now, given_digest = nil)
32
- @timestamp, @given_digest = timestamp.to_i, given_digest
33
- end
34
-
35
- def to_s
36
- Base64.encode64("#{@timestamp} #{digest}").strip
37
- end
38
-
39
- def digest
40
- ::Digest::MD5.hexdigest("#{@timestamp}:#{self.class.private_key}")
41
- end
42
-
43
- def valid?
44
- digest == @given_digest
45
- end
46
-
47
- def stale?
48
- !self.class.time_limit.nil? && (Time.now.to_i - @timestamp) > self.class.time_limit
49
- end
50
-
51
- def fresh?
52
- !stale?
53
- end
54
-
55
- end
56
-
57
- class Params < Hash
58
-
59
- def self.parse(str)
60
- Params[*split_header_value(str).map do |param|
61
- k, v = param.split('=', 2)
62
- [k, dequote(v)]
63
- end.flatten]
64
- end
65
-
66
- def self.dequote(str) # From WEBrick::HTTPUtils
67
- ret = (/\A"(.*)"\Z/ =~ str) ? $1 : str.dup
68
- ret.gsub!(/\\(.)/, "\\1")
69
- ret
70
- end
71
-
72
- def self.split_header_value(str)
73
- str.scan(/\w+\=(?:"[^\"]+"|[^,]+)/n)
74
- end
75
-
76
- def initialize
77
- super()
78
-
79
- yield self if block_given?
80
- end
81
-
82
- def [](k)
83
- super k.to_s
84
- end
85
-
86
- def []=(k, v)
87
- super k.to_s, v.to_s
88
- end
89
-
90
- UNQUOTED = ['nc', 'stale']
91
-
92
- def to_s
93
- map do |k, v|
94
- "#{k}=#{(UNQUOTED.include?(k) ? v.to_s : quote(v))}"
95
- end.join(', ')
96
- end
97
-
98
- def quote(str) # From WEBrick::HTTPUtils
99
- '"' + str.gsub(/[\\\"]/o, "\\\1") + '"'
100
- end
101
-
102
- end
103
-
104
- class Request < Auth::AbstractRequest
105
- def method
106
- @env[RACK_METHODOVERRIDE_ORIGINAL_METHOD] || @env[REQUEST_METHOD]
107
- end
108
-
109
- def digest?
110
- "digest" == scheme
111
- end
112
-
113
- def correct_uri?
114
- request.fullpath == uri
115
- end
116
-
117
- def nonce
118
- @nonce ||= Nonce.parse(params['nonce'])
119
- end
120
-
121
- def params
122
- @params ||= Params.parse(parts.last)
123
- end
124
-
125
- def respond_to?(sym, *)
126
- super or params.has_key? sym.to_s
127
- end
128
-
129
- def method_missing(sym, *args)
130
- return super unless params.has_key?(key = sym.to_s)
131
- return params[key] if args.size == 0
132
- raise ArgumentError, "wrong number of arguments (#{args.size} for 0)"
133
- end
134
- end
135
-
136
- # Rack::Auth::Digest::MD5 implements the MD5 algorithm version of
137
- # HTTP Digest Authentication, as per RFC 2617.
138
- #
139
- # Initialize with the [Rack] application that you want protecting,
140
- # and a block that looks up a plaintext password for a given username.
141
- #
142
- # +opaque+ needs to be set to a constant base64/hexadecimal string.
143
- #
144
- class MD5 < AbstractHandler
145
-
146
- attr_accessor :opaque
147
-
148
- attr_writer :passwords_hashed
149
-
150
- def initialize(app, realm = nil, opaque = nil, &authenticator)
151
- @passwords_hashed = nil
152
- if opaque.nil? and realm.respond_to? :values_at
153
- realm, opaque, @passwords_hashed = realm.values_at :realm, :opaque, :passwords_hashed
154
- end
155
- super(app, realm, &authenticator)
156
- @opaque = opaque
157
- end
158
-
159
- def passwords_hashed?
160
- !!@passwords_hashed
161
- end
162
-
163
- def call(env)
164
- auth = Request.new(env)
165
-
166
- unless auth.provided?
167
- return unauthorized
168
- end
169
-
170
- if !auth.digest? || !auth.correct_uri? || !valid_qop?(auth)
171
- return bad_request
172
- end
173
-
174
- if valid?(auth)
175
- if auth.nonce.stale?
176
- return unauthorized(challenge(stale: true))
177
- else
178
- env['REMOTE_USER'] = auth.username
179
-
180
- return @app.call(env)
181
- end
182
- end
183
-
184
- unauthorized
185
- end
186
-
187
-
188
- private
189
-
190
- QOP = 'auth'
191
-
192
- def params(hash = {})
193
- Params.new do |params|
194
- params['realm'] = realm
195
- params['nonce'] = Nonce.new.to_s
196
- params['opaque'] = H(opaque)
197
- params['qop'] = QOP
198
-
199
- hash.each { |k, v| params[k] = v }
200
- end
201
- end
202
-
203
- def challenge(hash = {})
204
- "Digest #{params(hash)}"
205
- end
206
-
207
- def valid?(auth)
208
- valid_opaque?(auth) && valid_nonce?(auth) && valid_digest?(auth)
209
- end
210
-
211
- def valid_qop?(auth)
212
- QOP == auth.qop
213
- end
214
-
215
- def valid_opaque?(auth)
216
- H(opaque) == auth.opaque
217
- end
218
-
219
- def valid_nonce?(auth)
220
- auth.nonce.valid?
221
- end
222
-
223
- def valid_digest?(auth)
224
- pw = @authenticator.call(auth.username)
225
- pw && Rack::Utils.secure_compare(digest(auth, pw), auth.response)
226
- end
227
-
228
- def md5(data)
229
- ::Digest::MD5.hexdigest(data)
230
- end
231
-
232
- alias :H :md5
233
-
234
- def KD(secret, data)
235
- H "#{secret}:#{data}"
236
- end
237
-
238
- def A1(auth, password)
239
- "#{auth.username}:#{auth.realm}:#{password}"
240
- end
241
-
242
- def A2(auth)
243
- "#{auth.method}:#{auth.uri}"
244
- end
245
-
246
- def digest(auth, password)
247
- password_hash = passwords_hashed? ? password : H(A1(auth, password))
248
-
249
- KD password_hash, "#{auth.nonce}:#{auth.nc}:#{auth.cnonce}:#{QOP}:#{H A2(auth)}"
250
- end
251
-
252
- end
253
- end
254
- end
255
- end
256
-