actionpack 6.0.0

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

Potentially problematic release.


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

Files changed (181) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +311 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +58 -0
  5. data/lib/abstract_controller.rb +27 -0
  6. data/lib/abstract_controller/asset_paths.rb +12 -0
  7. data/lib/abstract_controller/base.rb +267 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/caching/fragments.rb +150 -0
  10. data/lib/abstract_controller/callbacks.rb +224 -0
  11. data/lib/abstract_controller/collector.rb +43 -0
  12. data/lib/abstract_controller/error.rb +6 -0
  13. data/lib/abstract_controller/helpers.rb +194 -0
  14. data/lib/abstract_controller/logger.rb +14 -0
  15. data/lib/abstract_controller/railties/routes_helpers.rb +20 -0
  16. data/lib/abstract_controller/rendering.rb +127 -0
  17. data/lib/abstract_controller/translation.rb +32 -0
  18. data/lib/abstract_controller/url_for.rb +35 -0
  19. data/lib/action_controller.rb +67 -0
  20. data/lib/action_controller/api.rb +150 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +271 -0
  23. data/lib/action_controller/caching.rb +46 -0
  24. data/lib/action_controller/form_builder.rb +50 -0
  25. data/lib/action_controller/log_subscriber.rb +81 -0
  26. data/lib/action_controller/metal.rb +256 -0
  27. data/lib/action_controller/metal/basic_implicit_render.rb +13 -0
  28. data/lib/action_controller/metal/conditional_get.rb +280 -0
  29. data/lib/action_controller/metal/content_security_policy.rb +52 -0
  30. data/lib/action_controller/metal/cookies.rb +16 -0
  31. data/lib/action_controller/metal/data_streaming.rb +151 -0
  32. data/lib/action_controller/metal/default_headers.rb +17 -0
  33. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  34. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  35. data/lib/action_controller/metal/exceptions.rb +74 -0
  36. data/lib/action_controller/metal/flash.rb +61 -0
  37. data/lib/action_controller/metal/force_ssl.rb +58 -0
  38. data/lib/action_controller/metal/head.rb +60 -0
  39. data/lib/action_controller/metal/helpers.rb +122 -0
  40. data/lib/action_controller/metal/http_authentication.rb +518 -0
  41. data/lib/action_controller/metal/implicit_render.rb +63 -0
  42. data/lib/action_controller/metal/instrumentation.rb +105 -0
  43. data/lib/action_controller/metal/live.rb +314 -0
  44. data/lib/action_controller/metal/mime_responds.rb +324 -0
  45. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  46. data/lib/action_controller/metal/params_wrapper.rb +297 -0
  47. data/lib/action_controller/metal/redirecting.rb +133 -0
  48. data/lib/action_controller/metal/renderers.rb +181 -0
  49. data/lib/action_controller/metal/rendering.rb +122 -0
  50. data/lib/action_controller/metal/request_forgery_protection.rb +456 -0
  51. data/lib/action_controller/metal/rescue.rb +28 -0
  52. data/lib/action_controller/metal/streaming.rb +223 -0
  53. data/lib/action_controller/metal/strong_parameters.rb +1105 -0
  54. data/lib/action_controller/metal/testing.rb +16 -0
  55. data/lib/action_controller/metal/url_for.rb +58 -0
  56. data/lib/action_controller/railtie.rb +89 -0
  57. data/lib/action_controller/railties/helpers.rb +24 -0
  58. data/lib/action_controller/renderer.rb +130 -0
  59. data/lib/action_controller/template_assertions.rb +11 -0
  60. data/lib/action_controller/test_case.rb +626 -0
  61. data/lib/action_dispatch.rb +114 -0
  62. data/lib/action_dispatch/http/cache.rb +226 -0
  63. data/lib/action_dispatch/http/content_disposition.rb +45 -0
  64. data/lib/action_dispatch/http/content_security_policy.rb +284 -0
  65. data/lib/action_dispatch/http/filter_parameters.rb +86 -0
  66. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  67. data/lib/action_dispatch/http/headers.rb +132 -0
  68. data/lib/action_dispatch/http/mime_negotiation.rb +177 -0
  69. data/lib/action_dispatch/http/mime_type.rb +350 -0
  70. data/lib/action_dispatch/http/mime_types.rb +50 -0
  71. data/lib/action_dispatch/http/parameter_filter.rb +12 -0
  72. data/lib/action_dispatch/http/parameters.rb +136 -0
  73. data/lib/action_dispatch/http/rack_cache.rb +63 -0
  74. data/lib/action_dispatch/http/request.rb +427 -0
  75. data/lib/action_dispatch/http/response.rb +534 -0
  76. data/lib/action_dispatch/http/upload.rb +92 -0
  77. data/lib/action_dispatch/http/url.rb +350 -0
  78. data/lib/action_dispatch/journey.rb +7 -0
  79. data/lib/action_dispatch/journey/formatter.rb +189 -0
  80. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  81. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  82. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  83. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  84. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  85. data/lib/action_dispatch/journey/nfa/simulator.rb +47 -0
  86. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  87. data/lib/action_dispatch/journey/nodes/node.rb +141 -0
  88. data/lib/action_dispatch/journey/parser.rb +199 -0
  89. data/lib/action_dispatch/journey/parser.y +50 -0
  90. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  91. data/lib/action_dispatch/journey/path/pattern.rb +203 -0
  92. data/lib/action_dispatch/journey/route.rb +204 -0
  93. data/lib/action_dispatch/journey/router.rb +153 -0
  94. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  95. data/lib/action_dispatch/journey/routes.rb +81 -0
  96. data/lib/action_dispatch/journey/scanner.rb +71 -0
  97. data/lib/action_dispatch/journey/visitors.rb +268 -0
  98. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  99. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  100. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  101. data/lib/action_dispatch/middleware/actionable_exceptions.rb +39 -0
  102. data/lib/action_dispatch/middleware/callbacks.rb +34 -0
  103. data/lib/action_dispatch/middleware/cookies.rb +663 -0
  104. data/lib/action_dispatch/middleware/debug_exceptions.rb +185 -0
  105. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  106. data/lib/action_dispatch/middleware/debug_view.rb +68 -0
  107. data/lib/action_dispatch/middleware/exception_wrapper.rb +181 -0
  108. data/lib/action_dispatch/middleware/executor.rb +21 -0
  109. data/lib/action_dispatch/middleware/flash.rb +300 -0
  110. data/lib/action_dispatch/middleware/host_authorization.rb +103 -0
  111. data/lib/action_dispatch/middleware/public_exceptions.rb +61 -0
  112. data/lib/action_dispatch/middleware/reloader.rb +12 -0
  113. data/lib/action_dispatch/middleware/remote_ip.rb +181 -0
  114. data/lib/action_dispatch/middleware/request_id.rb +43 -0
  115. data/lib/action_dispatch/middleware/session/abstract_store.rb +92 -0
  116. data/lib/action_dispatch/middleware/session/cache_store.rb +54 -0
  117. data/lib/action_dispatch/middleware/session/cookie_store.rb +113 -0
  118. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +28 -0
  119. data/lib/action_dispatch/middleware/show_exceptions.rb +62 -0
  120. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  121. data/lib/action_dispatch/middleware/stack.rb +148 -0
  122. data/lib/action_dispatch/middleware/static.rb +129 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/_actions.html.erb +13 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/_actions.text.erb +0 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +24 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +29 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +62 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.html.erb +7 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/blocked_host.text.erb +5 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +38 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +24 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +15 -0
  137. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +165 -0
  138. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.html.erb +19 -0
  139. data/lib/action_dispatch/middleware/templates/rescues/missing_exact_template.text.erb +3 -0
  140. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  141. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  142. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  143. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  144. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  145. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  146. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  147. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  148. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  149. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +203 -0
  150. data/lib/action_dispatch/railtie.rb +58 -0
  151. data/lib/action_dispatch/request/session.rb +242 -0
  152. data/lib/action_dispatch/request/utils.rb +78 -0
  153. data/lib/action_dispatch/routing.rb +261 -0
  154. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  155. data/lib/action_dispatch/routing/inspector.rb +274 -0
  156. data/lib/action_dispatch/routing/mapper.rb +2289 -0
  157. data/lib/action_dispatch/routing/polymorphic_routes.rb +351 -0
  158. data/lib/action_dispatch/routing/redirection.rb +201 -0
  159. data/lib/action_dispatch/routing/route_set.rb +887 -0
  160. data/lib/action_dispatch/routing/routes_proxy.rb +69 -0
  161. data/lib/action_dispatch/routing/url_for.rb +237 -0
  162. data/lib/action_dispatch/system_test_case.rb +168 -0
  163. data/lib/action_dispatch/system_testing/browser.rb +80 -0
  164. data/lib/action_dispatch/system_testing/driver.rb +68 -0
  165. data/lib/action_dispatch/system_testing/server.rb +31 -0
  166. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +97 -0
  167. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +33 -0
  168. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  169. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  170. data/lib/action_dispatch/testing/assertions.rb +24 -0
  171. data/lib/action_dispatch/testing/assertions/response.rb +106 -0
  172. data/lib/action_dispatch/testing/assertions/routing.rb +234 -0
  173. data/lib/action_dispatch/testing/integration.rb +659 -0
  174. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  175. data/lib/action_dispatch/testing/test_process.rb +50 -0
  176. data/lib/action_dispatch/testing/test_request.rb +71 -0
  177. data/lib/action_dispatch/testing/test_response.rb +25 -0
  178. data/lib/action_pack.rb +26 -0
  179. data/lib/action_pack/gem_version.rb +17 -0
  180. data/lib/action_pack/version.rb +10 -0
  181. metadata +329 -0
