sidekiq 5.2.10 → 7.2.0

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 (150) hide show
  1. checksums.yaml +4 -4
  2. data/Changes.md +600 -8
  3. data/LICENSE.txt +9 -0
  4. data/README.md +47 -50
  5. data/bin/sidekiq +22 -3
  6. data/bin/sidekiqload +213 -115
  7. data/bin/sidekiqmon +11 -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/{worker_spec.rb.erb → job_spec.rb.erb} +1 -1
  11. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  12. data/lib/sidekiq/api.rb +557 -354
  13. data/lib/sidekiq/capsule.rb +127 -0
  14. data/lib/sidekiq/cli.rb +204 -226
  15. data/lib/sidekiq/client.rb +127 -102
  16. data/lib/sidekiq/component.rb +68 -0
  17. data/lib/sidekiq/config.rb +287 -0
  18. data/lib/sidekiq/deploy.rb +62 -0
  19. data/lib/sidekiq/embedded.rb +61 -0
  20. data/lib/sidekiq/fetch.rb +49 -42
  21. data/lib/sidekiq/job.rb +374 -0
  22. data/lib/sidekiq/job_logger.rb +33 -7
  23. data/lib/sidekiq/job_retry.rb +147 -108
  24. data/lib/sidekiq/job_util.rb +107 -0
  25. data/lib/sidekiq/launcher.rb +203 -105
  26. data/lib/sidekiq/logger.rb +131 -0
  27. data/lib/sidekiq/manager.rb +43 -46
  28. data/lib/sidekiq/metrics/query.rb +155 -0
  29. data/lib/sidekiq/metrics/shared.rb +95 -0
  30. data/lib/sidekiq/metrics/tracking.rb +136 -0
  31. data/lib/sidekiq/middleware/chain.rb +113 -56
  32. data/lib/sidekiq/middleware/current_attributes.rb +95 -0
  33. data/lib/sidekiq/middleware/i18n.rb +7 -7
  34. data/lib/sidekiq/middleware/modules.rb +21 -0
  35. data/lib/sidekiq/monitor.rb +146 -0
  36. data/lib/sidekiq/paginator.rb +28 -16
  37. data/lib/sidekiq/processor.rb +122 -120
  38. data/lib/sidekiq/rails.rb +48 -38
  39. data/lib/sidekiq/redis_client_adapter.rb +111 -0
  40. data/lib/sidekiq/redis_connection.rb +39 -107
  41. data/lib/sidekiq/ring_buffer.rb +29 -0
  42. data/lib/sidekiq/scheduled.rb +111 -49
  43. data/lib/sidekiq/sd_notify.rb +149 -0
  44. data/lib/sidekiq/systemd.rb +24 -0
  45. data/lib/sidekiq/testing/inline.rb +6 -5
  46. data/lib/sidekiq/testing.rb +90 -89
  47. data/lib/sidekiq/transaction_aware_client.rb +44 -0
  48. data/lib/sidekiq/version.rb +3 -1
  49. data/lib/sidekiq/web/action.rb +15 -11
  50. data/lib/sidekiq/web/application.rb +186 -79
  51. data/lib/sidekiq/web/csrf_protection.rb +180 -0
  52. data/lib/sidekiq/web/helpers.rb +154 -115
  53. data/lib/sidekiq/web/router.rb +23 -19
  54. data/lib/sidekiq/web.rb +68 -107
  55. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  56. data/lib/sidekiq.rb +92 -182
  57. data/sidekiq.gemspec +25 -16
  58. data/web/assets/images/apple-touch-icon.png +0 -0
  59. data/web/assets/javascripts/application.js +146 -61
  60. data/web/assets/javascripts/base-charts.js +106 -0
  61. data/web/assets/javascripts/chart.min.js +13 -0
  62. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  63. data/web/assets/javascripts/dashboard-charts.js +182 -0
  64. data/web/assets/javascripts/dashboard.js +35 -293
  65. data/web/assets/javascripts/metrics.js +298 -0
  66. data/web/assets/stylesheets/application-dark.css +147 -0
  67. data/web/assets/stylesheets/application-rtl.css +2 -95
  68. data/web/assets/stylesheets/application.css +111 -522
  69. data/web/locales/ar.yml +71 -65
  70. data/web/locales/cs.yml +62 -62
  71. data/web/locales/da.yml +60 -53
  72. data/web/locales/de.yml +65 -53
  73. data/web/locales/el.yml +43 -24
  74. data/web/locales/en.yml +86 -66
  75. data/web/locales/es.yml +70 -54
  76. data/web/locales/fa.yml +65 -65
  77. data/web/locales/fr.yml +83 -62
  78. data/web/locales/gd.yml +99 -0
  79. data/web/locales/he.yml +65 -64
  80. data/web/locales/hi.yml +59 -59
  81. data/web/locales/it.yml +53 -53
  82. data/web/locales/ja.yml +75 -64
  83. data/web/locales/ko.yml +52 -52
  84. data/web/locales/lt.yml +83 -0
  85. data/web/locales/nb.yml +61 -61
  86. data/web/locales/nl.yml +52 -52
  87. data/web/locales/pl.yml +45 -45
  88. data/web/locales/pt-br.yml +83 -55
  89. data/web/locales/pt.yml +51 -51
  90. data/web/locales/ru.yml +68 -63
  91. data/web/locales/sv.yml +53 -53
  92. data/web/locales/ta.yml +60 -60
  93. data/web/locales/uk.yml +62 -61
  94. data/web/locales/ur.yml +64 -64
  95. data/web/locales/vi.yml +83 -0
  96. data/web/locales/zh-cn.yml +43 -16
  97. data/web/locales/zh-tw.yml +42 -8
  98. data/web/views/_footer.erb +6 -3
  99. data/web/views/_job_info.erb +21 -4
  100. data/web/views/_metrics_period_select.erb +12 -0
  101. data/web/views/_nav.erb +1 -1
  102. data/web/views/_paging.erb +2 -0
  103. data/web/views/_poll_link.erb +3 -6
  104. data/web/views/_summary.erb +7 -7
  105. data/web/views/busy.erb +77 -27
  106. data/web/views/dashboard.erb +48 -18
  107. data/web/views/dead.erb +3 -3
  108. data/web/views/filtering.erb +7 -0
  109. data/web/views/layout.erb +3 -1
  110. data/web/views/metrics.erb +91 -0
  111. data/web/views/metrics_for_job.erb +59 -0
  112. data/web/views/morgue.erb +14 -15
  113. data/web/views/queue.erb +33 -24
  114. data/web/views/queues.erb +19 -5
  115. data/web/views/retries.erb +16 -17
  116. data/web/views/retry.erb +3 -3
  117. data/web/views/scheduled.erb +17 -15
  118. metadata +71 -71
  119. data/.circleci/config.yml +0 -61
  120. data/.github/contributing.md +0 -32
  121. data/.github/issue_template.md +0 -11
  122. data/.gitignore +0 -15
  123. data/.travis.yml +0 -11
  124. data/3.0-Upgrade.md +0 -70
  125. data/4.0-Upgrade.md +0 -53
  126. data/5.0-Upgrade.md +0 -56
  127. data/COMM-LICENSE +0 -97
  128. data/Ent-Changes.md +0 -238
  129. data/Gemfile +0 -19
  130. data/LICENSE +0 -9
  131. data/Pro-2.0-Upgrade.md +0 -138
  132. data/Pro-3.0-Upgrade.md +0 -44
  133. data/Pro-4.0-Upgrade.md +0 -35
  134. data/Pro-Changes.md +0 -759
  135. data/Rakefile +0 -9
  136. data/bin/sidekiqctl +0 -20
  137. data/code_of_conduct.md +0 -50
  138. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  139. data/lib/sidekiq/core_ext.rb +0 -1
  140. data/lib/sidekiq/ctl.rb +0 -221
  141. data/lib/sidekiq/delay.rb +0 -42
  142. data/lib/sidekiq/exception_handler.rb +0 -29
  143. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  144. data/lib/sidekiq/extensions/active_record.rb +0 -40
  145. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  146. data/lib/sidekiq/extensions/generic_proxy.rb +0 -31
  147. data/lib/sidekiq/logging.rb +0 -122
  148. data/lib/sidekiq/middleware/server/active_record.rb +0 -23
  149. data/lib/sidekiq/util.rb +0 -66
  150. data/lib/sidekiq/worker.rb +0 -220
