sidekiq 4.1.4 → 6.5.6

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

Potentially problematic release.


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

Files changed (213) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +666 -0
  3. data/LICENSE +3 -3
  4. data/README.md +27 -35
  5. data/bin/sidekiq +27 -3
  6. data/bin/sidekiqload +78 -84
  7. data/bin/sidekiqmon +8 -0
  8. data/lib/generators/sidekiq/job_generator.rb +57 -0
  9. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  10. data/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  11. data/lib/generators/sidekiq/templates/job_test.rb.erb +8 -0
  12. data/lib/sidekiq/api.rb +583 -288
  13. data/lib/sidekiq/cli.rb +255 -218
  14. data/lib/sidekiq/client.rb +86 -83
  15. data/lib/sidekiq/component.rb +65 -0
  16. data/lib/sidekiq/delay.rb +43 -0
  17. data/lib/sidekiq/extensions/action_mailer.rb +13 -22
  18. data/lib/sidekiq/extensions/active_record.rb +13 -10
  19. data/lib/sidekiq/extensions/class_methods.rb +14 -11
  20. data/lib/sidekiq/extensions/generic_proxy.rb +13 -5
  21. data/lib/sidekiq/fetch.rb +50 -40
  22. data/lib/sidekiq/job.rb +13 -0
  23. data/lib/sidekiq/job_logger.rb +51 -0
  24. data/lib/sidekiq/job_retry.rb +282 -0
  25. data/lib/sidekiq/job_util.rb +71 -0
  26. data/lib/sidekiq/launcher.rb +192 -85
  27. data/lib/sidekiq/logger.rb +156 -0
  28. data/lib/sidekiq/manager.rb +44 -45
  29. data/lib/sidekiq/metrics/deploy.rb +47 -0
  30. data/lib/sidekiq/metrics/query.rb +153 -0
  31. data/lib/sidekiq/metrics/shared.rb +94 -0
  32. data/lib/sidekiq/metrics/tracking.rb +134 -0
  33. data/lib/sidekiq/middleware/chain.rb +102 -46
  34. data/lib/sidekiq/middleware/current_attributes.rb +63 -0
  35. data/lib/sidekiq/middleware/i18n.rb +7 -7
  36. data/lib/sidekiq/middleware/modules.rb +21 -0
  37. data/lib/sidekiq/monitor.rb +133 -0
  38. data/lib/sidekiq/paginator.rb +20 -16
  39. data/lib/sidekiq/processor.rb +178 -78
  40. data/lib/sidekiq/rails.rb +56 -27
  41. data/lib/sidekiq/redis_client_adapter.rb +154 -0
  42. data/lib/sidekiq/redis_connection.rb +123 -47
  43. data/lib/sidekiq/ring_buffer.rb +29 -0
  44. data/lib/sidekiq/scheduled.rb +97 -40
  45. data/lib/sidekiq/sd_notify.rb +149 -0
  46. data/lib/sidekiq/systemd.rb +24 -0
  47. data/lib/sidekiq/testing/inline.rb +6 -5
  48. data/lib/sidekiq/testing.rb +83 -56
  49. data/lib/sidekiq/transaction_aware_client.rb +45 -0
  50. data/lib/sidekiq/version.rb +2 -1
  51. data/lib/sidekiq/web/action.rb +93 -0
  52. data/lib/sidekiq/web/application.rb +379 -0
  53. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  54. data/lib/sidekiq/web/helpers.rb +365 -0
  55. data/lib/sidekiq/web/router.rb +104 -0
  56. data/lib/sidekiq/web.rb +108 -213
  57. data/lib/sidekiq/worker.rb +288 -42
  58. data/lib/sidekiq.rb +188 -80
  59. data/sidekiq.gemspec +24 -22
  60. data/web/assets/images/apple-touch-icon.png +0 -0
  61. data/web/assets/images/{status-sd8051fd480.png → status.png} +0 -0
  62. data/web/assets/javascripts/application.js +130 -75
  63. data/web/assets/javascripts/chart.min.js +13 -0
  64. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  65. data/web/assets/javascripts/dashboard.js +70 -91
  66. data/web/assets/javascripts/graph.js +16 -0
  67. data/web/assets/javascripts/metrics.js +262 -0
  68. data/web/assets/stylesheets/application-dark.css +143 -0
  69. data/web/assets/stylesheets/application-rtl.css +242 -0
  70. data/web/assets/stylesheets/application.css +390 -145
  71. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  72. data/web/assets/stylesheets/bootstrap.css +4 -8
  73. data/web/locales/ar.yml +87 -0
  74. data/web/locales/de.yml +15 -3
  75. data/web/locales/el.yml +43 -19
  76. data/web/locales/en.yml +15 -1
  77. data/web/locales/es.yml +22 -5
  78. data/web/locales/fa.yml +80 -0
  79. data/web/locales/fr.yml +10 -3
  80. data/web/locales/he.yml +79 -0
  81. data/web/locales/ja.yml +12 -4
  82. data/web/locales/lt.yml +83 -0
  83. data/web/locales/pl.yml +4 -4
  84. data/web/locales/pt-br.yml +27 -9
  85. data/web/locales/ru.yml +4 -0
  86. data/web/locales/ur.yml +80 -0
  87. data/web/locales/vi.yml +83 -0
  88. data/web/views/_footer.erb +6 -3
  89. data/web/views/_job_info.erb +4 -3
  90. data/web/views/_nav.erb +5 -19
  91. data/web/views/_paging.erb +1 -1
  92. data/web/views/_poll_link.erb +2 -5
  93. data/web/views/_summary.erb +7 -7
  94. data/web/views/busy.erb +64 -26
  95. data/web/views/dashboard.erb +26 -17
  96. data/web/views/dead.erb +4 -4
  97. data/web/views/layout.erb +15 -5
  98. data/web/views/metrics.erb +69 -0
  99. data/web/views/metrics_for_job.erb +87 -0
  100. data/web/views/morgue.erb +21 -14
  101. data/web/views/queue.erb +28 -14
  102. data/web/views/queues.erb +15 -5
  103. data/web/views/retries.erb +24 -15
  104. data/web/views/retry.erb +5 -5
  105. data/web/views/scheduled.erb +9 -6
  106. data/web/views/scheduled_job_info.erb +1 -1
  107. metadata +73 -256
  108. data/.github/contributing.md +0 -32
  109. data/.github/issue_template.md +0 -4
  110. data/.gitignore +0 -12
  111. data/.travis.yml +0 -18
  112. data/3.0-Upgrade.md +0 -70
  113. data/4.0-Upgrade.md +0 -53
  114. data/COMM-LICENSE +0 -95
  115. data/Ent-Changes.md +0 -123
  116. data/Gemfile +0 -29
  117. data/Pro-2.0-Upgrade.md +0 -138
  118. data/Pro-3.0-Upgrade.md +0 -44
  119. data/Pro-Changes.md +0 -559
  120. data/Rakefile +0 -9
  121. data/bin/sidekiqctl +0 -99
  122. data/code_of_conduct.md +0 -50
  123. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  124. data/lib/generators/sidekiq/templates/worker_test.rb.erb +0 -8
  125. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  126. data/lib/sidekiq/core_ext.rb +0 -106
  127. data/lib/sidekiq/exception_handler.rb +0 -31
  128. data/lib/sidekiq/logging.rb +0 -106
  129. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  130. data/lib/sidekiq/middleware/server/logging.rb +0 -40
  131. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  132. data/lib/sidekiq/util.rb +0 -62
  133. data/lib/sidekiq/web_helpers.rb +0 -255
  134. data/test/config.yml +0 -9
  135. data/test/env_based_config.yml +0 -11
  136. data/test/fake_env.rb +0 -1
  137. data/test/fixtures/en.yml +0 -2
  138. data/test/helper.rb +0 -75
  139. data/test/test_actors.rb +0 -138
  140. data/test/test_api.rb +0 -528
  141. data/test/test_cli.rb +0 -406
  142. data/test/test_client.rb +0 -266
  143. data/test/test_exception_handler.rb +0 -56
  144. data/test/test_extensions.rb +0 -127
  145. data/test/test_fetch.rb +0 -50
  146. data/test/test_launcher.rb +0 -85
  147. data/test/test_logging.rb +0 -35
  148. data/test/test_manager.rb +0 -50
  149. data/test/test_middleware.rb +0 -158
  150. data/test/test_processor.rb +0 -201
  151. data/test/test_rails.rb +0 -22
  152. data/test/test_redis_connection.rb +0 -132
  153. data/test/test_retry.rb +0 -326
  154. data/test/test_retry_exhausted.rb +0 -149
  155. data/test/test_scheduled.rb +0 -115
  156. data/test/test_scheduling.rb +0 -50
  157. data/test/test_sidekiq.rb +0 -107
  158. data/test/test_testing.rb +0 -143
  159. data/test/test_testing_fake.rb +0 -357
  160. data/test/test_testing_inline.rb +0 -94
  161. data/test/test_util.rb +0 -13
  162. data/test/test_web.rb +0 -614
  163. data/test/test_web_helpers.rb +0 -54
  164. data/web/assets/images/bootstrap/glyphicons-halflings-white.png +0 -0
  165. data/web/assets/images/bootstrap/glyphicons-halflings.png +0 -0
  166. data/web/assets/images/status/active.png +0 -0
  167. data/web/assets/images/status/idle.png +0 -0
  168. data/web/assets/javascripts/locales/README.md +0 -27
  169. data/web/assets/javascripts/locales/jquery.timeago.ar.js +0 -96
  170. data/web/assets/javascripts/locales/jquery.timeago.bg.js +0 -18
  171. data/web/assets/javascripts/locales/jquery.timeago.bs.js +0 -49
  172. data/web/assets/javascripts/locales/jquery.timeago.ca.js +0 -18
  173. data/web/assets/javascripts/locales/jquery.timeago.cs.js +0 -18
  174. data/web/assets/javascripts/locales/jquery.timeago.cy.js +0 -20
  175. data/web/assets/javascripts/locales/jquery.timeago.da.js +0 -18
  176. data/web/assets/javascripts/locales/jquery.timeago.de.js +0 -18
  177. data/web/assets/javascripts/locales/jquery.timeago.el.js +0 -18
  178. data/web/assets/javascripts/locales/jquery.timeago.en-short.js +0 -20
  179. data/web/assets/javascripts/locales/jquery.timeago.en.js +0 -20
  180. data/web/assets/javascripts/locales/jquery.timeago.es.js +0 -18
  181. data/web/assets/javascripts/locales/jquery.timeago.et.js +0 -18
  182. data/web/assets/javascripts/locales/jquery.timeago.fa.js +0 -22
  183. data/web/assets/javascripts/locales/jquery.timeago.fi.js +0 -28
  184. data/web/assets/javascripts/locales/jquery.timeago.fr-short.js +0 -16
  185. data/web/assets/javascripts/locales/jquery.timeago.fr.js +0 -17
  186. data/web/assets/javascripts/locales/jquery.timeago.he.js +0 -18
  187. data/web/assets/javascripts/locales/jquery.timeago.hr.js +0 -49
  188. data/web/assets/javascripts/locales/jquery.timeago.hu.js +0 -18
  189. data/web/assets/javascripts/locales/jquery.timeago.hy.js +0 -18
  190. data/web/assets/javascripts/locales/jquery.timeago.id.js +0 -18
  191. data/web/assets/javascripts/locales/jquery.timeago.it.js +0 -16
  192. data/web/assets/javascripts/locales/jquery.timeago.ja.js +0 -19
  193. data/web/assets/javascripts/locales/jquery.timeago.ko.js +0 -17
  194. data/web/assets/javascripts/locales/jquery.timeago.lt.js +0 -20
  195. data/web/assets/javascripts/locales/jquery.timeago.mk.js +0 -20
  196. data/web/assets/javascripts/locales/jquery.timeago.nb.js +0 -18
  197. data/web/assets/javascripts/locales/jquery.timeago.nl.js +0 -20
  198. data/web/assets/javascripts/locales/jquery.timeago.pl.js +0 -31
  199. data/web/assets/javascripts/locales/jquery.timeago.pt-br.js +0 -16
  200. data/web/assets/javascripts/locales/jquery.timeago.pt.js +0 -16
  201. data/web/assets/javascripts/locales/jquery.timeago.ro.js +0 -18
  202. data/web/assets/javascripts/locales/jquery.timeago.rs.js +0 -49
  203. data/web/assets/javascripts/locales/jquery.timeago.ru.js +0 -34
  204. data/web/assets/javascripts/locales/jquery.timeago.sk.js +0 -18
  205. data/web/assets/javascripts/locales/jquery.timeago.sl.js +0 -44
  206. data/web/assets/javascripts/locales/jquery.timeago.sv.js +0 -18
  207. data/web/assets/javascripts/locales/jquery.timeago.th.js +0 -20
  208. data/web/assets/javascripts/locales/jquery.timeago.tr.js +0 -16
  209. data/web/assets/javascripts/locales/jquery.timeago.uk.js +0 -34
  210. data/web/assets/javascripts/locales/jquery.timeago.uz.js +0 -19
  211. data/web/assets/javascripts/locales/jquery.timeago.zh-cn.js +0 -20
  212. data/web/assets/javascripts/locales/jquery.timeago.zh-tw.js +0 -20
  213. data/web/views/_poll_js.erb +0 -5