@@ -0,0 +1,30 @@
1
+ body {
2
+ font-family: "Helvetica Neue", Helvetica, Arial, Sans-Serif;
3
+ margin: 0;
4
+ }
5
+
6
+ h1 {
7
+ font-size: 2.0em; font-weight: bold; text-align: center;
8
+ color: white; background-color: black;
9
+ padding: 5px 0;
10
+ margin: 0 0 20px;
11
+ }
12
+
13
+ h2 {
14
+ text-align: center;
15
+ display: none;
16
+ font-size: 0.5em;
17
+ }
18
+
19
+ .clearfix {display: inline-block; }
20
+ .input { overflow: show;}
21
+ .instruction { color: #666; padding: 0 30px 20px; font-size: 0.9em}
22
+ .instruction p { padding: 0 0 5px; }
23
+ .instruction li { padding: 0 10px 5px; }
24
+
25
+ .form { background: #EEE; padding: 20px 30px; border-radius: 5px; margin-left: auto; margin-right: auto; width: 500px; margin-bottom: 20px}
26
+ .form p, .form form { text-align: center }
27
+ .form form {padding: 0 10px 5px; }
28
+ .form .fun_routes { font-size: 0.9em;}
29
+ .form .fun_routes a { margin: 0 5px 0 0; }
30
+
@@ -0,0 +1,134 @@
1
+ function tokenize(input, callback) {
2
+ while(input.length > 0) {
3
+ callback(input.match(/^[\/\.\?]|[^\/\.\?]+/)[0]);
4
+ input = input.replace(/^[\/\.\?]|[^\/\.\?]+/, '');
5
+ }
6
+ }
7
+
8
+ var graph = d3.select("#chart-2 svg");
9
+ var svg_edges = {};
10
+ var svg_nodes = {};
11
+
12
+ graph.selectAll("g.edge").each(function() {
13
+ var node = d3.select(this);
14
+ var index = node.select("title").text().split("->");
15
+ var left = parseInt(index[0]);
16
+ var right = parseInt(index[1]);
17
+
18
+ if(!svg_edges[left]) { svg_edges[left] = {} }
19
+ svg_edges[left][right] = node;
20
+ });
21
+
22
+ graph.selectAll("g.node").each(function() {
23
+ var node = d3.select(this);
24
+ var index = parseInt(node.select("title").text());
25
+ svg_nodes[index] = node;
26
+ });
27
+
28
+ function reset_graph() {
29
+ for(var key in svg_edges) {
30
+ for(var mkey in svg_edges[key]) {
31
+ var node = svg_edges[key][mkey];
32
+ var path = node.select("path");
33
+ var arrow = node.select("polygon");
34
+ path.style("stroke", "black");
35
+ arrow.style("stroke", "black").style("fill", "black");
36
+ }
37
+ }
38
+
39
+ for(var key in svg_nodes) {
40
+ var node = svg_nodes[key];
41
+ node.select('ellipse').style("fill", "white");
42
+ node.select('polygon').style("fill", "white");
43
+ }
44
+ return false;
45
+ }
46
+
47
+ function highlight_edge(from, to) {
48
+ var node = svg_edges[from][to];
49
+ var path = node.select("path");
50
+ var arrow = node.select("polygon");
51
+
52
+ path
53
+ .transition().duration(500)
54
+ .style("stroke", "green");
55
+
56
+ arrow
57
+ .transition().duration(500)
58
+ .style("stroke", "green").style("fill", "green");
59
+ }
60
+
61
+ function highlight_state(index, color) {
62
+ if(!color) { color = "green"; }
63
+
64
+ svg_nodes[index].select('ellipse')
65
+ .style("fill", "white")
66
+ .transition().duration(500)
67
+ .style("fill", color);
68
+ }
69
+
70
+ function highlight_finish(index) {
71
+ svg_nodes[index].select('polygon')
72
+ .style("fill", "while")
73
+ .transition().duration(500)
74
+ .style("fill", "blue");
75
+ }
76
+
77
+ function match(input) {
78
+ reset_graph();
79
+ var table = tt();
80
+ var states = [0];
81
+ var regexp_states = table['regexp_states'];
82
+ var string_states = table['string_states'];
83
+ var accepting = table['accepting'];
84
+
85
+ highlight_state(0);
86
+
87
+ tokenize(input, function(token) {
88
+ var new_states = [];
89
+ for(var key in states) {
90
+ var state = states[key];
91
+
92
+ if(string_states[state] && string_states[state][token]) {
93
+ var new_state = string_states[state][token];
94
+ highlight_edge(state, new_state);
95
+ highlight_state(new_state);
96
+ new_states.push(new_state);
97
+ }
98
+
99
+ if(regexp_states[state]) {
100
+ for(var key in regexp_states[state]) {
101
+ var re = new RegExp("^" + key + "$");
102
+ if(re.test(token)) {
103
+ var new_state = regexp_states[state][key];
104
+ highlight_edge(state, new_state);
105
+ highlight_state(new_state);
106
+ new_states.push(new_state);
107
+ }
108
+ }
109
+ }
110
+ }
111
+
112
+ if(new_states.length == 0) {
113
+ return;
114
+ }
115
+ states = new_states;
116
+ });
117
+
118
+ for(var key in states) {
119
+ var state = states[key];
120
+ if(accepting[state]) {
121
+ for(var mkey in svg_edges[state]) {
122
+ if(!regexp_states[mkey] && !string_states[mkey]) {
123
+ highlight_edge(state, mkey);
124
+ highlight_finish(mkey);
125
+ }
126
+ }
127
+ } else {
128
+ highlight_state(state, "red");
129
+ }
130
+ }
131
+
132
+ return false;
133
+ }
134
+
@@ -0,0 +1,52 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title><%= title %></title>
5
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.css" type="text/css">
6
+ <style>
7
+ <% stylesheets.each do |style| %>
8
+ <%= style %>
9
+ <% end %>
10
+ </style>
11
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.8/d3.min.js" type="text/javascript"></script>
12
+ </head>
13
+ <body>
14
+ <div id="wrapper">
15
+ <h1>Routes FSM with NFA simulation</h1>
16
+ <div class="instruction form">
17
+ <p>
18
+ Type a route in to the box and click "simulate".
19
+ </p>
20
+ <form onsubmit="return match(this.route.value);">
21
+ <input type="text" size="30" name="route" value="/articles/new" />
22
+ <button>simulate</button>
23
+ <input type="reset" value="reset" onclick="return reset_graph();"/>
24
+ </form>
25
+ <p class="fun_routes">
26
+ Some fun routes to try:
27
+ <% fun_routes.each do |path| %>
28
+ <a href="#" onclick="document.forms[0].elements[0].value=this.text.replace(/^\s+|\s+$/g,''); return match(this.text.replace(/^\s+|\s+$/g,''));">
29
+ <%= path %>
30
+ </a>
31
+ <% end %>
32
+ </p>
33
+ </div>
34
+ <div class='chart' id='chart-2'>
35
+ <%= svg %>
36
+ </div>
37
+ <div class="instruction">
38
+ <p>
39
+ This is a FSM for a system that has the following routes:
40
+ </p>
41
+ <ul>
42
+ <% paths.each do |route| %>
43
+ <li><%= route %></li>
44
+ <% end %>
45
+ </ul>
46
+ </div>
47
+ </div>
48
+ <% javascripts.each do |js| %>
49
+ <script><%= js %></script>
50
+ <% end %>
51
+ </body>
52
+ </html>
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "erb"
4
+ require "action_dispatch/http/request"
5
+ require "active_support/actionable_error"
6
+
7
+ module ActionDispatch
8
+ class ActionableExceptions # :nodoc:
9
+ cattr_accessor :endpoint, default: "/rails/actions"
10
+
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ request = ActionDispatch::Request.new(env)
17
+ return @app.call(env) unless actionable_request?(request)
18
+
19
+ ActiveSupport::ActionableError.dispatch(request.params[:error].to_s.safe_constantize, request.params[:action])
20
+
21
+ redirect_to request.params[:location]
22
+ end
23
+
24
+ private
25
+ def actionable_request?(request)
26
+ request.show_exceptions? && request.post? && request.path == endpoint
27
+ end
28
+
29
+ def redirect_to(location)
30
+ body = "<html><body>You are being <a href=\"#{ERB::Util.unwrapped_html_escape(location)}\">redirected</a>.</body></html>"
31
+
32
+ [302, {
33
+ "Content-Type" => "text/html; charset=#{Response.default_charset}",
34
+ "Content-Length" => body.bytesize.to_s,
35
+ "Location" => location,
36
+ }, [body]]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActionDispatch
4
+ # Provides callbacks to be executed before and after dispatching the request.
5
+ class Callbacks
6
+ include ActiveSupport::Callbacks
7
+
8
+ define_callbacks :call
9
+
10
+ class << self
11
+ def before(*args, &block)
12
+ set_callback(:call, :before, *args, &block)
13
+ end
14
+
15
+ def after(*args, &block)
16
+ set_callback(:call, :after, *args, &block)
17
+ end
18
+ end
19
+
20
+ def initialize(app)
21
+ @app = app
22
+ end
23
+
24
+ def call(env)
25
+ error = nil
26
+ result = run_callbacks :call do
27
+ @app.call(env)
28
+ rescue => error
29
+ end
30
+ raise error if error
31
+ result
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,663 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/keys"
4
+ require "active_support/key_generator"
5
+ require "active_support/message_verifier"
6
+ require "active_support/json"
7
+ require "rack/utils"
8
+
9
+ module ActionDispatch
10
+ class Request
11
+ def cookie_jar
12
+ fetch_header("action_dispatch.cookies") do
13
+ self.cookie_jar = Cookies::CookieJar.build(self, cookies)
14
+ end
15
+ end
16
+
17
+ # :stopdoc:
18
+ prepend Module.new {
19
+ def commit_cookie_jar!
20
+ cookie_jar.commit!
21
+ end
22
+ }
23
+
24
+ def have_cookie_jar?
25
+ has_header? "action_dispatch.cookies"
26
+ end
27
+
28
+ def cookie_jar=(jar)
29
+ set_header "action_dispatch.cookies", jar
30
+ end
31
+
32
+ def key_generator
33
+ get_header Cookies::GENERATOR_KEY
34
+ end
35
+
36
+ def signed_cookie_salt
37
+ get_header Cookies::SIGNED_COOKIE_SALT
38
+ end
39
+
40
+ def encrypted_cookie_salt
41
+ get_header Cookies::ENCRYPTED_COOKIE_SALT
42
+ end
43
+
44
+ def encrypted_signed_cookie_salt
45
+ get_header Cookies::ENCRYPTED_SIGNED_COOKIE_SALT
46
+ end
47
+
48
+ def authenticated_encrypted_cookie_salt
49
+ get_header Cookies::AUTHENTICATED_ENCRYPTED_COOKIE_SALT
50
+ end
51
+
52
+ def use_authenticated_cookie_encryption
53
+ get_header Cookies::USE_AUTHENTICATED_COOKIE_ENCRYPTION
54
+ end
55
+
56
+ def encrypted_cookie_cipher
57
+ get_header Cookies::ENCRYPTED_COOKIE_CIPHER
58
+ end
59
+
60
+ def signed_cookie_digest
61
+ get_header Cookies::SIGNED_COOKIE_DIGEST
62
+ end
63
+
64
+ def secret_key_base
65
+ get_header Cookies::SECRET_KEY_BASE
66
+ end
67
+
68
+ def cookies_serializer
69
+ get_header Cookies::COOKIES_SERIALIZER
70
+ end
71
+
72
+ def cookies_digest
73
+ get_header Cookies::COOKIES_DIGEST
74
+ end
75
+
76
+ def cookies_rotations
77
+ get_header Cookies::COOKIES_ROTATIONS
78
+ end
79
+
80
+ def use_cookies_with_metadata
81
+ get_header Cookies::USE_COOKIES_WITH_METADATA
82
+ end
83
+
84
+ # :startdoc:
85
+ end
86
+
87
+ # \Cookies are read and written through ActionController#cookies.
88
+ #
89
+ # The cookies being read are the ones received along with the request, the cookies
90
+ # being written will be sent out with the response. Reading a cookie does not get
91
+ # the cookie object itself back, just the value it holds.
92
+ #
93
+ # Examples of writing:
94
+ #
95
+ # # Sets a simple session cookie.
96
+ # # This cookie will be deleted when the user's browser is closed.
97
+ # cookies[:user_name] = "david"
98
+ #
99
+ # # Cookie values are String based. Other data types need to be serialized.
100
+ # cookies[:lat_lon] = JSON.generate([47.68, -122.37])
101
+ #
102
+ # # Sets a cookie that expires in 1 hour.
103
+ # cookies[:login] = { value: "XJ-122", expires: 1.hour }
104
+ #
105
+ # # Sets a cookie that expires at a specific time.
106
+ # cookies[:login] = { value: "XJ-122", expires: Time.utc(2020, 10, 15, 5) }
107
+ #
108
+ # # Sets a signed cookie, which prevents users from tampering with its value.
109
+ # # It can be read using the signed method `cookies.signed[:name]`
110
+ # cookies.signed[:user_id] = current_user.id
111
+ #
112
+ # # Sets an encrypted cookie value before sending it to the client which
113
+ # # prevent users from reading and tampering with its value.
114
+ # # It can be read using the encrypted method `cookies.encrypted[:name]`
115
+ # cookies.encrypted[:discount] = 45
116
+ #
117
+ # # Sets a "permanent" cookie (which expires in 20 years from now).
118
+ # cookies.permanent[:login] = "XJ-122"
119
+ #
120
+ # # You can also chain these methods:
121
+ # cookies.signed.permanent[:login] = "XJ-122"
122
+ #
123
+ # Examples of reading:
124
+ #
125
+ # cookies[:user_name] # => "david"
126
+ # cookies.size # => 2
127
+ # JSON.parse(cookies[:lat_lon]) # => [47.68, -122.37]
128
+ # cookies.signed[:login] # => "XJ-122"
129
+ # cookies.encrypted[:discount] # => 45
130
+ #
131
+ # Example for deleting:
132
+ #
133
+ # cookies.delete :user_name
134
+ #
135
+ # Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
136
+ #
137
+ # cookies[:name] = {
138
+ # value: 'a yummy cookie',
139
+ # expires: 1.year,
140
+ # domain: 'domain.com'
141
+ # }
142
+ #
143
+ # cookies.delete(:name, domain: 'domain.com')
144
+ #
145
+ # The option symbols for setting cookies are:
146
+ #
147
+ # * <tt>:value</tt> - The cookie's value.
148
+ # * <tt>:path</tt> - The path for which this cookie applies. Defaults to the root
149
+ # of the application.
150
+ # * <tt>:domain</tt> - The domain for which this cookie applies so you can
151
+ # restrict to the domain level. If you use a schema like www.example.com
152
+ # and want to share session with user.example.com set <tt>:domain</tt>
153
+ # to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
154
+ # <tt>:all</tt> or <tt>Array</tt> again when deleting cookies.
155
+ #
156
+ # domain: nil # Does not set cookie domain. (default)
157
+ # domain: :all # Allow the cookie for the top most level
158
+ # # domain and subdomains.
159
+ # domain: %w(.example.com .example.org) # Allow the cookie
160
+ # # for concrete domain names.
161
+ #
162
+ # * <tt>:tld_length</tt> - When using <tt>:domain => :all</tt>, this option can be used to explicitly
163
+ # set the TLD length when using a short (<= 3 character) domain that is being interpreted as part of a TLD.
164
+ # For example, to share cookies between user1.lvh.me and user2.lvh.me, set <tt>:tld_length</tt> to 2.
165
+ # * <tt>:expires</tt> - The time at which this cookie expires, as a \Time or ActiveSupport::Duration object.
166
+ # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
167
+ # Default is +false+.
168
+ # * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
169
+ # only HTTP. Defaults to +false+.
170
+ class Cookies
171
+ HTTP_HEADER = "Set-Cookie"
172
+ GENERATOR_KEY = "action_dispatch.key_generator"
173
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt"
174
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt"
175
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt"
176
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt"
177
+ USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption"
178
+ ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher"
179
+ SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest"
180
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base"
181
+ COOKIES_SERIALIZER = "action_dispatch.cookies_serializer"
182
+ COOKIES_DIGEST = "action_dispatch.cookies_digest"
183
+ COOKIES_ROTATIONS = "action_dispatch.cookies_rotations"
184
+ USE_COOKIES_WITH_METADATA = "action_dispatch.use_cookies_with_metadata"
185
+
186
+ # Cookies can typically store 4096 bytes.
187
+ MAX_COOKIE_SIZE = 4096
188
+
189
+ # Raised when storing more than 4K of session data.
190
+ CookieOverflow = Class.new StandardError
191
+
192
+ # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed.
193
+ module ChainedCookieJars
194
+ # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
195
+ #
196
+ # cookies.permanent[:prefers_open_id] = true
197
+ # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
198
+ #
199
+ # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
200
+ #
201
+ # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
202
+ #
203
+ # cookies.permanent.signed[:remember_me] = current_user.id
204
+ # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
205
+ def permanent
206
+ @permanent ||= PermanentCookieJar.new(self)
207
+ end
208
+
209
+ # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
210
+ # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
211
+ # cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
212
+ #
213
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
214
+ #
215
+ # Example:
216
+ #
217
+ # cookies.signed[:discount] = 45
218
+ # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
219
+ #
220
+ # cookies.signed[:discount] # => 45
221
+ def signed
222
+ @signed ||= SignedKeyRotatingCookieJar.new(self)
223
+ end
224
+
225
+ # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
226
+ # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
227
+ #
228
+ # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
229
+ # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
230
+ #
231
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
232
+ #
233
+ # Example:
234
+ #
235
+ # cookies.encrypted[:discount] = 45
236
+ # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
237
+ #
238
+ # cookies.encrypted[:discount] # => 45
239
+ def encrypted
240
+ @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
241
+ end
242
+
243
+ # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
244
+ # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
245
+ def signed_or_encrypted
246
+ @signed_or_encrypted ||=
247
+ if request.secret_key_base.present?
248
+ encrypted
249
+ else
250
+ signed
251
+ end
252
+ end
253
+
254
+ private
255
+
256
+ def upgrade_legacy_hmac_aes_cbc_cookies?
257
+ request.secret_key_base.present? &&
258
+ request.encrypted_signed_cookie_salt.present? &&
259
+ request.encrypted_cookie_salt.present? &&
260
+ request.use_authenticated_cookie_encryption
261
+ end
262
+
263
+ def encrypted_cookie_cipher
264
+ request.encrypted_cookie_cipher || "aes-256-gcm"
265
+ end
266
+
267
+ def signed_cookie_digest
268
+ request.signed_cookie_digest || "SHA1"
269
+ end
270
+ end
271
+
272
+ class CookieJar #:nodoc:
273
+ include Enumerable, ChainedCookieJars
274
+
275
+ # This regular expression is used to split the levels of a domain.
276
+ # The top level domain can be any string without a period or
277
+ # **.**, ***.** style TLDs like co.uk or com.au
278
+ #
279
+ # www.example.co.uk gives:
280
+ # $& => example.co.uk
281
+ #
282
+ # example.com gives:
283
+ # $& => example.com
284
+ #
285
+ # lots.of.subdomains.example.local gives:
286
+ # $& => example.local
287
+ DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
288
+
289
+ def self.build(req, cookies)
290
+ new(req).tap do |hash|
291
+ hash.update(cookies)
292
+ end
293
+ end
294
+
295
+ attr_reader :request
296
+
297
+ def initialize(request)
298
+ @set_cookies = {}
299
+ @delete_cookies = {}
300
+ @request = request
301
+ @cookies = {}
302
+ @committed = false
303
+ end
304
+
305
+ def committed?; @committed; end
306
+
307
+ def commit!
308
+ @committed = true
309
+ @set_cookies.freeze
310
+ @delete_cookies.freeze
311
+ end
312
+
313
+ def each(&block)
314
+ @cookies.each(&block)
315
+ end
316
+
317
+ # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
318
+ def [](name)
319
+ @cookies[name.to_s]
320
+ end
321
+
322
+ def fetch(name, *args, &block)
323
+ @cookies.fetch(name.to_s, *args, &block)
324
+ end
325
+
326
+ def key?(name)
327
+ @cookies.key?(name.to_s)
328
+ end
329
+ alias :has_key? :key?
330
+
331
+ # Returns the cookies as Hash.
332
+ alias :to_hash :to_h
333
+
334
+ def update(other_hash)
335
+ @cookies.update other_hash.stringify_keys
336
+ self
337
+ end
338
+
339
+ def update_cookies_from_jar
340
+ request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
341
+ set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) || @set_cookies.key?(k) }
342
+
343
+ @cookies.update set_cookies if set_cookies
344
+ end
345
+
346
+ def to_header
347
+ @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
348
+ end
349
+
350
+ def handle_options(options) # :nodoc:
351
+ if options[:expires].respond_to?(:from_now)
352
+ options[:expires] = options[:expires].from_now
353
+ end
354
+
355
+ options[:path] ||= "/"
356
+
357
+ if options[:domain] == :all || options[:domain] == "all"
358
+ # If there is a provided tld length then we use it otherwise default domain regexp.
359
+ domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
360
+
361
+ # If host is not ip and matches domain regexp.
362
+ # (ip confirms to domain regexp so we explicitly check for ip)
363
+ options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
364
+ ".#{$&}"
365
+ end
366
+ elsif options[:domain].is_a? Array
367
+ # If host matches one of the supplied domains without a dot in front of it.
368
+ options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
369
+ end
370
+ end
371
+
372
+ # Sets the cookie named +name+. The second argument may be the cookie's
373
+ # value or a hash of options as documented above.
374
+ def []=(name, options)
375
+ if options.is_a?(Hash)
376
+ options.symbolize_keys!
377
+ value = options[:value]
378
+ else
379
+ value = options
380
+ options = { value: value }
381
+ end
382
+
383
+ handle_options(options)
384
+
385
+ if @cookies[name.to_s] != value || options[:expires]
386
+ @cookies[name.to_s] = value
387
+ @set_cookies[name.to_s] = options
388
+ @delete_cookies.delete(name.to_s)
389
+ end
390
+
391
+ value
392
+ end
393
+
394
+ # Removes the cookie on the client machine by setting the value to an empty string
395
+ # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
396
+ # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
397
+ def delete(name, options = {})
398
+ return unless @cookies.has_key? name.to_s
399
+
400
+ options.symbolize_keys!
401
+ handle_options(options)
402
+
403
+ value = @cookies.delete(name.to_s)
404
+ @delete_cookies[name.to_s] = options
405
+ value
406
+ end
407
+
408
+ # Whether the given cookie is to be deleted by this CookieJar.
409
+ # Like <tt>[]=</tt>, you can pass in an options hash to test if a
410
+ # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
411
+ def deleted?(name, options = {})
412
+ options.symbolize_keys!
413
+ handle_options(options)
414
+ @delete_cookies[name.to_s] == options
415
+ end
416
+
417
+ # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie.
418
+ def clear(options = {})
419
+ @cookies.each_key { |k| delete(k, options) }
420
+ end
421
+
422
+ def write(headers)
423
+ if header = make_set_cookie_header(headers[HTTP_HEADER])
424
+ headers[HTTP_HEADER] = header
425
+ end
426
+ end
427
+
428
+ mattr_accessor :always_write_cookie, default: false
429
+
430
+ private
431
+
432
+ def escape(string)
433
+ ::Rack::Utils.escape(string)
434
+ end
435
+
436
+ def make_set_cookie_header(header)
437
+ header = @set_cookies.inject(header) { |m, (k, v)|
438
+ if write_cookie?(v)
439
+ ::Rack::Utils.add_cookie_to_header(m, k, v)
440
+ else
441
+ m
442
+ end
443
+ }
444
+ @delete_cookies.inject(header) { |m, (k, v)|
445
+ ::Rack::Utils.add_remove_cookie_to_header(m, k, v)
446
+ }
447
+ end
448
+
449
+ def write_cookie?(cookie)
450
+ request.ssl? || !cookie[:secure] || always_write_cookie
451
+ end
452
+ end
453
+
454
+ class AbstractCookieJar # :nodoc:
455
+ include ChainedCookieJars
456
+
457
+ def initialize(parent_jar)
458
+ @parent_jar = parent_jar
459
+ end
460
+
461
+ def [](name)
462
+ if data = @parent_jar[name.to_s]
463
+ parse(name, data, purpose: "cookie.#{name}") || parse(name, data)
464
+ end
465
+ end
466
+
467
+ def []=(name, options)
468
+ if options.is_a?(Hash)
469
+ options.symbolize_keys!
470
+ else
471
+ options = { value: options }
472
+ end
473
+
474
+ commit(name, options)
475
+ @parent_jar[name] = options
476
+ end
477
+
478
+ protected
479
+ def request; @parent_jar.request; end
480
+
481
+ private
482
+ def expiry_options(options)
483
+ if options[:expires].respond_to?(:from_now)
484
+ { expires_in: options[:expires] }
485
+ else
486
+ { expires_at: options[:expires] }
487
+ end
488
+ end
489
+
490
+ def cookie_metadata(name, options)
491
+ expiry_options(options).tap do |metadata|
492
+ metadata[:purpose] = "cookie.#{name}" if request.use_cookies_with_metadata
493
+ end
494
+ end
495
+
496
+ def parse(name, data, purpose: nil); data; end
497
+ def commit(name, options); end
498
+ end
499
+
500
+ class PermanentCookieJar < AbstractCookieJar # :nodoc:
501
+ private
502
+ def commit(name, options)
503
+ options[:expires] = 20.years.from_now
504
+ end
505
+ end
506
+
507
+ class JsonSerializer # :nodoc:
508
+ def self.load(value)
509
+ ActiveSupport::JSON.decode(value)
510
+ end
511
+
512
+ def self.dump(value)
513
+ ActiveSupport::JSON.encode(value)
514
+ end
515
+ end
516
+
517
+ module SerializedCookieJars # :nodoc:
518
+ MARSHAL_SIGNATURE = "\x04\x08"
519
+ SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
520
+
521
+ protected
522
+ def needs_migration?(value)
523
+ request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
524
+ end
525
+
526
+ def serialize(value)
527
+ serializer.dump(value)
528
+ end
529
+
530
+ def deserialize(name)
531
+ rotate = false
532
+ value = yield -> { rotate = true }
533
+
534
+ if value
535
+ case
536
+ when needs_migration?(value)
537
+ Marshal.load(value).tap do |v|
538
+ self[name] = { value: v }
539
+ end
540
+ when rotate
541
+ serializer.load(value).tap do |v|
542
+ self[name] = { value: v }
543
+ end
544
+ else
545
+ serializer.load(value)
546
+ end
547
+ end
548
+ end
549
+
550
+ def serializer
551
+ serializer = request.cookies_serializer || :marshal
552
+ case serializer
553
+ when :marshal
554
+ Marshal
555
+ when :json, :hybrid
556
+ JsonSerializer
557
+ else
558
+ serializer
559
+ end
560
+ end
561
+
562
+ def digest
563
+ request.cookies_digest || "SHA1"
564
+ end
565
+ end
566
+
567
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
568
+ include SerializedCookieJars
569
+
570
+ def initialize(parent_jar)
571
+ super
572
+
573
+ secret = request.key_generator.generate_key(request.signed_cookie_salt)
574
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
575
+
576
+ request.cookies_rotations.signed.each do |*secrets, **options|
577
+ @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
578
+ end
579
+ end
580
+
581
+ private
582
+ def parse(name, signed_message, purpose: nil)
583
+ deserialize(name) do |rotate|
584
+ @verifier.verified(signed_message, on_rotation: rotate, purpose: purpose)
585
+ end
586
+ end
587
+
588
+ def commit(name, options)
589
+ options[:value] = @verifier.generate(serialize(options[:value]), cookie_metadata(name, options))
590
+
591
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
592
+ end
593
+ end
594
+
595
+ class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
596
+ include SerializedCookieJars
597
+
598
+ def initialize(parent_jar)
599
+ super
600
+
601
+ if request.use_authenticated_cookie_encryption
602
+ key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
603
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
604
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
605
+ else
606
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
607
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
608
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
609
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
610
+ end
611
+
612
+ request.cookies_rotations.encrypted.each do |*secrets, **options|
613
+ @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
614
+ end
615
+
616
+ if upgrade_legacy_hmac_aes_cbc_cookies?
617
+ legacy_cipher = "aes-256-cbc"
618
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
619
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
620
+
621
+ @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
622
+ end
623
+ end
624
+
625
+ private
626
+ def parse(name, encrypted_message, purpose: nil)
627
+ deserialize(name) do |rotate|
628
+ @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate, purpose: purpose)
629
+ end
630
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
631
+ nil
632
+ end
633
+
634
+ def commit(name, options)
635
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), cookie_metadata(name, options))
636
+
637
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
638
+ end
639
+ end
640
+
641
+ def initialize(app)
642
+ @app = app
643
+ end
644
+
645
+ def call(env)
646
+ request = ActionDispatch::Request.new env
647
+
648
+ status, headers, body = @app.call(env)
649
+
650
+ if request.have_cookie_jar?
651
+ cookie_jar = request.cookie_jar
652
+ unless cookie_jar.committed?
653
+ cookie_jar.write(headers)
654
+ if headers[HTTP_HEADER].respond_to?(:join)
655
+ headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
656
+ end
657
+ end
658
+ end
659
+
660
+ [status, headers, body]
661
+ end
662
+ end
663
+ end