@@ -4,9 +4,7 @@ module Sidekiq
4
4
  class WebApplication
5
5
  extend WebRouter
6
6
 
7
- CONTENT_LENGTH = "Content-Length"
8
- CONTENT_TYPE = "Content-Type"
9
- REDIS_KEYS = %w(redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human)
7
+ REDIS_KEYS = %w[redis_version uptime_in_days connected_clients used_memory_human used_memory_peak_human]
10
8
  CSP_HEADER = [
11
9
  "default-src 'self' https: http:",
12
10
  "child-src 'self'",
@@ -17,11 +15,17 @@ module Sidekiq
17
15
  "manifest-src 'self'",
18
16
  "media-src 'self'",
19
17
  "object-src 'none'",
20
- "script-src 'self' https: http: 'unsafe-inline'",
18
+ "script-src 'self' https: http:",
21
19
  "style-src 'self' https: http: 'unsafe-inline'",
22
20
  "worker-src 'self'",
23
21
  "base-uri 'self'"
24
- ].join('; ').freeze
22
+ ].join("; ").freeze
23
+ METRICS_PERIODS = {
24
+ "1h" => 60,
25
+ "2h" => 120,
26
+ "4h" => 240,
27
+ "8h" => 480
28
+ }
25
29
 
26
30
  def initialize(klass)
