rack 3.0.11 → 3.1.8

Sign up to get free protection for your applications and to get access to all the features.
data/lib/rack/headers.rb CHANGED
@@ -1,9 +1,93 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Rack
2
4
  # Rack::Headers is a Hash subclass that downcases all keys. It's designed
3
5
  # to be used by rack applications that don't implement the Rack 3 SPEC
4
6
  # (by using non-lowercase response header keys), automatically handling
5
7
  # the downcasing of keys.
6
8
  class Headers < Hash
9
+ KNOWN_HEADERS = {}
10
+ %w(
11
+ Accept-CH
12
+ Accept-Patch
13
+ Accept-Ranges
14
+ Access-Control-Allow-Credentials
15
+ Access-Control-Allow-Headers
16
+ Access-Control-Allow-Methods
17
+ Access-Control-Allow-Origin
18
+ Access-Control-Expose-Headers
19
+ Access-Control-Max-Age
20
+ Age
21
+ Allow
22
+ Alt-Svc
23
+ Cache-Control
24
+ Connection
25
+ Content-Disposition
26
+ Content-Encoding
27
+ Content-Language
28
+ Content-Length
29
+ Content-Location
30
+ Content-MD5
31
+ Content-Range
32
+ Content-Security-Policy
33
+ Content-Security-Policy-Report-Only
34
+ Content-Type
35
+ Date
36
+ Delta-Base
37
+ ETag
38
+ Expect-CT
39
+ Expires
40
+ Feature-Policy
41
+ IM
42
+ Last-Modified
43
+ Link
44
+ Location
45
+ NEL
46
+ P3P
47
+ Permissions-Policy
48
+ Pragma
49
+ Preference-Applied
50
+ Proxy-Authenticate
51
+ Public-Key-Pins
52
+ Referrer-Policy
53
+ Refresh
54
+ Report-To
55
+ Retry-After
56
+ Server
57
+ Set-Cookie
58
+ Status
59
+ Strict-Transport-Security
60
+ Timing-Allow-Origin
61
+ Tk
62
+ Trailer
63
+ Transfer-Encoding
64
+ Upgrade
65
+ Vary
66
+ Via
67
+ WWW-Authenticate
68
+ Warning
69
+ X-Cascade
70
+ X-Content-Duration
71
+ X-Content-Security-Policy
72
+ X-Content-Type-Options
73
+ X-Correlation-ID
74
+ X-Correlation-Id
75
+ X-Download-Options
76
+ X-Frame-Options
77
+ X-Permitted-Cross-Domain-Policies
78
+ X-Powered-By
79
+ X-Redirect-By
80
+ X-Request-ID
81
+ X-Request-Id
82
+ X-Runtime
83
+ X-UA-Compatible
84
+ X-WebKit-CS
85
+ X-XSS-Protection
86
+ ).each do |str|
87
+ downcased = str.downcase.freeze
88
+ KNOWN_HEADERS[str] = KNOWN_HEADERS[downcased] = downcased
89
+ end
90
+
7
91
  def self.[](*items)
8
92
  if items.length % 2 != 0
9
93
  if items.length == 1 && items.first.is_a?(Hash)
@@ -28,7 +112,7 @@ module Rack
28
112
  end
29
113
 
30
114
  def []=(key, value)
31
- super(key.downcase.freeze, value)
115
+ super(KNOWN_HEADERS[key] || key.downcase.freeze, value)
32
116
  end
33
117
  alias store []=
34
118
 
@@ -148,7 +232,7 @@ module Rack
148
232
  private
149
233
 
150
234
  def downcase_key(key)
151
- key.is_a?(String) ? key.downcase : key
235
+ key.is_a?(String) ? KNOWN_HEADERS[key] || key.downcase : key
152
236
  end
153
237
  end