@@ -0,0 +1,379 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sidekiq
4
+ class WebApplication
5
+ extend WebRouter
6
+
7
+ REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
8
+ CSP_HEADER = [
9
+ "default-src 'self' https: http:",
10
+ "child-src 'self'",
11
+ "connect-src 'self' https: http: wss: ws:",
12
+ "font-src 'self' https: http:",
13
+ "frame-src 'self'",
14
+ "img-src 'self' https: http: data:",
15
+ "manifest-src 'self'",
16
+ "media-src 'self'",
17
+ "object-src 'none'",
18
+ "script-src 'self' https: http: 'unsafe-inline'",
19
+ "style-src 'self' https: http: 'unsafe-inline'",
20
+ "worker-src 'self'",
21
+ "base-uri 'self'"
22
+ ].join("; ").freeze
23
+
24
+ def initialize(klass)
25
+ @klass = klass
26
+ end
27
+
28
+ def settings
29
+ @klass.settings
30
+ end
31
+
32
+ def self.settings
33
+ Sidekiq::Web.settings
34
+ end
35
+
36
+ def self.tabs
37
+ Sidekiq::Web.tabs
38
+ end
39
+
40
+ def self.set(key, val)
41
+ # nothing, backwards compatibility
42
+ end
43
+
44
+ head "/" do
45
+ # HEAD / is the cheapest heartbeat possible,
46
+ # it hits Redis to ensure connectivity
47
+ Sidekiq.redis { |c| c.llen("queue:default") }
48
+ ""
49
+ end
50
+
51
+ get "/" do
52
+ @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
53
+ days = (params["days"] || 30).to_i
54
+ return halt(401) if days < 1 || days > 180
55
+
56
+ stats_history = Sidekiq::Stats::History.new(days)
57
+ @processed_history = stats_history.processed
58
+ @failed_history = stats_history.failed
59
+
60
+ erb(:dashboard)
61
+ end
62
+
63
+ get "/metrics" do
64
+ q = Sidekiq::Metrics::Query.new
65
+ @query_result = q.top_jobs
66
+ erb(:metrics)
67
+ end
68
+
69
+ get "/metrics/:name" do
70
+ @name = route_params[:name]
71
+ q = Sidekiq::Metrics::Query.new
72
+ @query_result = q.for_job(@name)
73
+ erb(:metrics_for_job)
74
+ end
75
+
76
+ get "/busy" do
77
+ erb(:busy)
78
+ end
79
+
80
+ post "/busy" do
81
+ if params["identity"]
82
+ p = Sidekiq::Process.new("identity" => params["identity"])
83
+ p.quiet! if params["quiet"]
84
+ p.stop! if params["stop"]
85
+ else
86
+ processes.each do |pro|
87
+ pro.quiet! if params["quiet"]
88
+ pro.stop! if params["stop"]
89
+ end
90
+ end
91
+
92
+ redirect "#{root_path}busy"
93
+ end
94
+
95
+ get "/queues" do
96
+ @queues = Sidekiq::Queue.all
97
+
98
+ erb(:queues)
99
+ end
100
+
101
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
102
+
103
+ get "/queues/:name" do
104
+ @name = route_params[:name]
105
+
106
+ halt(404) if !@name || @name !~ QUEUE_NAME
107
+
108
+ @count = (params["count"] || 25).to_i
109
+ @queue = Sidekiq::Queue.new(@name)
110
+ (@current_page, @total_size, @jobs) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
111
+ @jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
112
+
113
+ erb(:queue)
114
+ end
115
+
116
+ post "/queues/:name" do
117
+ queue = Sidekiq::Queue.new(route_params[:name])
118
+
119
+ if Sidekiq.pro? && params["pause"]
120
+ queue.pause!
121
+ elsif Sidekiq.pro? && params["unpause"]
122
+ queue.unpause!
123
+ else
124
+ queue.clear
125
+ end
126
+
127
+ redirect "#{root_path}queues"
128
+ end
129
+
130
+ post "/queues/:name/delete" do
131
+ name = route_params[:name]
132
+ Sidekiq::JobRecord.new(params["key_val"], name).delete
133
+
134
+ redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
135
+ end
136
+
137
+ get "/morgue" do
138
+ @count = (params["count"] || 25).to_i
139
+ (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
140
+ @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
141
+
142
+ erb(:morgue)
143
+ end
144
+
145
+ get "/morgue/:key" do
146
+ key = route_params[:key]
147
+ halt(404) unless key
148
+
149
+ @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
150
+
151
+ if @dead.nil?
152
+ redirect "#{root_path}morgue"
153
+ else
154
+ erb(:dead)
155
+ end
156
+ end
157
+
158
+ post "/morgue" do
159
+ redirect(request.path) unless params["key"]
160
+
161
+ params["key"].each do |key|
162
+ job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
163
+ retry_or_delete_or_kill job, params if job
164
+ end
165
+
166
+ redirect_with_query("#{root_path}morgue")
167
+ end
168
+
169
+ post "/morgue/all/delete" do
170
+ Sidekiq::DeadSet.new.clear
171
+
172
+ redirect "#{root_path}morgue"
173
+ end
174
+
175
+ post "/morgue/all/retry" do
176
+ Sidekiq::DeadSet.new.retry_all
177
+
178
+ redirect "#{root_path}morgue"
179
+ end
180
+
181
+ post "/morgue/:key" do
182
+ key = route_params[:key]
183
+ halt(404) unless key
184
+
185
+ job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
186
+ retry_or_delete_or_kill job, params if job
187
+
188
+ redirect_with_query("#{root_path}morgue")
189
+ end
190
+
191
+ get "/retries" do
192
+ @count = (params["count"] || 25).to_i
193
+ (@current_page, @total_size, @retries) = page("retry", params["page"], @count)
194
+ @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
195
+
196
+ erb(:retries)
197
+ end
198
+
199
+ get "/retries/:key" do
200
+ @retry = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
201
+
202
+ if @retry.nil?
203
+ redirect "#{root_path}retries"
204
+ else
205
+ erb(:retry)
206
+ end
207
+ end
208
+
209
+ post "/retries" do
210
+ redirect(request.path) unless params["key"]
211
+
212
+ params["key"].each do |key|
213
+ job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
214
+ retry_or_delete_or_kill job, params if job
215
+ end
216
+
217
+ redirect_with_query("#{root_path}retries")
218
+ end
219
+
220
+ post "/retries/all/delete" do
221
+ Sidekiq::RetrySet.new.clear
222
+
223
+ redirect "#{root_path}retries"
224
+ end
225
+
226
+ post "/retries/all/retry" do
227
+ Sidekiq::RetrySet.new.retry_all
228
+
229
+ redirect "#{root_path}retries"
230
+ end
231
+
232
+ post "/retries/all/kill" do
233
+ Sidekiq::RetrySet.new.kill_all
234
+
235
+ redirect "#{root_path}retries"
236
+ end
237
+
238
+ post "/retries/:key" do
239
+ job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
240
+
241
+ retry_or_delete_or_kill job, params if job
242
+
243
+ redirect_with_query("#{root_path}retries")
244
+ end
245
+
246
+ get "/scheduled" do
247
+ @count = (params["count"] || 25).to_i
248
+ (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
249
+ @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
250
+
251
+ erb(:scheduled)
252
+ end
253
+
254
+ get "/scheduled/:key" do
255
+ @job = Sidekiq::ScheduledSet.new.fetch(*parse_params(route_params[:key])).first
256
+
257
+ if @job.nil?
258
+ redirect "#{root_path}scheduled"
259
+ else
260
+ erb(:scheduled_job_info)
261
+ end
262
+ end
263
+
264
+ post "/scheduled" do
265
+ redirect(request.path) unless params["key"]
266
+
267
+ params["key"].each do |key|
268
+ job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
269
+ delete_or_add_queue job, params if job
270
+ end
271
+
272
+ redirect_with_query("#{root_path}scheduled")
273
+ end
274
+
275
+ post "/scheduled/:key" do
276
+ key = route_params[:key]
277
+ halt(404) unless key
278
+
279
+ job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
280
+ delete_or_add_queue job, params if job
281
+
282
+ redirect_with_query("#{root_path}scheduled")
283
+ end
284
+
285
+ get "/dashboard/stats" do
286
+ redirect "#{root_path}stats"
287
+ end
288
+
289
+ get "/stats" do
290
+ sidekiq_stats = Sidekiq::Stats.new
291
+ redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
292
+ json(
293
+ sidekiq: {
294
+ processed: sidekiq_stats.processed,
295
+ failed: sidekiq_stats.failed,
296
+ busy: sidekiq_stats.workers_size,
297
+ processes: sidekiq_stats.processes_size,
298
+ enqueued: sidekiq_stats.enqueued,
299
+ scheduled: sidekiq_stats.scheduled_size,
300
+ retries: sidekiq_stats.retry_size,
301
+ dead: sidekiq_stats.dead_size,
302
+ default_latency: sidekiq_stats.default_queue_latency
303
+ },
304
+ redis: redis_stats,
305
+ server_utc_time: server_utc_time
306
+ )
307
+ end
308
+
309
+ get "/stats/queues" do
310
+ json Sidekiq::Stats::Queues.new.lengths
311
+ end
312
+
313
+ def call(env)
314
+ action = self.class.match(env)
315
+ return [404, {"content-type" => "text/plain", "x-cascade" => "pass"}, ["Not Found"]] unless action
316
+
317
+ app = @klass
318
+ resp = catch(:halt) do
319
+ self.class.run_befores(app, action)
320
+ action.instance_exec env, &action.block
321
+ ensure
322
+ self.class.run_afters(app, action)
323
+ end
324
+
325
+ case resp
326
+ when Array
327
+ # redirects go here
328
+ resp
329
+ else
330
+ # rendered content goes here
331
+ headers = {
332
+ "content-type" => "text/html",
333
+ "cache-control" => "private, no-store",
334
+ "content-language" => action.locale,
335
+ "content-security-policy" => CSP_HEADER
336
+ }
337
+ # we'll let Rack calculate Content-Length for us.
338
+ [200, headers, [resp]]
339
+ end
340
+ end
341
+
342
+ def self.helpers(mod = nil, &block)
343
+ if block
344
+ WebAction.class_eval(&block)
345
+ else
346
+ WebAction.send(:include, mod)
347
+ end
348
+ end
349
+
350
+ def self.before(path = nil, &block)
351
+ befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
352
+ end
353
+
354
+ def self.after(path = nil, &block)
355
+ afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
356
+ end
357
+
358
+ def self.run_befores(app, action)
359
+ run_hooks(befores, app, action)
360
+ end
361
+
362
+ def self.run_afters(app, action)
363
+ run_hooks(afters, app, action)
364
+ end
365
+
366
+ def self.run_hooks(hooks, app, action)
367
+ hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
368
+ .each { |_, b| action.instance_exec(action.env, app, &b) }
369
+ end
370
+
371
+ def self.befores
372
+ @befores ||= []
373
+ end
374
+
375
+ def self.afters
376
+ @afters ||= []
377
+ end
378
+ end
379
+ end
@@ -0,0 +1,180 @@
1
+ # frozen_string_literal: true
2
+
3
+ # this file originally based on authenticity_token.rb from the sinatra/rack-protection project
4
+ #
5
+ # The MIT License (MIT)
6
+ #
7
+ # Copyright (c) 2011-2017 Konstantin Haase
8
+ # Copyright (c) 2015-2017 Zachary Scott
9
+ #
10
+ # Permission is hereby granted, free of charge, to any person obtaining
11
+ # a copy of this software and associated documentation files (the
12
+ # 'Software'), to deal in the Software without restriction, including
13
+ # without limitation the rights to use, copy, modify, merge, publish,
14
+ # distribute, sublicense, and/or sell copies of the Software, and to
15
+ # permit persons to whom the Software is furnished to do so, subject to
16
+ # the following conditions:
17
+ #
18
+ # The above copyright notice and this permission notice shall be
19
+ # included in all copies or substantial portions of the Software.
20
+ #
21
+ # THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
22
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
23
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
24
+ # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
25
+ # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
26
+ # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
27
+ # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
28
+
29
+ require "securerandom"
30
+ require "base64"
31
+ require "rack/request"
32
+
33
+ module Sidekiq
34
+ class Web
35
+ class CsrfProtection
36
+ def initialize(app, options = nil)
37
+ @app = app
38
+ end
39
+
40
+ def call(env)
41
+ accept?(env) ? admit(env) : deny(env)
42
+ end
43
+
44
+ private
45
+
46
+ def admit(env)
47
+ # On each successful request, we create a fresh masked token
48
+ # which will be used in any forms rendered for this request.
49
+ s = session(env)
50
+ s[:csrf] ||= SecureRandom.base64(TOKEN_LENGTH)
51
+ env[:csrf_token] = mask_token(s[:csrf])
52
+ @app.call(env)
53
+ end
54
+
55
+ def safe?(env)
56
+ %w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"]
57
+ end
58
+
59
+ def logger(env)
60
+ @logger ||= (env["rack.logger"] || ::Logger.new(env["rack.errors"]))
61
+ end
62
+
63
+ def deny(env)
64
+ logger(env).warn "attack prevented by #{self.class}"
65
+ [403, {"Content-Type" => "text/plain"}, ["Forbidden"]]
66
+ end
67
+
68
+ def session(env)
69
+ env["rack.session"] || fail(<<~EOM)
70
+ Sidekiq::Web needs a valid Rack session for CSRF protection. If this is a Rails app,
71
+ make sure you mount Sidekiq::Web *inside* your application routes:
72
+
73
+
74
+ Rails.application.routes.draw do
75
+ mount Sidekiq::Web => "/sidekiq"
76
+ ....
77
+ end
78
+
79
+
80
+ If this is a Rails app in API mode, you need to enable sessions.
81
+
82
+ https://guides.rubyonrails.org/api_app.html#using-session-middlewares
83
+
84
+ If this is a bare Rack app, use a session middleware before Sidekiq::Web:
85
+
86
+ # first, use IRB to create a shared secret key for sessions and commit it
87
+ require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
88
+
89
+ # now use the secret with a session cookie middleware
90
+ use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
91
+ run Sidekiq::Web
92
+
93
+ EOM
94
+ end
95
+
96
+ def accept?(env)
97
+ return true if safe?(env)
98
+
99
+ giventoken = ::Rack::Request.new(env).params["authenticity_token"]
100
+ valid_token?(env, giventoken)
101
+ end
102
+
103
+ TOKEN_LENGTH = 32
104
+
105
+ # Checks that the token given to us as a parameter matches
106
+ # the token stored in the session.
107
+ def valid_token?(env, giventoken)
108
+ return false if giventoken.nil? || giventoken.empty?
109
+
110
+ begin
111
+ token = decode_token(giventoken)
112
+ rescue ArgumentError # client input is invalid
113
+ return false
114
+ end
115
+
116
+ sess = session(env)
117
+ localtoken = sess[:csrf]
118
+
119
+ # Checks that Rack::Session::Cookie actualy contains the csrf toekn
120
+ return false if localtoken.nil?
121
+
122
+ # Rotate the session token after every use
123
+ sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
124
+
125
+ # See if it's actually a masked token or not. We should be able
126
+ # to handle any unmasked tokens that we've issued without error.
127
+
128
+ if unmasked_token?(token)
129
+ compare_with_real_token token, localtoken
130
+ elsif masked_token?(token)
131
+ unmasked = unmask_token(token)
132
+ compare_with_real_token unmasked, localtoken
133
+ else
134
+ false # Token is malformed
135
+ end
136
+ end
137
+
138
+ # Creates a masked version of the authenticity token that varies
139
+ # on each request. The masking is used to mitigate SSL attacks
140
+ # like BREACH.
141
+ def mask_token(token)
142
+ token = decode_token(token)
143
+ one_time_pad = SecureRandom.random_bytes(token.length)
144
+ encrypted_token = xor_byte_strings(one_time_pad, token)
145
+ masked_token = one_time_pad + encrypted_token
146
+ Base64.urlsafe_encode64(masked_token)
147
+ end
148
+
149
+ # Essentially the inverse of +mask_token+.
150
+ def unmask_token(masked_token)
151
+ # Split the token into the one-time pad and the encrypted
152
+ # value and decrypt it
153
+ token_length = masked_token.length / 2
154
+ one_time_pad = masked_token[0...token_length]
155
+ encrypted_token = masked_token[token_length..-1]
156
+ xor_byte_strings(one_time_pad, encrypted_token)
157
+ end
158
+
159
+ def unmasked_token?(token)
160
+ token.length == TOKEN_LENGTH
161
+ end
162
+
163
+ def masked_token?(token)
164
+ token.length == TOKEN_LENGTH * 2
165
+ end
166
+
167
+ def compare_with_real_token(token, local)
168
+ ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
169
+ end
170
+
171
+ def decode_token(token)
172
+ Base64.urlsafe_decode64(token)
173
+ end
174
+
175
+ def xor_byte_strings(s1, s2)
176
+ s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack("c*")
177
+ end
178
+ end
179
+ end
180
+ end