27
31
  @klass = klass
@@ -43,8 +47,15 @@ module Sidekiq
43
47
  # nothing, backwards compatibility
44
48
  end
45
49
 
50
+ head "/" do
51
+ # HEAD / is the cheapest heartbeat possible,
52
+ # it hits Redis to ensure connectivity
53
+ Sidekiq.redis { |c| c.llen("queue:default") }
54
+ ""
55
+ end
56
+
46
57
  get "/" do
47
- @redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
58
+ @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
48
59
  days = (params["days"] || 30).to_i
49
60
  return halt(401) if days < 1 || days > 180
50
61
 
@@ -55,19 +66,44 @@ module Sidekiq
55
66
  erb(:dashboard)
56
67
  end
57
68
 
69
+ get "/metrics" do
70
+ q = Sidekiq::Metrics::Query.new
71
+ @period = h((params[:period] || "")[0..1])
72
+ @periods = METRICS_PERIODS
73
+ minutes = @periods.fetch(@period, @periods.values.first)
74
+ @query_result = q.top_jobs(minutes: minutes)
75
+ erb(:metrics)
76
+ end
77
+
78
+ get "/metrics/:name" do
79
+ @name = route_params[:name]
80
+ @period = h((params[:period] || "")[0..1])
81
+ q = Sidekiq::Metrics::Query.new
82
+ @periods = METRICS_PERIODS
83
+ minutes = @periods.fetch(@period, @periods.values.first)
84
+ @query_result = q.for_job(@name, minutes: minutes)
85
+ erb(:metrics_for_job)
86
+ end
87
+
58
88
  get "/busy" do
89
+ @count = (params["count"] || 100).to_i
90
+ (@current_page, @total_size, @workset) = page_items(workset, params["page"], @count)
91
+
59
92
  erb(:busy)
60
93
  end
61
94
 
62
95
  post "/busy" do
63
- if params['identity']
64
- p = Sidekiq::Process.new('identity' => params['identity'])
65
- p.quiet! if params['quiet']
66
- p.stop! if params['stop']
96
+ if params["identity"]
97
+ pro = Sidekiq::ProcessSet[params["identity"]]
98
+
99
+ pro.quiet! if params["quiet"]
100
+ pro.stop! if params["stop"]
67
101
  else
68
102
  processes.each do |pro|
69
- pro.quiet! if params['quiet']
70
- pro.stop! if params['stop']
103
+ next if pro.embedded?
104
+
105
+ pro.quiet! if params["quiet"]
106
+ pro.stop! if params["stop"]
71
107
  end
72
108
  end
73
109
 