154
238
  end
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#{Utils::URI_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,34 @@ 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 or an empty string.
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
+ when ""
385
+ # Empty string is okay.
386
+ else
387
+ raise LintError, "PATH_INFO must start with a '/' and must not include a fragment part starting with '#' (origin-form)"
388
+ end
350
389
  end
390
+
351
391
  ## * The <tt>CONTENT_LENGTH</tt>, if given, must consist of digits only.
352
392
  if env.include?("CONTENT_LENGTH") && env["CONTENT_LENGTH"] !~ /\A\d+\z/
353
393
  raise LintError, "Invalid CONTENT_LENGTH: #{env["CONTENT_LENGTH"]}"
@@ -384,9 +424,9 @@ module Rack
384
424
  ##
385
425
  ## The input stream is an IO-like object which contains the raw HTTP
386
426
  ## POST data.
387
- def check_input(input)
427
+ def check_input_stream(input)
388
428
  ## When applicable, its external encoding must be "ASCII-8BIT" and it
389
- ## must be opened in binary mode, for Ruby 1.9 compatibility.
429
+ ## must be opened in binary mode.
390
430
  if input.respond_to?(:external_encoding) && input.external_encoding != Encoding::ASCII_8BIT
391
431
  raise LintError, "rack.input #{input} does not have ASCII-8BIT as its external encoding"
392
432
  end
@@ -418,7 +458,7 @@ module Rack
418
458
  v
419
459
  end
420
460
 
421
- ## * +read+ behaves like IO#read.
461
+ ## * +read+ behaves like <tt>IO#read</tt>.
422
462
  ## Its signature is <tt>read([length, [buffer]])</tt>.
423
463
  ##
424
464
  ## If given, +length+ must be a non-negative Integer (>= 0) or +nil+,
@@ -478,8 +518,8 @@ module Rack
478
518
  }
479
519
  end
480
520
 
481
- ## * +close+ can be called on the input stream to indicate that the
482
- ## any remaining input is not needed.
521
+ ## * +close+ can be called on the input stream to indicate that
522
+ ## any remaining input is not needed.
483
523
  def close(*args)
484
524
  @input.close(*args)
485
525
  end
@@ -488,7 +528,7 @@ module Rack
488
528
  ##
489
529
  ## === The Error Stream
490
530
  ##
491
- def check_error(error)
531
+ def check_error_stream(error)
492
532
  ## The error stream must respond to +puts+, +write+ and +flush+.
