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/lint.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'forwardable'
4
+ require 'uri'
4
5
 
5
6
  require_relative 'constants'
6
7
  require_relative 'utils'
@@ -10,6 +11,11 @@ module Rack
10
11
  # responses according to the Rack spec.
11
12
 
12
13
  class Lint
14
+ REQUEST_PATH_ORIGIN_FORM = /\A\/[^#]*\z/
15
+ REQUEST_PATH_ABSOLUTE_FORM = /\A#{URI::DEFAULT_PARSER.make_regexp}\z/
16
+ REQUEST_PATH_AUTHORITY_FORM = /\A(.*?)(:\d*)\z/
17
+ REQUEST_PATH_ASTERISK_FORM = '*'
18
+
13
19
  def initialize(app)
14
20
  @app = app
15
21
  end
@@ -56,9 +62,6 @@ module Rack
56
62
  raise LintError, "No env given" unless @env
57
63
  check_environment(@env)
58
64
 
59
- @env[RACK_INPUT] = InputWrapper.new(@env[RACK_INPUT])
60
- @env[RACK_ERRORS] = ErrorWrapper.new(@env[RACK_ERRORS])
61
-
62
65
  ## and returns a non-frozen Array of exactly three values:
63
66
  @response = @app.call(@env)
64
67
  raise LintError, "response is not an Array, but #{@response.class}" unless @response.kind_of? Array
@@ -78,8 +81,9 @@ module Rack
78
81
  end
79
82
 
80
83
  ## and the *body*.
81
- check_content_type(@status, @headers)
82
- check_content_length(@status, @headers)
84
+ check_content_type_header(@status, @headers)
85
+ check_content_length_header(@status, @headers)
86
+ check_rack_protocol_header(@status, @headers)
83
87
  @head_request = @env[REQUEST_METHOD] == HEAD
84
88
 
85
89
  @lint = (@env['rack.lint'] ||= []) << self
@@ -179,6 +183,16 @@ module Rack
179
183
  ## to +call+ that is used to perform a full
180
184
  ## hijack.
181
185
 
186
+ ## <tt>rack.protocol</tt>:: An optional +Array+ of +String+, containing
187
+ ## the protocols advertised by the client in
188
+ ## the +upgrade+ header (HTTP/1) or the
189
+ ## +:protocol+ pseudo-header (HTTP/2).
190
+ if protocols = @env['rack.protocol']
191
+ unless protocols.is_a?(Array) && protocols.all?{|protocol| protocol.is_a?(String)}
192
+ raise LintError, "rack.protocol must be an Array of Strings"
193
+ end
194
+ end
195
+
182
196
  ## Additional environment specifications have approved to
183
197
  ## standardized middleware APIs. None of these are required to
184
198
  ## be implemented by the server.
@@ -265,11 +279,9 @@ module Rack
265
279
  ## is reserved for use with the Rack core distribution and other
266
280
  ## accepted specifications and must not be used otherwise.
267
281
  ##
268
-
269
- %w[REQUEST_METHOD SERVER_NAME QUERY_STRING SERVER_PROTOCOL
270
- rack.input rack.errors].each { |header|
282
+ %w[REQUEST_METHOD SERVER_NAME QUERY_STRING SERVER_PROTOCOL rack.errors].each do |header|
271
283
  raise LintError, "env missing required key #{header}" unless env.include? header
272
- }
284
+ end
273
285
 
274
286
  ## The <tt>SERVER_PORT</tt> must be an Integer if set.
275
287
  server_port = env["SERVER_PORT"]
@@ -293,11 +305,6 @@ module Rack
293
305
  raise LintError, "env[SERVER_PROTOCOL] does not match HTTP/\\d(\\.\\d)?"
294
306
  end
295
307
 
296
- ## If the <tt>HTTP_VERSION</tt> is present, it must equal the <tt>SERVER_PROTOCOL</tt>.
297
- if env['HTTP_VERSION'] && env['HTTP_VERSION'] != server_protocol
298
- raise LintError, "env[HTTP_VERSION] does not equal env[SERVER_PROTOCOL]"
299
- end
300
-
301
308
  ## The environment must not contain the keys
302
309
  ## <tt>HTTP_CONTENT_TYPE</tt> or <tt>HTTP_CONTENT_LENGTH</tt>
303
310
  ## (use the versions without <tt>HTTP_</tt>).
@@ -328,12 +335,21 @@ module Rack
328
335
  raise LintError, "rack.url_scheme unknown: #{env[RACK_URL_SCHEME].inspect}"
329
336
  end
330
337
 
331
- ## * There must be a valid input stream in <tt>rack.input</tt>.
332
- check_input env[RACK_INPUT]
338
+ ## * There may be a valid input stream in <tt>rack.input</tt>.
339
+ if rack_input = env[RACK_INPUT]
340
+ check_input_stream(rack_input)
341
+ @env[RACK_INPUT] = InputWrapper.new(rack_input)
342
+ end
343
+
333
344
  ## * There must be a valid error stream in <tt>rack.errors</tt>.
334
- check_error env[RACK_ERRORS]
345
+ rack_errors = env[RACK_ERRORS]
346
+ check_error_stream(rack_errors)
347
+ @env[RACK_ERRORS] = ErrorWrapper.new(rack_errors)
348
+
335
349
  ## * There may be a valid hijack callback in <tt>rack.hijack</tt>
336
350
  check_hijack env
351
+ ## * There may be a valid early hints callback in <tt>rack.early_hints</tt>
352
+ check_early_hints env
337
353
 
338
354
  ## * The <tt>REQUEST_METHOD</tt> must be a valid token.
339
355
  unless env[REQUEST_METHOD] =~ /\A[0-9A-Za-z!\#$%&'*+.^_`|~-]+\z/
@@ -344,10 +360,32 @@ module Rack
344
360
  if env.include?(SCRIPT_NAME) && env[SCRIPT_NAME] != "" && env[SCRIPT_NAME] !~ /\A\//
345
361
  raise LintError, "SCRIPT_NAME must start with /"
346
362
  end
347
- ## * The <tt>PATH_INFO</tt>, if non-empty, must start with <tt>/</tt>
348
- if env.include?(PATH_INFO) && env[PATH_INFO] != "" && env[PATH_INFO] !~ /\A\//
349
- raise LintError, "PATH_INFO must start with /"
363
+
364
+ ## * The <tt>PATH_INFO</tt>, if provided, must be a valid request target.
365
+ if env.include?(PATH_INFO)
366
+ case env[PATH_INFO]
367
+ when REQUEST_PATH_ASTERISK_FORM
368
+ ## * Only <tt>OPTIONS</tt> requests may have <tt>PATH_INFO</tt> set to <tt>*</tt> (asterisk-form).
369
+ unless env[REQUEST_METHOD] == OPTIONS
370
+ raise LintError, "Only OPTIONS requests may have PATH_INFO set to '*' (asterisk-form)"
371
+ end
372
+ when REQUEST_PATH_AUTHORITY_FORM
373
+ ## * Only <tt>CONNECT</tt> requests may have <tt>PATH_INFO</tt> set to an authority (authority-form). Note that in HTTP/2+, the authority-form is not a valid request target.
374
+ unless env[REQUEST_METHOD] == CONNECT
375
+ raise LintError, "Only CONNECT requests may have PATH_INFO set to an authority (authority-form)"
376
+ end
377
+ when REQUEST_PATH_ABSOLUTE_FORM
378
+ ## * <tt>CONNECT</tt> and <tt>OPTIONS</tt> requests must not have <tt>PATH_INFO</tt> set to a URI (absolute-form).
379
+ if env[REQUEST_METHOD] == CONNECT || env[REQUEST_METHOD] == OPTIONS
380
+ raise LintError, "CONNECT and OPTIONS requests must not have PATH_INFO set to a URI (absolute-form)"
381
+ end
382
+ when REQUEST_PATH_ORIGIN_FORM
383
+ ## * Otherwise, <tt>PATH_INFO</tt> must start with a <tt>/</tt> and must not include a fragment part starting with '#' (origin-form).
384
+ else
385
+ raise LintError, "PATH_INFO must start with a '/' and must not include a fragment part starting with '#' (origin-form)"
386
+ end
350
387
  end
388
+
351
389
  ## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
352
390
  if env.include?("CONTENT_LENGTH") && env["CONTENT_LENGTH"] !~ /\A\d+\z/
353
391
  raise LintError, "Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}"
@@ -384,9 +422,9 @@ module Rack
384
422
  ##
385
423
  ## The input stream is an IO-like object which contains the raw HTTP
386
424
  ## POST data.
387
- def check_input(input)
425
+ def check_input_stream(input)
388
426
  ## When applicable, its external encoding must be "ASCII-8BIT" and it
389
- ## must be opened in binary mode, for Ruby 1.9 compatibility.
427
+ ## must be opened in binary mode.
390
428
  if input.respond_to?(:external_encoding) && input.external_encoding != Encoding::ASCII_8BIT
391
429
  raise LintError, "rack.input #{input} does not have ASCII-8BIT as its external encoding"
392
430
  end
@@ -418,7 +456,7 @@ module Rack
418
456
  v
419
457
  end
420
458
 
421
- ## * +read+ behaves like IO#read.
459
+ ## * +read+ behaves like <tt>IO#read</tt>.
422
460
  ## Its signature is <tt>read([length, [buffer]])</tt>.
423
461
  ##
424
462
  ## If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
@@ -478,8 +516,8 @@ module Rack
478
516
  }
479
517
  end
480
518
 
481
- ## * +close+ can be called on the input stream to indicate that the
482
- ## any remaining input is not needed.
519
+ ## * +close+ can be called on the input stream to indicate that
520
+ ## any remaining input is not needed.
483
521
  def close(*args)
484
522
  @input.close(*args)
485
523
  end
@@ -488,7 +526,7 @@ module Rack
488
526
  ##
489
527
  ## === The Error Stream
490
528
  ##
491
- def check_error(error)
529
+ def check_error_stream(error)
492
530
  ## The error stream must respond to +puts+, +write+ and +flush+.
493
531
  [:puts, :write, :flush].each { |method|
494
532
  unless error.respond_to? method
@@ -609,6 +647,30 @@ module Rack
609
647
  nil
610
648
  end
611
649
 
650
+ ##
651
+ ## === Early Hints
652
+ ##
653
+ ## The application or any middleware may call the <tt>rack.early_hints</tt>
654
+ ## with an object which would be valid as the headers of a Rack response.
655
+ def check_early_hints(env)
656
+ if env[RACK_EARLY_HINTS]
657
+ ##
658
+ ## If <tt>rack.early_hints</tt> is present, it must respond to #call.
659
+ unless env[RACK_EARLY_HINTS].respond_to?(:call)
660
+ raise LintError, "rack.early_hints must respond to call"
661
+ end
662
+
663
+ original_callback = env[RACK_EARLY_HINTS]
664
+ env[RACK_EARLY_HINTS] = lambda do |headers|
665
+ ## If <tt>rack.early_hints</tt> is called, it must be called with
666
+ ## valid Rack response headers.
667
+ check_headers(headers)
668
+ original_callback.call(headers)
669
+ end
670
+ end
671
+ end
672
+
673
+ ##
612
674
  ## == The Response
613
675
  ##
614
676
  ## === The Status
@@ -672,9 +734,9 @@ module Rack
672
734
  end
673
735
 
674
736
  ##
675
- ## === The content-type
737
+ ## ==== The +content-type+ Header
676
738
  ##
677
- def check_content_type(status, headers)
739
+ def check_content_type_header(status, headers)
678
740
  headers.each { |key, value|
679
741
  ## There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx,
680
742
  ## 204, or 304.
@@ -688,9 +750,9 @@ module Rack
688
750
  end
689
751
 
690
752
  ##
691
- ## === The content-length
753
+ ## ==== The +content-length+ Header
692
754
  ##
693
- def check_content_length(status, headers)
755
+ def check_content_length_header(status, headers)
694
756
  headers.each { |key, value|
695
757
  if key == 'content-length'
696
758
  ## There must not be a <tt>content-length</tt> header key when the
@@ -715,6 +777,29 @@ module Rack
715
777
  end
716
778
  end
717
779
 
780
+ ##
781
+ ## ==== The +rack.protocol+ Header
782
+ ##
783
+ def check_rack_protocol_header(status, headers)
784
+ ## If the +rack.protocol+ header is present, it must be a +String+, and
785
+ ## must be one of the values from the +rack.protocol+ array from the
786
+ ## environment.
787
+ protocol = headers['rack.protocol']
788
+
789
+ if protocol
790
+ request_protocols = @env['rack.protocol']
791
+
792
+ if request_protocols.nil?
793
+ raise LintError, "rack.protocol header is #{protocol.inspect}, but rack.protocol was not set in request!"
794
+ elsif !request_protocols.include?(protocol)
795
+ raise LintError, "rack.protocol header is #{protocol.inspect}, but should be one of #{request_protocols.inspect} from the request!"
796
+ end
797
+ end
798
+ end
799
+ ##
800
+ ## Setting this value informs the server that it should perform a
801
+ ## connection upgrade. In HTTP/1, this is done using the +upgrade+
802
+ ## header. In HTTP/2, this is done by accepting the request.
718
803
  ##
719
804
  ## === The Body
720
805
  ##
@@ -782,7 +867,7 @@ module Rack
782
867
  ## It must only be called once.
783
868
  raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil?
784
869
 
785
- ## It must not be called after being closed.
870
+ ## It must not be called after being closed,
786
871
  raise LintError, "Response body is already closed" if @closed
787
872
 
788
873
  @invoked = :each
@@ -793,9 +878,6 @@ module Rack
793
878
  raise LintError, "Body yielded non-string value #{chunk.inspect}"
794
879
  end
795
880
 
796
- ##
797
- ## The Body itself should not be an instance of String, as this will
798
- ## break in Ruby 1.9.
799
881
  ##
800
882
  ## Middleware must not call +each+ directly on the Body.
801
883
  ## Instead, middleware can return a new Body that calls +each+ on the
data/lib/rack/logger.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'logger'
4
-
5
4
  require_relative 'constants'
6
5
 
6
+ warn "Rack::Logger is deprecated and will be removed in Rack 3.2.", uplevel: 1
7
+
7
8
  module Rack
8
9
  # Sets up rack.logger to write to rack.errors stream
9
10
  class Logger
data/lib/rack/mime.rb CHANGED
@@ -290,7 +290,7 @@ module Rack
290
290
  ".jpg" => "image/jpeg",
291
291
  ".jpgv" => "video/jpeg",
292
292
  ".jpm" => "video/jpm",
293
- ".js" => "application/javascript",
293
+ ".js" => "text/javascript",
294
294
  ".json" => "application/json",
295
295
  ".karbon" => "application/vnd.kde.karbon",
296
296
  ".kfo" => "application/vnd.kde.kformula",
@@ -338,6 +338,7 @@ module Rack
338
338
  ".mif" => "application/vnd.mif",
339
339
  ".mime" => "message/rfc822",
340
340
  ".mj2" => "video/mj2",
341
+ ".mjs" => "text/javascript",
341
342
  ".mlp" => "application/vnd.dolby.mlp",
342
343
  ".mmd" => "application/vnd.chipnuts.karaoke-mmd",
343
344
  ".mmf" => "application/vnd.smaf",
@@ -409,7 +410,7 @@ module Rack
409
410
  ".ogx" => "application/ogg",
410
411
  ".org" => "application/vnd.lotus-organizer",
411
412
  ".otc" => "application/vnd.oasis.opendocument.chart-template",
412
- ".otf" => "application/vnd.oasis.opendocument.formula-template",
413
+ ".otf" => "font/otf",
413
414
  ".otg" => "application/vnd.oasis.opendocument.graphics-template",
414
415
  ".oth" => "application/vnd.oasis.opendocument.text-web",
415
416
  ".oti" => "application/vnd.oasis.opendocument.image-template",
@@ -590,7 +591,7 @@ module Rack
590
591
  ".trm" => "application/x-msterminal",
591
592
  ".ts" => "video/mp2t",
592
593
  ".tsv" => "text/tab-separated-values",
593
- ".ttf" => "application/octet-stream",
594
+ ".ttf" => "font/ttf",
594
595
  ".twd" => "application/vnd.simtech-mindmapper",
595
596
  ".txd" => "application/vnd.genomatix.tuxedo",
596
597
  ".txf" => "application/vnd.mobius.txf",
@@ -636,8 +637,8 @@ module Rack
636
637
  ".wmv" => "video/x-ms-wmv",
637
638
  ".wmx" => "video/x-ms-wmx",
638
639
  ".wmz" => "application/x-ms-wmz",
639
- ".woff" => "application/font-woff",
640
- ".woff2" => "application/font-woff2",
640
+ ".woff" => "font/woff",
641
+ ".woff2" => "font/woff2",
641
642
  ".wpd" => "application/vnd.wordperfect",
642
643
  ".wpl" => "application/vnd.ms-wpl",
643
644
  ".wps" => "application/vnd.ms-works",
@@ -41,11 +41,6 @@ module Rack
41
41
  end
42
42
  end
43
43
 
44
- DEFAULT_ENV = {
45
- RACK_INPUT => StringIO.new,
46
- RACK_ERRORS => StringIO.new,
47
- }.freeze
48
-
49
44
  def initialize(app)
50
45
  @app = app
51
46
  end
@@ -104,7 +99,7 @@ module Rack
104
99
  uri = parse_uri_rfc2396(uri)
105
100
  uri.path = "/#{uri.path}" unless uri.path[0] == ?/
106
101
 
107
- env = DEFAULT_ENV.dup
102
+ env = {}
108
103
 
109
104
  env[REQUEST_METHOD] = (opts[:method] ? opts[:method].to_s.upcase : GET).b
110
105
  env[SERVER_NAME] = (uri.host || "example.org").b
@@ -144,20 +139,30 @@ module Rack
144
139
  end
145
140
  end
146
141
 
147
- opts[:input] ||= String.new
148
- if String === opts[:input]
149
- rack_input = StringIO.new(opts[:input])
142
+ input = opts[:input]
143
+ if String === input
144
+ rack_input = StringIO.new(input)
145
+ rack_input.set_encoding(Encoding::BINARY)
150
146
  else
151
- rack_input = opts[:input]
147
+ if input.respond_to?(:encoding) && input.encoding != Encoding::BINARY
148
+ warn "input encoding not binary", uplevel: 1
149
+ if input.respond_to?(:set_encoding)
150
+ input.set_encoding(Encoding::BINARY)
151
+ else
152
+ raise ArgumentError, "could not coerce input to binary encoding"
153
+ end
154
+ end
155
+ rack_input = input
152
156
  end
153
157
 
154
- rack_input.set_encoding(Encoding::BINARY)
155
- env[RACK_INPUT] = rack_input
158
+ if rack_input
159
+ env[RACK_INPUT] = rack_input
156
160
 
157
- env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
161
+ env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
162
+ end
158
163
 
159
164
  opts.each { |field, value|
160
- env[field] = value if String === field
165
+ env[field] = value if String === field
161
166
  }
162
167
 
163
168
  env
@@ -80,20 +80,18 @@ module Rack
80
80
  cookies = Hash.new
81
81
  if headers.has_key? 'set-cookie'
82
82
  set_cookie_header = headers.fetch('set-cookie')
83
- Array(set_cookie_header).each do |header_value|
84
- header_value.split("\n").each do |cookie|
85
- cookie_name, cookie_filling = cookie.split('=', 2)
86
- cookie_attributes = identify_cookie_attributes cookie_filling
87
- parsed_cookie = CGI::Cookie.new(
88
- 'name' => cookie_name.strip,
89
- 'value' => cookie_attributes.fetch('value'),
90
- 'path' => cookie_attributes.fetch('path', nil),
91
- 'domain' => cookie_attributes.fetch('domain', nil),
92
- 'expires' => cookie_attributes.fetch('expires', nil),
93
- 'secure' => cookie_attributes.fetch('secure', false)
94
- )
95
- cookies.store(cookie_name, parsed_cookie)
96
- end
83
+ Array(set_cookie_header).each do |cookie|
84
+ cookie_name, cookie_filling = cookie.split('=', 2)
85
+ cookie_attributes = identify_cookie_attributes cookie_filling
86
+ parsed_cookie = CGI::Cookie.new(
87
+ 'name' => cookie_name.strip,
88
+ 'value' => cookie_attributes.fetch('value'),
89
+ 'path' => cookie_attributes.fetch('path', nil),
90
+ 'domain' => cookie_attributes.fetch('domain', nil),
91
+ 'expires' => cookie_attributes.fetch('expires', nil),
92
+ 'secure' => cookie_attributes.fetch('secure', false)
93
+ )
94
+ cookies.store(cookie_name, parsed_cookie)
97
95
  end
98
96
  end
99
97
  cookies