@@ -80,42 +116,53 @@ module Sidekiq
80
116
  erb(:queues)
81
117
  end
82
118
 
119
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
120
+
83
121
  get "/queues/:name" do
84
122
  @name = route_params[:name]
85
123
 
86
- halt(404) unless @name
124
+ halt(404) if !@name || @name !~ QUEUE_NAME
87
125
 
88
- @count = (params['count'] || 25).to_i
126
+ @count = (params["count"] || 25).to_i
89
127
  @queue = Sidekiq::Queue.new(@name)
90
- (@current_page, @total_size, @messages) = page("queue:#{@name}", params['page'], @count)
91
- @messages = @messages.map { |msg| Sidekiq::Job.new(msg, @name) }
128
+ (@current_page, @total_size, @jobs) = page("queue:#{@name}", params["page"], @count, reverse: params["direction"] == "asc")
129
+ @jobs = @jobs.map { |msg| Sidekiq::JobRecord.new(msg, @name) }
92
130
 
93
131
  erb(:queue)
94
132
  end
95
133
 
96
134
  post "/queues/:name" do
97
- Sidekiq::Queue.new(route_params[:name]).clear
135
+ queue = Sidekiq::Queue.new(route_params[:name])
136
+
137
+ if Sidekiq.pro? && params["pause"]
138
+ queue.pause!
139
+ elsif Sidekiq.pro? && params["unpause"]
140
+ queue.unpause!
141
+ else
142
+ queue.clear
143
+ end
98
144
 
99
145
  redirect "#{root_path}queues"
100
146
  end
101
147
 
102
148
  post "/queues/:name/delete" do
103
149
  name = route_params[:name]
104
- Sidekiq::Job.new(params['key_val'], name).delete
150
+ Sidekiq::JobRecord.new(params["key_val"], name).delete
105
151
 
106
152
  redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
107
153
  end
108
154
 
109
- get '/morgue' do
110
- @count = (params['count'] || 25).to_i
111
- (@current_page, @total_size, @dead) = page("dead", params['page'], @count, reverse: true)
155
+ get "/morgue" do
156
+ @count = (params["count"] || 25).to_i
157
+ (@current_page, @total_size, @dead) = page("dead", params["page"], @count, reverse: true)
112
158
  @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
113
159
 
114
160
  erb(:morgue)
115
161
  end
116
162
 
117
163
  get "/morgue/:key" do
118
- halt(404) unless key = route_params[:key]
164
+ key = route_params[:key]
165
+ halt(404) unless key
119
166
 
120
167
  @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
121
168
 
@@ -126,10 +173,10 @@ module Sidekiq
126
173
  end
127
174
  end
128
175
 
129
- post '/morgue' do
130
- redirect(request.path) unless params['key']
176
+ post "/morgue" do
177
+ redirect(request.path) unless params["key"]
131
178
 
132
- params['key'].each do |key|
179
+ params["key"].each do |key|
133
180
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
134
181
  retry_or_delete_or_kill job, params if job
135
182
  end
@@ -150,7 +197,8 @@ module Sidekiq
150
197
  end
151
198
 
152
199
  post "/morgue/:key" do
153
- halt(404) unless key = route_params[:key]
200
+ key = route_params[:key]
201
+ halt(404) unless key
154
202
 
155
203
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
156
204
  retry_or_delete_or_kill job, params if job
@@ -158,9 +206,9 @@ module Sidekiq
158
206
  redirect_with_query("#{root_path}morgue")
159
207
  end
160
208
 
161
- get '/retries' do
162
- @count = (params['count'] || 25).to_i
163
- (@current_page, @total_size, @retries) = page("retry", params['page'], @count)
209
+ get "/retries" do
210
+ @count = (params["count"] || 25).to_i
211
+ (@current_page, @total_size, @retries) = page("retry", params["page"], @count)
164
212
  @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
165
213
 
166
214
  erb(:retries)
@@ -176,10 +224,10 @@ module Sidekiq
176
224
  end
177
225
  end
178
226
 
179
- post '/retries' do
180
- redirect(request.path) unless params['key']
227
+ post "/retries" do
228
+ redirect(request.path) unless params["key"]
181
229
 