493
533
  [:puts, :write, :flush].each { |method|
494
534
  unless error.respond_to? method
@@ -609,6 +649,30 @@ module Rack
609
649
  nil
610
650
  end
611
651
 
652
+ ##
653
+ ## === Early Hints
654
+ ##
655
+ ## The application or any middleware may call the <tt>rack.early_hints</tt>
656
+ ## with an object which would be valid as the headers of a Rack response.
657
+ def check_early_hints(env)
658
+ if env[RACK_EARLY_HINTS]
659
+ ##
660
+ ## If <tt>rack.early_hints</tt> is present, it must respond to #call.
661
+ unless env[RACK_EARLY_HINTS].respond_to?(:call)
662
+ raise LintError, "rack.early_hints must respond to call"
663
+ end
664
+
665
+ original_callback = env[RACK_EARLY_HINTS]
666
+ env[RACK_EARLY_HINTS] = lambda do |headers|
667
+ ## If <tt>rack.early_hints</tt> is called, it must be called with
668
+ ## valid Rack response headers.
669
+ check_headers(headers)
670
+ original_callback.call(headers)
671
+ end
672
+ end
673
+ end
674
+
675
+ ##
612
676
  ## == The Response
613
677
  ##
614
678
  ## === The Status
@@ -672,9 +736,9 @@ module Rack
672
736
  end
673
737
 
674
738
  ##
675
- ## === The content-type
739
+ ## ==== The +content-type+ Header
676
740
  ##
677
- def check_content_type(status, headers)
741
+ def check_content_type_header(status, headers)
678
742
  headers.each { |key, value|
679
743
  ## There must not be a <tt>content-type</tt> header key when the +Status+ is 1xx,
680
744
  ## 204, or 304.
@@ -688,9 +752,9 @@ module Rack
688
752
  end
689
753
 
690
754
  ##
691
- ## === The content-length
755
+ ## ==== The +content-length+ Header
692
756
  ##
693
- def check_content_length(status, headers)
757
+ def check_content_length_header(status, headers)
694
758
  headers.each { |key, value|
695
759
  if key == 'content-length'
696
760
  ## There must not be a <tt>content-length</tt> header key when the
@@ -715,6 +779,29 @@ module Rack
715
779
  end
716
780
  end
717
781
 
782
+ ##
783
+ ## ==== The +rack.protocol+ Header
784
+ ##
785
+ def check_rack_protocol_header(status, headers)
786
+ ## If the +rack.protocol+ header is present, it must be a +String+, and
787
+ ## must be one of the values from the +rack.protocol+ array from the
788
+ ## environment.
789
+ protocol = headers['rack.protocol']
790
+
791
+ if protocol
792
+ request_protocols = @env['rack.protocol']
793
+
794
+ if request_protocols.nil?
795
+ raise LintError, "rack.protocol header is #{protocol.inspect}, but rack.protocol was not set in request!"
796
+ elsif !request_protocols.include?(protocol)
797
+ raise LintError, "rack.protocol header is #{protocol.inspect}, but should be one of #{request_protocols.inspect} from the request!"
798
+ end
799
+ end
800
+ end
801
+ ##
802
+ ## Setting this value informs the server that it should perform a
803
+ ## connection upgrade. In HTTP/1, this is done using the +upgrade+
804
+ ## header. In HTTP/2, this is done by accepting the request.
718
805
  ##
719
806
  ## === The Body
720
807
  ##
@@ -782,7 +869,7 @@ module Rack
782
869
  ## It must only be called once.
783
870
  raise LintError, "Response body must only be invoked once (#{@invoked})" unless @invoked.nil?
784
871
 
785
- ## It must not be called after being closed.
872
+ ## It must not be called after being closed,
786
873
  raise LintError, "Response body is already closed" if @closed
787
874
 
788
875
  @invoked = :each
@@ -793,9 +880,6 @@ module Rack
793
880
  raise LintError, "Body yielded non-string value #{chunk.inspect}"
794
881
  end
795
882
 
796
- ##
797
- ## The Body itself should not be an instance of String, as this will
798
- ## break in Ruby 1.9.
799
883
  ##
800
884
  ## Middleware must not call +each+ directly on the Body.
801
885
  ## 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,20 @@ 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])
150
- else
151
- rack_input = opts[:input]
142
+ rack_input = opts[:input]
143
+ if String === rack_input
144
+ rack_input = StringIO.new(rack_input)
152
145
  end
153
146
 
154
- rack_input.set_encoding(Encoding::BINARY)
155
- env[RACK_INPUT] = rack_input
147
+ if rack_input
148
+ rack_input.set_encoding(Encoding::BINARY) if rack_input.respond_to?(:set_encoding)
149
+ env[RACK_INPUT] = rack_input
156
150
 
157
- env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
151
+ env["CONTENT_LENGTH"] ||= env[RACK_INPUT].size.to_s if env[RACK_INPUT].respond_to?(:size)
152
+ end
158
153
 
159
154
  opts.each { |field, value|
160
- env[field] = value if String === field
155
+ env[field] = value if String === field
161
156
  }
162
157
 
163
158
  env
@@ -78,22 +78,20 @@ module Rack
78
78
 
79
79
  def parse_cookies_from_header
80
80
  cookies = Hash.new
81
- if headers.has_key? 'set-cookie'
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
81
+ set_cookie_header = headers['set-cookie']
82
+ if set_cookie_header && !set_cookie_header.empty?
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