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