182
- params['key'].each do |key|
230
+ params["key"].each do |key|
183
231
  job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
184
232
  retry_or_delete_or_kill job, params if job
185
233
  end
@@ -213,9 +261,9 @@ module Sidekiq
213
261
  redirect_with_query("#{root_path}retries")
214
262
  end
215
263
 
216
- get '/scheduled' do
217
- @count = (params['count'] || 25).to_i
218
- (@current_page, @total_size, @scheduled) = page("schedule", params['page'], @count)
264
+ get "/scheduled" do
265
+ @count = (params["count"] || 25).to_i
266
+ (@current_page, @total_size, @scheduled) = page("schedule", params["page"], @count)
219
267
  @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
220
268
 
221
269
  erb(:scheduled)
@@ -231,10 +279,10 @@ module Sidekiq
231
279
  end
232
280
  end
233
281
 
234
- post '/scheduled' do
235
- redirect(request.path) unless params['key']
282
+ post "/scheduled" do
283
+ redirect(request.path) unless params["key"]
236
284
 
237
- params['key'].each do |key|
285
+ params["key"].each do |key|
238
286
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
239
287
  delete_or_add_queue job, params if job
240
288
  end
@@ -243,7 +291,8 @@ module Sidekiq
243
291
  end
244
292
 
245
293
  post "/scheduled/:key" do
246
- halt(404) unless key = route_params[:key]
294
+ key = route_params[:key]
295
+ halt(404) unless key
247
296
 
248
297
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
249
298
  delete_or_add_queue job, params if job
@@ -251,23 +300,23 @@ module Sidekiq
251
300
  redirect_with_query("#{root_path}scheduled")
252
301
  end
253
302
 
254
- get '/dashboard/stats' do
303
+ get "/dashboard/stats" do
255
304
  redirect "#{root_path}stats"
256
305
  end
257
306
 
258
- get '/stats' do
307
+ get "/stats" do
259
308
  sidekiq_stats = Sidekiq::Stats.new
260
- redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
309
+ redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
261
310
  json(
262
311
  sidekiq: {
263
- processed: sidekiq_stats.processed,
264
- failed: sidekiq_stats.failed,
265
- busy: sidekiq_stats.workers_size,
266
- processes: sidekiq_stats.processes_size,
267
- enqueued: sidekiq_stats.enqueued,
268
- scheduled: sidekiq_stats.scheduled_size,
269
- retries: sidekiq_stats.retry_size,
270
- dead: sidekiq_stats.dead_size,
312
+ processed: sidekiq_stats.processed,
313
+ failed: sidekiq_stats.failed,
314
+ busy: sidekiq_stats.workers_size,
315
+ processes: sidekiq_stats.processes_size,
316
+ enqueued: sidekiq_stats.enqueued,
317
+ scheduled: sidekiq_stats.scheduled_size,
318
+ retries: sidekiq_stats.retry_size,
319
+ dead: sidekiq_stats.dead_size,
271
320
  default_latency: sidekiq_stats.default_queue_latency
272
321
  },
273
322
  redis: redis_stats,
@@ -275,60 +324,118 @@ module Sidekiq
275
324
  )
276
325
  end
277
326
 
