rack 3.0.11 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of rack might be problematic. Click here for more details.

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
-