actionpack 5.2.3

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 (170) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +429 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +57 -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 +265 -0
  8. data/lib/abstract_controller/caching.rb +66 -0
  9. data/lib/abstract_controller/caching/fragments.rb +166 -0
  10. data/lib/abstract_controller/callbacks.rb +212 -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 +31 -0
  18. data/lib/abstract_controller/url_for.rb +35 -0
  19. data/lib/action_controller.rb +66 -0
  20. data/lib/action_controller/api.rb +149 -0
  21. data/lib/action_controller/api/api_rendering.rb +16 -0
  22. data/lib/action_controller/base.rb +276 -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 +78 -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 +274 -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 +152 -0
  32. data/lib/action_controller/metal/etag_with_flash.rb +18 -0
  33. data/lib/action_controller/metal/etag_with_template_digest.rb +57 -0
  34. data/lib/action_controller/metal/exceptions.rb +53 -0
  35. data/lib/action_controller/metal/flash.rb +61 -0
  36. data/lib/action_controller/metal/force_ssl.rb +99 -0
  37. data/lib/action_controller/metal/head.rb +60 -0
  38. data/lib/action_controller/metal/helpers.rb +123 -0
  39. data/lib/action_controller/metal/http_authentication.rb +519 -0
  40. data/lib/action_controller/metal/implicit_render.rb +73 -0
  41. data/lib/action_controller/metal/instrumentation.rb +107 -0
  42. data/lib/action_controller/metal/live.rb +312 -0
  43. data/lib/action_controller/metal/mime_responds.rb +313 -0
  44. data/lib/action_controller/metal/parameter_encoding.rb +51 -0
  45. data/lib/action_controller/metal/params_wrapper.rb +293 -0
  46. data/lib/action_controller/metal/redirecting.rb +133 -0
  47. data/lib/action_controller/metal/renderers.rb +181 -0
  48. data/lib/action_controller/metal/rendering.rb +122 -0
  49. data/lib/action_controller/metal/request_forgery_protection.rb +445 -0
  50. data/lib/action_controller/metal/rescue.rb +28 -0
  51. data/lib/action_controller/metal/streaming.rb +223 -0
  52. data/lib/action_controller/metal/strong_parameters.rb +1086 -0
  53. data/lib/action_controller/metal/testing.rb +16 -0
  54. data/lib/action_controller/metal/url_for.rb +58 -0
  55. data/lib/action_controller/railtie.rb +89 -0
  56. data/lib/action_controller/railties/helpers.rb +24 -0
  57. data/lib/action_controller/renderer.rb +117 -0
  58. data/lib/action_controller/template_assertions.rb +11 -0
  59. data/lib/action_controller/test_case.rb +629 -0
  60. data/lib/action_dispatch.rb +112 -0
  61. data/lib/action_dispatch/http/cache.rb +222 -0
  62. data/lib/action_dispatch/http/content_security_policy.rb +272 -0
  63. data/lib/action_dispatch/http/filter_parameters.rb +84 -0
  64. data/lib/action_dispatch/http/filter_redirect.rb +37 -0
  65. data/lib/action_dispatch/http/headers.rb +132 -0
  66. data/lib/action_dispatch/http/mime_negotiation.rb +175 -0
  67. data/lib/action_dispatch/http/mime_type.rb +342 -0
  68. data/lib/action_dispatch/http/mime_types.rb +50 -0
  69. data/lib/action_dispatch/http/parameter_filter.rb +86 -0
  70. data/lib/action_dispatch/http/parameters.rb +126 -0
  71. data/lib/action_dispatch/http/rack_cache.rb +63 -0
  72. data/lib/action_dispatch/http/request.rb +430 -0
  73. data/lib/action_dispatch/http/response.rb +519 -0
  74. data/lib/action_dispatch/http/upload.rb +84 -0
  75. data/lib/action_dispatch/http/url.rb +350 -0
  76. data/lib/action_dispatch/journey.rb +7 -0
  77. data/lib/action_dispatch/journey/formatter.rb +189 -0
  78. data/lib/action_dispatch/journey/gtg/builder.rb +164 -0
  79. data/lib/action_dispatch/journey/gtg/simulator.rb +41 -0
  80. data/lib/action_dispatch/journey/gtg/transition_table.rb +158 -0
  81. data/lib/action_dispatch/journey/nfa/builder.rb +78 -0
  82. data/lib/action_dispatch/journey/nfa/dot.rb +36 -0
  83. data/lib/action_dispatch/journey/nfa/simulator.rb +49 -0
  84. data/lib/action_dispatch/journey/nfa/transition_table.rb +120 -0
  85. data/lib/action_dispatch/journey/nodes/node.rb +140 -0
  86. data/lib/action_dispatch/journey/parser.rb +199 -0
  87. data/lib/action_dispatch/journey/parser.y +50 -0
  88. data/lib/action_dispatch/journey/parser_extras.rb +31 -0
  89. data/lib/action_dispatch/journey/path/pattern.rb +198 -0
  90. data/lib/action_dispatch/journey/route.rb +203 -0
  91. data/lib/action_dispatch/journey/router.rb +156 -0
  92. data/lib/action_dispatch/journey/router/utils.rb +102 -0
  93. data/lib/action_dispatch/journey/routes.rb +82 -0
  94. data/lib/action_dispatch/journey/scanner.rb +64 -0
  95. data/lib/action_dispatch/journey/visitors.rb +268 -0
  96. data/lib/action_dispatch/journey/visualizer/fsm.css +30 -0
  97. data/lib/action_dispatch/journey/visualizer/fsm.js +134 -0
  98. data/lib/action_dispatch/journey/visualizer/index.html.erb +52 -0
  99. data/lib/action_dispatch/middleware/callbacks.rb +36 -0
  100. data/lib/action_dispatch/middleware/cookies.rb +685 -0
  101. data/lib/action_dispatch/middleware/debug_exceptions.rb +205 -0
  102. data/lib/action_dispatch/middleware/debug_locks.rb +124 -0
  103. data/lib/action_dispatch/middleware/exception_wrapper.rb +147 -0
  104. data/lib/action_dispatch/middleware/executor.rb +21 -0
  105. data/lib/action_dispatch/middleware/flash.rb +300 -0
  106. data/lib/action_dispatch/middleware/public_exceptions.rb +57 -0
  107. data/lib/action_dispatch/middleware/reloader.rb +12 -0
  108. data/lib/action_dispatch/middleware/remote_ip.rb +183 -0
  109. data/lib/action_dispatch/middleware/request_id.rb +43 -0
  110. data/lib/action_dispatch/middleware/session/abstract_store.rb +92 -0
  111. data/lib/action_dispatch/middleware/session/cache_store.rb +54 -0
  112. data/lib/action_dispatch/middleware/session/cookie_store.rb +118 -0
  113. data/lib/action_dispatch/middleware/session/mem_cache_store.rb +28 -0
  114. data/lib/action_dispatch/middleware/show_exceptions.rb +62 -0
  115. data/lib/action_dispatch/middleware/ssl.rb +150 -0
  116. data/lib/action_dispatch/middleware/stack.rb +116 -0
  117. data/lib/action_dispatch/middleware/static.rb +130 -0
  118. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb +22 -0
  119. data/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb +23 -0
  120. data/lib/action_dispatch/middleware/templates/rescues/_source.html.erb +27 -0
  121. data/lib/action_dispatch/middleware/templates/rescues/_source.text.erb +8 -0
  122. data/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb +52 -0
  123. data/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb +9 -0
  124. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.html.erb +16 -0
  125. data/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb +9 -0
  126. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.html.erb +21 -0
  127. data/lib/action_dispatch/middleware/templates/rescues/invalid_statement.text.erb +13 -0
  128. data/lib/action_dispatch/middleware/templates/rescues/layout.erb +161 -0
  129. data/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb +11 -0
  130. data/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb +3 -0
  131. data/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb +32 -0
  132. data/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb +11 -0
  133. data/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb +20 -0
  134. data/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb +7 -0
  135. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb +6 -0
  136. data/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb +3 -0
  137. data/lib/action_dispatch/middleware/templates/routes/_route.html.erb +16 -0
  138. data/lib/action_dispatch/middleware/templates/routes/_table.html.erb +200 -0
  139. data/lib/action_dispatch/railtie.rb +55 -0
  140. data/lib/action_dispatch/request/session.rb +234 -0
  141. data/lib/action_dispatch/request/utils.rb +78 -0
  142. data/lib/action_dispatch/routing.rb +260 -0
  143. data/lib/action_dispatch/routing/endpoint.rb +17 -0
  144. data/lib/action_dispatch/routing/inspector.rb +225 -0
  145. data/lib/action_dispatch/routing/mapper.rb +2267 -0
  146. data/lib/action_dispatch/routing/polymorphic_routes.rb +352 -0
  147. data/lib/action_dispatch/routing/redirection.rb +201 -0
  148. data/lib/action_dispatch/routing/route_set.rb +890 -0
  149. data/lib/action_dispatch/routing/routes_proxy.rb +69 -0
  150. data/lib/action_dispatch/routing/url_for.rb +236 -0
  151. data/lib/action_dispatch/system_test_case.rb +147 -0
  152. data/lib/action_dispatch/system_testing/browser.rb +49 -0
  153. data/lib/action_dispatch/system_testing/driver.rb +59 -0
  154. data/lib/action_dispatch/system_testing/server.rb +31 -0
  155. data/lib/action_dispatch/system_testing/test_helpers/screenshot_helper.rb +96 -0
  156. data/lib/action_dispatch/system_testing/test_helpers/setup_and_teardown.rb +31 -0
  157. data/lib/action_dispatch/system_testing/test_helpers/undef_methods.rb +26 -0
  158. data/lib/action_dispatch/testing/assertion_response.rb +47 -0
  159. data/lib/action_dispatch/testing/assertions.rb +24 -0
  160. data/lib/action_dispatch/testing/assertions/response.rb +107 -0
  161. data/lib/action_dispatch/testing/assertions/routing.rb +222 -0
  162. data/lib/action_dispatch/testing/integration.rb +652 -0
  163. data/lib/action_dispatch/testing/request_encoder.rb +55 -0
  164. data/lib/action_dispatch/testing/test_process.rb +50 -0
  165. data/lib/action_dispatch/testing/test_request.rb +71 -0
  166. data/lib/action_dispatch/testing/test_response.rb +53 -0
  167. data/lib/action_pack.rb +26 -0
  168. data/lib/action_pack/gem_version.rb +17 -0
  169. data/lib/action_pack/version.rb +10 -0
  170. metadata +318 -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,36 @@
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
+ begin
28
+ @app.call(env)
29
+ rescue => error
30
+ end
31
+ end
32
+ raise error if error
33
+ result
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,685 @@
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".freeze) 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".freeze
26
+ end
27
+
28
+ def cookie_jar=(jar)
29
+ set_header "action_dispatch.cookies".freeze, 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_token
65
+ get_header Cookies::SECRET_TOKEN
66
+ end
67
+
68
+ def secret_key_base
69
+ get_header Cookies::SECRET_KEY_BASE
70
+ end
71
+
72
+ def cookies_serializer
73
+ get_header Cookies::COOKIES_SERIALIZER
74
+ end
75
+
76
+ def cookies_digest
77
+ get_header Cookies::COOKIES_DIGEST
78
+ end
79
+
80
+ def cookies_rotations
81
+ get_header Cookies::COOKIES_ROTATIONS
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".freeze
172
+ GENERATOR_KEY = "action_dispatch.key_generator".freeze
173
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
174
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
175
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
176
+ AUTHENTICATED_ENCRYPTED_COOKIE_SALT = "action_dispatch.authenticated_encrypted_cookie_salt".freeze
177
+ USE_AUTHENTICATED_COOKIE_ENCRYPTION = "action_dispatch.use_authenticated_cookie_encryption".freeze
178
+ ENCRYPTED_COOKIE_CIPHER = "action_dispatch.encrypted_cookie_cipher".freeze
179
+ SIGNED_COOKIE_DIGEST = "action_dispatch.signed_cookie_digest".freeze
180
+ SECRET_TOKEN = "action_dispatch.secret_token".freeze
181
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
182
+ COOKIES_SERIALIZER = "action_dispatch.cookies_serializer".freeze
183
+ COOKIES_DIGEST = "action_dispatch.cookies_digest".freeze
184
+ COOKIES_ROTATIONS = "action_dispatch.cookies_rotations".freeze
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
+ # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
214
+ # legacy cookies signed with the old key generator will be transparently upgraded.
215
+ #
216
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
217
+ #
218
+ # Example:
219
+ #
220
+ # cookies.signed[:discount] = 45
221
+ # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
222
+ #
223
+ # cookies.signed[:discount] # => 45
224
+ def signed
225
+ @signed ||= SignedKeyRotatingCookieJar.new(self)
226
+ end
227
+
228
+ # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
229
+ # If the cookie was tampered with by the user (or a 3rd party), +nil+ will be returned.
230
+ #
231
+ # If +secret_key_base+ and +secrets.secret_token+ (deprecated) are both set,
232
+ # legacy cookies signed with the old key generator will be transparently upgraded.
233
+ #
234
+ # If +config.action_dispatch.encrypted_cookie_salt+ and +config.action_dispatch.encrypted_signed_cookie_salt+
235
+ # are both set, legacy cookies encrypted with HMAC AES-256-CBC will be transparently upgraded.
236
+ #
237
+ # This jar requires that you set a suitable secret for the verification on your app's +secret_key_base+.
238
+ #
239
+ # Example:
240
+ #
241
+ # cookies.encrypted[:discount] = 45
242
+ # # => Set-Cookie: discount=DIQ7fw==--K3n//8vvnSbGq9dA--7Xh91HfLpwzbj1czhBiwOg==; path=/
243
+ #
244
+ # cookies.encrypted[:discount] # => 45
245
+ def encrypted
246
+ @encrypted ||= EncryptedKeyRotatingCookieJar.new(self)
247
+ end
248
+
249
+ # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
250
+ # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
251
+ def signed_or_encrypted
252
+ @signed_or_encrypted ||=
253
+ if request.secret_key_base.present?
254
+ encrypted
255
+ else
256
+ signed
257
+ end
258
+ end
259
+
260
+ private
261
+
262
+ def upgrade_legacy_signed_cookies?
263
+ request.secret_token.present? && request.secret_key_base.present?
264
+ end
265
+
266
+ def upgrade_legacy_hmac_aes_cbc_cookies?
267
+ request.secret_key_base.present? &&
268
+ request.encrypted_signed_cookie_salt.present? &&
269
+ request.encrypted_cookie_salt.present? &&
270
+ request.use_authenticated_cookie_encryption
271
+ end
272
+
273
+ def encrypted_cookie_cipher
274
+ request.encrypted_cookie_cipher || "aes-256-gcm"
275
+ end
276
+
277
+ def signed_cookie_digest
278
+ request.signed_cookie_digest || "SHA1"
279
+ end
280
+ end
281
+
282
+ class CookieJar #:nodoc:
283
+ include Enumerable, ChainedCookieJars
284
+
285
+ # This regular expression is used to split the levels of a domain.
286
+ # The top level domain can be any string without a period or
287
+ # **.**, ***.** style TLDs like co.uk or com.au
288
+ #
289
+ # www.example.co.uk gives:
290
+ # $& => example.co.uk
291
+ #
292
+ # example.com gives:
293
+ # $& => example.com
294
+ #
295
+ # lots.of.subdomains.example.local gives:
296
+ # $& => example.local
297
+ DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
298
+
299
+ def self.build(req, cookies)
300
+ new(req).tap do |hash|
301
+ hash.update(cookies)
302
+ end
303
+ end
304
+
305
+ attr_reader :request
306
+
307
+ def initialize(request)
308
+ @set_cookies = {}
309
+ @delete_cookies = {}
310
+ @request = request
311
+ @cookies = {}
312
+ @committed = false
313
+ end
314
+
315
+ def committed?; @committed; end
316
+
317
+ def commit!
318
+ @committed = true
319
+ @set_cookies.freeze
320
+ @delete_cookies.freeze
321
+ end
322
+
323
+ def each(&block)
324
+ @cookies.each(&block)
325
+ end
326
+
327
+ # Returns the value of the cookie by +name+, or +nil+ if no such cookie exists.
328
+ def [](name)
329
+ @cookies[name.to_s]
330
+ end
331
+
332
+ def fetch(name, *args, &block)
333
+ @cookies.fetch(name.to_s, *args, &block)
334
+ end
335
+
336
+ def key?(name)
337
+ @cookies.key?(name.to_s)
338
+ end
339
+ alias :has_key? :key?
340
+
341
+ # Returns the cookies as Hash.
342
+ alias :to_hash :to_h
343
+
344
+ def update(other_hash)
345
+ @cookies.update other_hash.stringify_keys
346
+ self
347
+ end
348
+
349
+ def update_cookies_from_jar
350
+ request_jar = @request.cookie_jar.instance_variable_get(:@cookies)
351
+ set_cookies = request_jar.reject { |k, _| @delete_cookies.key?(k) }
352
+
353
+ @cookies.update set_cookies if set_cookies
354
+ end
355
+
356
+ def to_header
357
+ @cookies.map { |k, v| "#{escape(k)}=#{escape(v)}" }.join "; "
358
+ end
359
+
360
+ def handle_options(options) # :nodoc:
361
+ if options[:expires].respond_to?(:from_now)
362
+ options[:expires] = options[:expires].from_now
363
+ end
364
+
365
+ options[:path] ||= "/"
366
+
367
+ if options[:domain] == :all || options[:domain] == "all"
368
+ # If there is a provided tld length then we use it otherwise default domain regexp.
369
+ domain_regexp = options[:tld_length] ? /([^.]+\.?){#{options[:tld_length]}}$/ : DOMAIN_REGEXP
370
+
371
+ # If host is not ip and matches domain regexp.
372
+ # (ip confirms to domain regexp so we explicitly check for ip)
373
+ options[:domain] = if (request.host !~ /^[\d.]+$/) && (request.host =~ domain_regexp)
374
+ ".#{$&}"
375
+ end
376
+ elsif options[:domain].is_a? Array
377
+ # If host matches one of the supplied domains without a dot in front of it.
378
+ options[:domain] = options[:domain].find { |domain| request.host.include? domain.sub(/^\./, "") }
379
+ end
380
+ end
381
+
382
+ # Sets the cookie named +name+. The second argument may be the cookie's
383
+ # value or a hash of options as documented above.
384
+ def []=(name, options)
385
+ if options.is_a?(Hash)
386
+ options.symbolize_keys!
387
+ value = options[:value]
388
+ else
389
+ value = options
390
+ options = { value: value }
391
+ end
392
+
393
+ handle_options(options)
394
+
395
+ if @cookies[name.to_s] != value || options[:expires]
396
+ @cookies[name.to_s] = value
397
+ @set_cookies[name.to_s] = options
398
+ @delete_cookies.delete(name.to_s)
399
+ end
400
+
401
+ value
402
+ end
403
+
404
+ # Removes the cookie on the client machine by setting the value to an empty string
405
+ # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
406
+ # an options hash to delete cookies with extra data such as a <tt>:path</tt>.
407
+ def delete(name, options = {})
408
+ return unless @cookies.has_key? name.to_s
409
+
410
+ options.symbolize_keys!
411
+ handle_options(options)
412
+
413
+ value = @cookies.delete(name.to_s)
414
+ @delete_cookies[name.to_s] = options
415
+ value
416
+ end
417
+
418
+ # Whether the given cookie is to be deleted by this CookieJar.
419
+ # Like <tt>[]=</tt>, you can pass in an options hash to test if a
420
+ # deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
421
+ def deleted?(name, options = {})
422
+ options.symbolize_keys!
423
+ handle_options(options)
424
+ @delete_cookies[name.to_s] == options
425
+ end
426
+
427
+ # Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie.
428
+ def clear(options = {})
429
+ @cookies.each_key { |k| delete(k, options) }
430
+ end
431
+
432
+ def write(headers)
433
+ if header = make_set_cookie_header(headers[HTTP_HEADER])
434
+ headers[HTTP_HEADER] = header
435
+ end
436
+ end
437
+
438
+ mattr_accessor :always_write_cookie, default: false
439
+
440
+ private
441
+
442
+ def escape(string)
443
+ ::Rack::Utils.escape(string)
444
+ end
445
+
446
+ def make_set_cookie_header(header)
447
+ header = @set_cookies.inject(header) { |m, (k, v)|
448
+ if write_cookie?(v)
449
+ ::Rack::Utils.add_cookie_to_header(m, k, v)
450
+ else
451
+ m
452
+ end
453
+ }
454
+ @delete_cookies.inject(header) { |m, (k, v)|
455
+ ::Rack::Utils.add_remove_cookie_to_header(m, k, v)
456
+ }
457
+ end
458
+
459
+ def write_cookie?(cookie)
460
+ request.ssl? || !cookie[:secure] || always_write_cookie
461
+ end
462
+ end
463
+
464
+ class AbstractCookieJar # :nodoc:
465
+ include ChainedCookieJars
466
+
467
+ def initialize(parent_jar)
468
+ @parent_jar = parent_jar
469
+ end
470
+
471
+ def [](name)
472
+ if data = @parent_jar[name.to_s]
473
+ parse name, data
474
+ end
475
+ end
476
+
477
+ def []=(name, options)
478
+ if options.is_a?(Hash)
479
+ options.symbolize_keys!
480
+ else
481
+ options = { value: options }
482
+ end
483
+
484
+ commit(options)
485
+ @parent_jar[name] = options
486
+ end
487
+
488
+ protected
489
+ def request; @parent_jar.request; end
490
+
491
+ private
492
+ def expiry_options(options)
493
+ if request.use_authenticated_cookie_encryption
494
+ if options[:expires].respond_to?(:from_now)
495
+ { expires_in: options[:expires] }
496
+ else
497
+ { expires_at: options[:expires] }
498
+ end
499
+ else
500
+ {}
501
+ end
502
+ end
503
+
504
+ def parse(name, data); data; end
505
+ def commit(options); end
506
+ end
507
+
508
+ class PermanentCookieJar < AbstractCookieJar # :nodoc:
509
+ private
510
+ def commit(options)
511
+ options[:expires] = 20.years.from_now
512
+ end
513
+ end
514
+
515
+ class JsonSerializer # :nodoc:
516
+ def self.load(value)
517
+ ActiveSupport::JSON.decode(value)
518
+ end
519
+
520
+ def self.dump(value)
521
+ ActiveSupport::JSON.encode(value)
522
+ end
523
+ end
524
+
525
+ module SerializedCookieJars # :nodoc:
526
+ MARSHAL_SIGNATURE = "\x04\x08".freeze
527
+ SERIALIZER = ActiveSupport::MessageEncryptor::NullSerializer
528
+
529
+ protected
530
+ def needs_migration?(value)
531
+ request.cookies_serializer == :hybrid && value.start_with?(MARSHAL_SIGNATURE)
532
+ end
533
+
534
+ def serialize(value)
535
+ serializer.dump(value)
536
+ end
537
+
538
+ def deserialize(name)
539
+ rotate = false
540
+ value = yield -> { rotate = true }
541
+
542
+ if value
543
+ case
544
+ when needs_migration?(value)
545
+ self[name] = Marshal.load(value)
546
+ when rotate
547
+ self[name] = serializer.load(value)
548
+ else
549
+ serializer.load(value)
550
+ end
551
+ end
552
+ end
553
+
554
+ def serializer
555
+ serializer = request.cookies_serializer || :marshal
556
+ case serializer
557
+ when :marshal
558
+ Marshal
559
+ when :json, :hybrid
560
+ JsonSerializer
561
+ else
562
+ serializer
563
+ end
564
+ end
565
+
566
+ def digest
567
+ request.cookies_digest || "SHA1"
568
+ end
569
+ end
570
+
571
+ class SignedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
572
+ include SerializedCookieJars
573
+
574
+ def initialize(parent_jar)
575
+ super
576
+
577
+ secret = request.key_generator.generate_key(request.signed_cookie_salt)
578
+ @verifier = ActiveSupport::MessageVerifier.new(secret, digest: signed_cookie_digest, serializer: SERIALIZER)
579
+
580
+ request.cookies_rotations.signed.each do |*secrets, **options|
581
+ @verifier.rotate(*secrets, serializer: SERIALIZER, **options)
582
+ end
583
+
584
+ if upgrade_legacy_signed_cookies?
585
+ @verifier.rotate request.secret_token, serializer: SERIALIZER
586
+ end
587
+ end
588
+
589
+ private
590
+ def parse(name, signed_message)
591
+ deserialize(name) do |rotate|
592
+ @verifier.verified(signed_message, on_rotation: rotate)
593
+ end
594
+ end
595
+
596
+ def commit(options)
597
+ options[:value] = @verifier.generate(serialize(options[:value]), expiry_options(options))
598
+
599
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
600
+ end
601
+ end
602
+
603
+ class EncryptedKeyRotatingCookieJar < AbstractCookieJar # :nodoc:
604
+ include SerializedCookieJars
605
+
606
+ def initialize(parent_jar)
607
+ super
608
+
609
+ if request.use_authenticated_cookie_encryption
610
+ key_len = ActiveSupport::MessageEncryptor.key_len(encrypted_cookie_cipher)
611
+ secret = request.key_generator.generate_key(request.authenticated_encrypted_cookie_salt, key_len)
612
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, cipher: encrypted_cookie_cipher, serializer: SERIALIZER)
613
+ else
614
+ key_len = ActiveSupport::MessageEncryptor.key_len("aes-256-cbc")
615
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, key_len)
616
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
617
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret, cipher: "aes-256-cbc", serializer: SERIALIZER)
618
+ end
619
+
620
+ request.cookies_rotations.encrypted.each do |*secrets, **options|
621
+ @encryptor.rotate(*secrets, serializer: SERIALIZER, **options)
622
+ end
623
+
624
+ if upgrade_legacy_hmac_aes_cbc_cookies?
625
+ legacy_cipher = "aes-256-cbc"
626
+ secret = request.key_generator.generate_key(request.encrypted_cookie_salt, ActiveSupport::MessageEncryptor.key_len(legacy_cipher))
627
+ sign_secret = request.key_generator.generate_key(request.encrypted_signed_cookie_salt)
628
+
629
+ @encryptor.rotate(secret, sign_secret, cipher: legacy_cipher, digest: digest, serializer: SERIALIZER)
630
+ end
631
+
632
+ if upgrade_legacy_signed_cookies?
633
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(request.secret_token, digest: digest, serializer: SERIALIZER)
634
+ end
635
+ end
636
+
637
+ private
638
+ def parse(name, encrypted_message)
639
+ deserialize(name) do |rotate|
640
+ @encryptor.decrypt_and_verify(encrypted_message, on_rotation: rotate)
641
+ end
642
+ rescue ActiveSupport::MessageEncryptor::InvalidMessage, ActiveSupport::MessageVerifier::InvalidSignature
643
+ parse_legacy_signed_message(name, encrypted_message)
644
+ end
645
+
646
+ def commit(options)
647
+ options[:value] = @encryptor.encrypt_and_sign(serialize(options[:value]), expiry_options(options))
648
+
649
+ raise CookieOverflow if options[:value].bytesize > MAX_COOKIE_SIZE
650
+ end
651
+
652
+ def parse_legacy_signed_message(name, legacy_signed_message)
653
+ if defined?(@legacy_verifier)
654
+ deserialize(name) do |rotate|
655
+ rotate.call
656
+
657
+ @legacy_verifier.verified(legacy_signed_message)
658
+ end
659
+ end
660
+ end
661
+ end
662
+
663
+ def initialize(app)
664
+ @app = app
665
+ end
666
+
667
+ def call(env)
668
+ request = ActionDispatch::Request.new env
669
+
670
+ status, headers, body = @app.call(env)
671
+
672
+ if request.have_cookie_jar?
673
+ cookie_jar = request.cookie_jar
674
+ unless cookie_jar.committed?
675
+ cookie_jar.write(headers)
676
+ if headers[HTTP_HEADER].respond_to?(:join)
677
+ headers[HTTP_HEADER] = headers[HTTP_HEADER].join("\n")
678
+ end
679
+ end
680
+ end
681
+
682
+ [status, headers, body]
683
+ end
684
+ end
685
+ end