278
- get '/stats/queues' do
279
- json Sidekiq::Stats::Queues.new.lengths
327
+ get "/stats/queues" do
328
+ json Sidekiq::Stats.new.queues
329
+ end
330
+
331
+ ########
332
+ # Filtering
333
+
334
+ get "/filter/metrics" do
335
+ redirect "#{root_path}metrics"
336
+ end
337
+
338
+ post "/filter/metrics" do
339
+ x = params[:substr]
340
+ q = Sidekiq::Metrics::Query.new
341
+ @period = h((params[:period] || "")[0..1])
342
+ @periods = METRICS_PERIODS
343
+ minutes = @periods.fetch(@period, @periods.values.first)
344
+ @query_result = q.top_jobs(minutes: minutes, class_filter: Regexp.new(Regexp.escape(x), Regexp::IGNORECASE))
345
+
346
+ erb :metrics
347
+ end
348
+
349
+ get "/filter/retries" do
350
+ x = params[:substr]
351
+ return redirect "#{root_path}retries" unless x && x != ""
352
+
353
+ @retries = search(Sidekiq::RetrySet.new, params[:substr])
354
+ erb :retries
355
+ end
356
+
357
+ post "/filter/retries" do
358
+ x = params[:substr]
359
+ return redirect "#{root_path}retries" unless x && x != ""
360
+
361
+ @retries = search(Sidekiq::RetrySet.new, params[:substr])
362
+ erb :retries
363
+ end
364
+
365
+ get "/filter/scheduled" do
366
+ x = params[:substr]
367
+ return redirect "#{root_path}scheduled" unless x && x != ""
368
+
369
+ @scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
370
+ erb :scheduled
371
+ end
372
+
373
+ post "/filter/scheduled" do
374
+ x = params[:substr]
375
+ return redirect "#{root_path}scheduled" unless x && x != ""
376
+
377
+ @scheduled = search(Sidekiq::ScheduledSet.new, params[:substr])
378
+ erb :scheduled
379
+ end
380
+
381
+ get "/filter/dead" do
382
+ x = params[:substr]
383
+ return redirect "#{root_path}morgue" unless x && x != ""
384
+
385
+ @dead = search(Sidekiq::DeadSet.new, params[:substr])
386
+ erb :morgue
387
+ end
388
+
389
+ post "/filter/dead" do
390
+ x = params[:substr]
391
+ return redirect "#{root_path}morgue" unless x && x != ""
392
+
393
+ @dead = search(Sidekiq::DeadSet.new, params[:substr])
394
+ erb :morgue
280
395
  end
281
396
 
282
397
  def call(env)
283
398
  action = self.class.match(env)
284
- return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not Found"]] unless action
399
+ return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
285
400
 
401
+ app = @klass
286
402
  resp = catch(:halt) do
287
- app = @klass
288
403
  self.class.run_befores(app, action)
289
- begin
290
- resp = action.instance_exec env, &action.block
291
- ensure
292
- self.class.run_afters(app, action)
293
- end
294
-
295
- resp
404
+ action.instance_exec env, &action.block
405
+ ensure
406
+ self.class.run_afters(app, action)
296
407
  end
297
408
 
298
- resp = case resp
409
+ case resp
299
410
  when Array
411
+ # redirects go here
300
412
  resp
301
413
  else
414
+ # rendered content goes here
302
415
  headers = {
303
- "Content-Type" => "text/html",
304
- "Cache-Control" => "no-cache",
305
- "Content-Language" => action.locale,
306
- "Content-Security-Policy" => CSP_HEADER
416
+ Rack::CONTENT_TYPE => "text/html",
417
+ Rack::CACHE_CONTROL => "private, no-store",
418
+ Web::CONTENT_LANGUAGE => action.locale,
419
+ Web::CONTENT_SECURITY_POLICY => CSP_HEADER
307
420
  }
308
-
421
+ # we'll let Rack calculate Content-Length for us.
309
422
  [200, headers, [resp]]
310
423
  end
311
-
312
- resp[1] = resp[1].dup
313
-
314
- resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
315
-
316
- resp
317
424
  end
318
425
 
319
- def self.helpers(mod=nil, &block)
320
- if block_given?
426
+ def self.helpers(mod = nil, &block)
427
+ if block
321
428
  WebAction.class_eval(&block)
322
429
  else
323
430
  WebAction.send(:include, mod)
324
431
  end
325
432
  end
326
433
 
327
- def self.before(path=nil, &block)
434
+ def self.before(path = nil, &block)
328
435
  befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
329
436
  end
330
437
 
331
- def self.after(path=nil, &block)
438
+ def self.after(path = nil, &block)
332
439
  afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
333
440
  end
334
441
 
@@ -341,8 +448,8 @@ module Sidekiq
341
448
  end
342
449
 
343
450
  def self.run_hooks(hooks, app, action)
344
- hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }.
345
- each {|_,b| action.instance_exec(action.env, app, &b) }
451
+ hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
452
+ .each { |_, b| action.instance_exec(action.env, app, &b) }
346
453
  end
347
454
 
348
455
  def self.befores
@@ -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, {Rack::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..]
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