sidekiq 4.2.10 → 7.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (158) hide show
  1. checksums.yaml +5 -5
  2. data/Changes.md +859 -7
  3. data/LICENSE.txt +9 -0
  4. data/README.md +49 -50
  5. data/bin/multi_queue_bench +271 -0
  6. data/bin/sidekiq +22 -3
  7. data/bin/sidekiqload +212 -119
  8. data/bin/sidekiqmon +11 -0
  9. data/lib/generators/sidekiq/job_generator.rb +59 -0
  10. data/lib/generators/sidekiq/templates/{worker.rb.erb → job.rb.erb} +2 -2
  11. data/lib/generators/sidekiq/templates/job_spec.rb.erb +6 -0
  12. data/lib/generators/sidekiq/templates/{worker_test.rb.erb → job_test.rb.erb} +1 -1
  13. data/lib/sidekiq/api.rb +680 -315
  14. data/lib/sidekiq/capsule.rb +132 -0
  15. data/lib/sidekiq/cli.rb +268 -248
  16. data/lib/sidekiq/client.rb +136 -101
  17. data/lib/sidekiq/component.rb +68 -0
  18. data/lib/sidekiq/config.rb +293 -0
  19. data/lib/sidekiq/deploy.rb +64 -0
  20. data/lib/sidekiq/embedded.rb +63 -0
  21. data/lib/sidekiq/fetch.rb +49 -42
  22. data/lib/sidekiq/iterable_job.rb +55 -0
  23. data/lib/sidekiq/job/interrupt_handler.rb +24 -0
  24. data/lib/sidekiq/job/iterable/active_record_enumerator.rb +53 -0
  25. data/lib/sidekiq/job/iterable/csv_enumerator.rb +47 -0
  26. data/lib/sidekiq/job/iterable/enumerators.rb +135 -0
  27. data/lib/sidekiq/job/iterable.rb +231 -0
  28. data/lib/sidekiq/job.rb +385 -0
  29. data/lib/sidekiq/job_logger.rb +62 -0
  30. data/lib/sidekiq/job_retry.rb +305 -0
  31. data/lib/sidekiq/job_util.rb +109 -0
  32. data/lib/sidekiq/launcher.rb +208 -108
  33. data/lib/sidekiq/logger.rb +131 -0
  34. data/lib/sidekiq/manager.rb +43 -47
  35. data/lib/sidekiq/metrics/query.rb +158 -0
  36. data/lib/sidekiq/metrics/shared.rb +97 -0
  37. data/lib/sidekiq/metrics/tracking.rb +148 -0
  38. data/lib/sidekiq/middleware/chain.rb +113 -56
  39. data/lib/sidekiq/middleware/current_attributes.rb +113 -0
  40. data/lib/sidekiq/middleware/i18n.rb +7 -7
  41. data/lib/sidekiq/middleware/modules.rb +23 -0
  42. data/lib/sidekiq/monitor.rb +147 -0
  43. data/lib/sidekiq/paginator.rb +28 -16
  44. data/lib/sidekiq/processor.rb +188 -98
  45. data/lib/sidekiq/rails.rb +46 -97
  46. data/lib/sidekiq/redis_client_adapter.rb +114 -0
  47. data/lib/sidekiq/redis_connection.rb +71 -73
  48. data/lib/sidekiq/ring_buffer.rb +31 -0
  49. data/lib/sidekiq/scheduled.rb +140 -51
  50. data/lib/sidekiq/sd_notify.rb +149 -0
  51. data/lib/sidekiq/systemd.rb +26 -0
  52. data/lib/sidekiq/testing/inline.rb +6 -5
  53. data/lib/sidekiq/testing.rb +95 -85
  54. data/lib/sidekiq/transaction_aware_client.rb +51 -0
  55. data/lib/sidekiq/version.rb +3 -1
  56. data/lib/sidekiq/web/action.rb +22 -16
  57. data/lib/sidekiq/web/application.rb +230 -86
  58. data/lib/sidekiq/web/csrf_protection.rb +183 -0
  59. data/lib/sidekiq/web/helpers.rb +241 -104
  60. data/lib/sidekiq/web/router.rb +23 -19
  61. data/lib/sidekiq/web.rb +118 -110
  62. data/lib/sidekiq/worker_compatibility_alias.rb +13 -0
  63. data/lib/sidekiq.rb +96 -185
  64. data/sidekiq.gemspec +26 -27
  65. data/web/assets/images/apple-touch-icon.png +0 -0
  66. data/web/assets/javascripts/application.js +157 -61
  67. data/web/assets/javascripts/base-charts.js +106 -0
  68. data/web/assets/javascripts/chart.min.js +13 -0
  69. data/web/assets/javascripts/chartjs-plugin-annotation.min.js +7 -0
  70. data/web/assets/javascripts/dashboard-charts.js +192 -0
  71. data/web/assets/javascripts/dashboard.js +37 -280
  72. data/web/assets/javascripts/metrics.js +298 -0
  73. data/web/assets/stylesheets/application-dark.css +147 -0
  74. data/web/assets/stylesheets/application-rtl.css +163 -0
  75. data/web/assets/stylesheets/application.css +173 -198
  76. data/web/assets/stylesheets/bootstrap-rtl.min.css +9 -0
  77. data/web/assets/stylesheets/bootstrap.css +2 -2
  78. data/web/locales/ar.yml +87 -0
  79. data/web/locales/cs.yml +62 -62
  80. data/web/locales/da.yml +60 -53
  81. data/web/locales/de.yml +65 -53
  82. data/web/locales/el.yml +43 -24
  83. data/web/locales/en.yml +86 -64
  84. data/web/locales/es.yml +70 -53
  85. data/web/locales/fa.yml +65 -64
  86. data/web/locales/fr.yml +83 -62
  87. data/web/locales/gd.yml +99 -0
  88. data/web/locales/he.yml +80 -0
  89. data/web/locales/hi.yml +59 -59
  90. data/web/locales/it.yml +53 -53
  91. data/web/locales/ja.yml +75 -62
  92. data/web/locales/ko.yml +52 -52
  93. data/web/locales/lt.yml +83 -0
  94. data/web/locales/nb.yml +61 -61
  95. data/web/locales/nl.yml +52 -52
  96. data/web/locales/pl.yml +45 -45
  97. data/web/locales/pt-br.yml +83 -55
  98. data/web/locales/pt.yml +51 -51
  99. data/web/locales/ru.yml +68 -63
  100. data/web/locales/sv.yml +53 -53
  101. data/web/locales/ta.yml +60 -60
  102. data/web/locales/tr.yml +101 -0
  103. data/web/locales/uk.yml +62 -61
  104. data/web/locales/ur.yml +80 -0
  105. data/web/locales/vi.yml +83 -0
  106. data/web/locales/zh-cn.yml +43 -16
  107. data/web/locales/zh-tw.yml +42 -8
  108. data/web/views/_footer.erb +21 -3
  109. data/web/views/_job_info.erb +21 -4
  110. data/web/views/_metrics_period_select.erb +12 -0
  111. data/web/views/_nav.erb +5 -19
  112. data/web/views/_paging.erb +3 -1
  113. data/web/views/_poll_link.erb +3 -6
  114. data/web/views/_summary.erb +7 -7
  115. data/web/views/busy.erb +85 -31
  116. data/web/views/dashboard.erb +50 -20
  117. data/web/views/dead.erb +3 -3
  118. data/web/views/filtering.erb +7 -0
  119. data/web/views/layout.erb +17 -6
  120. data/web/views/metrics.erb +91 -0
  121. data/web/views/metrics_for_job.erb +59 -0
  122. data/web/views/morgue.erb +14 -15
  123. data/web/views/queue.erb +34 -24
  124. data/web/views/queues.erb +20 -4
  125. data/web/views/retries.erb +19 -16
  126. data/web/views/retry.erb +3 -3
  127. data/web/views/scheduled.erb +19 -17
  128. metadata +91 -198
  129. data/.github/contributing.md +0 -32
  130. data/.github/issue_template.md +0 -9
  131. data/.gitignore +0 -12
  132. data/.travis.yml +0 -18
  133. data/3.0-Upgrade.md +0 -70
  134. data/4.0-Upgrade.md +0 -53
  135. data/COMM-LICENSE +0 -95
  136. data/Ent-Changes.md +0 -173
  137. data/Gemfile +0 -29
  138. data/LICENSE +0 -9
  139. data/Pro-2.0-Upgrade.md +0 -138
  140. data/Pro-3.0-Upgrade.md +0 -44
  141. data/Pro-Changes.md +0 -628
  142. data/Rakefile +0 -12
  143. data/bin/sidekiqctl +0 -99
  144. data/code_of_conduct.md +0 -50
  145. data/lib/generators/sidekiq/templates/worker_spec.rb.erb +0 -6
  146. data/lib/generators/sidekiq/worker_generator.rb +0 -49
  147. data/lib/sidekiq/core_ext.rb +0 -119
  148. data/lib/sidekiq/exception_handler.rb +0 -31
  149. data/lib/sidekiq/extensions/action_mailer.rb +0 -57
  150. data/lib/sidekiq/extensions/active_record.rb +0 -40
  151. data/lib/sidekiq/extensions/class_methods.rb +0 -40
  152. data/lib/sidekiq/extensions/generic_proxy.rb +0 -25
  153. data/lib/sidekiq/logging.rb +0 -106
  154. data/lib/sidekiq/middleware/server/active_record.rb +0 -13
  155. data/lib/sidekiq/middleware/server/logging.rb +0 -31
  156. data/lib/sidekiq/middleware/server/retry_jobs.rb +0 -205
  157. data/lib/sidekiq/util.rb +0 -63
  158. data/lib/sidekiq/worker.rb +0 -121
@@ -4,9 +4,28 @@ module Sidekiq
4
4
  class WebApplication
5
5
  extend WebRouter
6
6
 
7
- CONTENT_LENGTH = "Content-Length".freeze
8
- CONTENT_TYPE = "Content-Type".freeze
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]
8
+ CSP_HEADER_TEMPLATE = [
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' 'nonce-!placeholder!'",
19
+ "style-src 'self' https: http: 'unsafe-inline'", # TODO Nonce in 8.0
20
+ "worker-src 'self'",
21
+ "base-uri 'self'"
22
+ ].join("; ").freeze
23
+ METRICS_PERIODS = {
24
+ "1h" => 60,
25
+ "2h" => 120,
26
+ "4h" => 240,
27
+ "8h" => 480
28
+ }
10
29
 
11
30
  def initialize(klass)
12
31
  @klass = klass
@@ -28,28 +47,63 @@ module Sidekiq
28
47
  # nothing, backwards compatibility
29
48
  end
30
49
 
50
+ head "/" do
51
+ # HEAD / is the cheapest heartbeat possible,
52
+ # it hits Redis to ensure connectivity and returns
53
+ # the size of the default queue
54
+ Sidekiq.redis { |c| c.llen("queue:default") }.to_s
55
+ end
56
+
31
57
  get "/" do
32
- @redis_info = redis_info.select{ |k, v| REDIS_KEYS.include? k }
33
- stats_history = Sidekiq::Stats::History.new((params['days'] || 30).to_i)
58
+ @redis_info = redis_info.select { |k, v| REDIS_KEYS.include? k }
59
+ days = (params["days"] || 30).to_i
60
+ return halt(401) if days < 1 || days > 180
61
+
62
+ stats_history = Sidekiq::Stats::History.new(days)
34
63
  @processed_history = stats_history.processed
35
64
  @failed_history = stats_history.failed
36
65
 
37
66
  erb(:dashboard)
38
67
  end
39
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
+
40
88
  get "/busy" do
89
+ @count = (params["count"] || 100).to_i
90
+ (@current_page, @total_size, @workset) = page_items(workset, params["page"], @count)
91
+
41
92
  erb(:busy)
42
93
  end
43
94
 
44
95
  post "/busy" do
45
- if params['identity']
46
- p = Sidekiq::Process.new('identity' => params['identity'])
47
- p.quiet! if params['quiet']
48
- 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"]
49
101
  else
50
102
  processes.each do |pro|
51
- pro.quiet! if params['quiet']
52
- pro.stop! if params['stop']
103
+ next if pro.embedded?
104
+
105
+ pro.quiet! if params["quiet"]
106
+ pro.stop! if params["stop"]
53
107
  end
54
108
  end
55
109
 
@@ -62,42 +116,53 @@ module Sidekiq
62
116
  erb(:queues)
63
117
  end
64
118
 
119
+ QUEUE_NAME = /\A[a-z_:.\-0-9]+\z/i
120
+
65
121
  get "/queues/:name" do
66
122
  @name = route_params[:name]
67
123
 
68
- halt(404) unless @name
124
+ halt(404) if !@name || @name !~ QUEUE_NAME
69
125
 
70
- @count = (params['count'] || 25).to_i
126
+ @count = (params["count"] || 25).to_i
71
127
  @queue = Sidekiq::Queue.new(@name)
72
- (@current_page, @total_size, @messages) = page("queue:#{@name}", params['page'], @count)
73
- @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) }
74
130
 
75
131
  erb(:queue)
76
132
  end
77
133
 
78
134
  post "/queues/:name" do
79
- 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
80
144
 
81
145
  redirect "#{root_path}queues"
82
146
  end
83
147
 
84
148
  post "/queues/:name/delete" do
85
149
  name = route_params[:name]
86
- Sidekiq::Job.new(params['key_val'], name).delete
150
+ Sidekiq::JobRecord.new(params["key_val"], name).delete
87
151
 
88
152
  redirect_with_query("#{root_path}queues/#{CGI.escape(name)}")
89
153
  end
90
154
 
91
- get '/morgue' do
92
- @count = (params['count'] || 25).to_i
93
- (@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)
94
158
  @dead = @dead.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
95
159
 
96
160
  erb(:morgue)
97
161
  end
98
162
 
99
163
  get "/morgue/:key" do
100
- halt(404) unless key = route_params[:key]
164
+ key = route_params[:key]
165
+ halt(404) unless key
101
166
 
102
167
  @dead = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
103
168
 
@@ -108,10 +173,10 @@ module Sidekiq
108
173
  end
109
174
  end
110
175
 
111
- post '/morgue' do
112
- redirect(request.path) unless params['key']
176
+ post "/morgue" do
177
+ redirect(request.path) unless params["key"]
113
178
 
114
- params['key'].each do |key|
179
+ params["key"].each do |key|
115
180
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
116
181
  retry_or_delete_or_kill job, params if job
117
182
  end
@@ -132,7 +197,8 @@ module Sidekiq
132
197
  end
133
198
 
134
199
  post "/morgue/:key" do
135
- halt(404) unless key = route_params[:key]
200
+ key = route_params[:key]
201
+ halt(404) unless key
136
202
 
137
203
  job = Sidekiq::DeadSet.new.fetch(*parse_params(key)).first
138
204
  retry_or_delete_or_kill job, params if job
@@ -140,9 +206,9 @@ module Sidekiq
140
206
  redirect_with_query("#{root_path}morgue")
141
207
  end
142
208
 
143
- get '/retries' do
144
- @count = (params['count'] || 25).to_i
145
- (@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)
146
212
  @retries = @retries.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
147
213
 
148
214
  erb(:retries)
@@ -158,10 +224,10 @@ module Sidekiq
158
224
  end
159
225
  end
160
226
 
161
- post '/retries' do
162
- redirect(request.path) unless params['key']
227
+ post "/retries" do
228
+ redirect(request.path) unless params["key"]
163
229
 
164
- params['key'].each do |key|
230
+ params["key"].each do |key|
165
231
  job = Sidekiq::RetrySet.new.fetch(*parse_params(key)).first
166
232
  retry_or_delete_or_kill job, params if job
167
233
  end
@@ -181,6 +247,12 @@ module Sidekiq
181
247
  redirect "#{root_path}retries"
182
248
  end
183
249
 
250
+ post "/retries/all/kill" do
251
+ Sidekiq::RetrySet.new.kill_all
252
+
253
+ redirect "#{root_path}retries"
254
+ end
255
+
184
256
  post "/retries/:key" do
185
257
  job = Sidekiq::RetrySet.new.fetch(*parse_params(route_params[:key])).first
186
258
 
@@ -189,9 +261,9 @@ module Sidekiq
189
261
  redirect_with_query("#{root_path}retries")
190
262
  end
191
263
 
192
- get '/scheduled' do
193
- @count = (params['count'] || 25).to_i
194
- (@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)
195
267
  @scheduled = @scheduled.map { |msg, score| Sidekiq::SortedEntry.new(nil, score, msg) }
196
268
 
197
269
  erb(:scheduled)
@@ -207,10 +279,10 @@ module Sidekiq
207
279
  end
208
280
  end
209
281
 
210
- post '/scheduled' do
211
- redirect(request.path) unless params['key']
282
+ post "/scheduled" do
283
+ redirect(request.path) unless params["key"]
212
284
 
213
- params['key'].each do |key|
285
+ params["key"].each do |key|
214
286
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
215
287
  delete_or_add_queue job, params if job
216
288
  end
@@ -219,7 +291,8 @@ module Sidekiq
219
291
  end
220
292
 
221
293
  post "/scheduled/:key" do
222
- halt(404) unless key = route_params[:key]
294
+ key = route_params[:key]
295
+ halt(404) unless key
223
296
 
224
297
  job = Sidekiq::ScheduledSet.new.fetch(*parse_params(key)).first
225
298
  delete_or_add_queue job, params if job
@@ -227,88 +300,159 @@ module Sidekiq
227
300
  redirect_with_query("#{root_path}scheduled")
228
301
  end
229
302
 
230
- get '/dashboard/stats' do
303
+ get "/dashboard/stats" do
231
304
  redirect "#{root_path}stats"
232
305
  end
233
306
 
234
- get '/stats' do
307
+ get "/stats" do
235
308
  sidekiq_stats = Sidekiq::Stats.new
236
- redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
237
-
309
+ redis_stats = redis_info.select { |k, v| REDIS_KEYS.include? k }
238
310
  json(
239
311
  sidekiq: {
240
- processed: sidekiq_stats.processed,
241
- failed: sidekiq_stats.failed,
242
- busy: sidekiq_stats.workers_size,
243
- processes: sidekiq_stats.processes_size,
244
- enqueued: sidekiq_stats.enqueued,
245
- scheduled: sidekiq_stats.scheduled_size,
246
- retries: sidekiq_stats.retry_size,
247
- 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,
248
320
  default_latency: sidekiq_stats.default_queue_latency
249
321
  },
250
- redis: redis_stats
322
+ redis: redis_stats,
323
+ server_utc_time: server_utc_time
251
324
  )
252
325
  end
253
326
 
254
- get '/stats/queues' do
255
- 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
395
+ end
396
+
397
+ post "/change_locale" do
398
+ locale = params["locale"]
399
+
400
+ match = available_locales.find { |available|
401
+ locale == available
402
+ }
403
+
404
+ session[:locale] = match if match
405
+
406
+ reload_page
256
407
  end
257
408
 
258
409
  def call(env)
259
410
  action = self.class.match(env)
260
- return [404, {"Content-Type" => "text/plain", "X-Cascade" => "pass" }, ["Not Found"]] unless action
411
+ return [404, {Rack::CONTENT_TYPE => "text/plain", Web::X_CASCADE => "pass"}, ["Not Found"]] unless action
261
412
 
413
+ app = @klass
262
414
  resp = catch(:halt) do
263
- app = @klass
264
415
  self.class.run_befores(app, action)
265
- begin
266
- resp = action.instance_exec env, &action.block
267
- ensure
268
- self.class.run_afters(app, action)
269
- end
270
-
271
- resp
416
+ action.instance_exec env, &action.block
417
+ ensure
418
+ self.class.run_afters(app, action)
272
419
  end
273
420
 
274
- resp = case resp
421
+ case resp
275
422
  when Array
423
+ # redirects go here
276
424
  resp
277
- when Integer
278
- [resp, {}, []]
279
425
  else
280
- type_header = case action.type
281
- when :json
282
- { "Content-Type" => "application/json", "Cache-Control" => "no-cache" }
283
- when String
284
- { "Content-Type" => (action.type || "text/html"), "Cache-Control" => "no-cache" }
285
- else
286
- { "Content-Type" => "text/html", "Cache-Control" => "no-cache" }
287
- end
288
-
289
- [200, type_header, [resp]]
426
+ # rendered content goes here
427
+ headers = {
428
+ Rack::CONTENT_TYPE => "text/html",
429
+ Rack::CACHE_CONTROL => "private, no-store",
430
+ Web::CONTENT_LANGUAGE => action.locale,
431
+ Web::CONTENT_SECURITY_POLICY => process_csp(env, CSP_HEADER_TEMPLATE),
432
+ Web::X_CONTENT_TYPE_OPTIONS => "nosniff"
433
+ }
434
+ # we'll let Rack calculate Content-Length for us.
435
+ [200, headers, [resp]]
290
436
  end
437
+ end
291
438
 
292
- resp[1] = resp[1].dup
293
-
294
- resp[1][CONTENT_LENGTH] = resp[2].inject(0) { |l, p| l + p.bytesize }.to_s
295
-
296
- resp
439
+ def process_csp(env, input)
440
+ input.gsub("!placeholder!", env[:csp_nonce])
297
441
  end
298
442
 
299
- def self.helpers(mod=nil, &block)
300
- if block_given?
443
+ def self.helpers(mod = nil, &block)
444
+ if block
301
445
  WebAction.class_eval(&block)
302
446
  else
303
447
  WebAction.send(:include, mod)
304
448
  end
305
449
  end
306
450
 
307
- def self.before(path=nil, &block)
451
+ def self.before(path = nil, &block)
308
452
  befores << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
309
453
  end
310
454
 
311
- def self.after(path=nil, &block)
455
+ def self.after(path = nil, &block)
312
456
  afters << [path && Regexp.new("\\A#{path.gsub("*", ".*")}\\z"), block]
313
457
  end
314
458
 
@@ -321,8 +465,8 @@ module Sidekiq
321
465
  end
322
466
 
323
467
  def self.run_hooks(hooks, app, action)
324
- hooks.select { |p,_| !p || p =~ action.env[WebRouter::PATH_INFO] }.
325
- each {|_,b| action.instance_exec(action.env, app, &b) }
468
+ hooks.select { |p, _| !p || p =~ action.env[WebRouter::PATH_INFO] }
469
+ .each { |_, b| action.instance_exec(action.env, app, &b) }
326
470
  end
327
471
 
328
472
  def self.befores
@@ -0,0 +1,183 @@
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 "rack/request"
31
+
32
+ module Sidekiq
33
+ class Web
34
+ class CsrfProtection
35
+ def initialize(app, options = nil)
36
+ @app = app
37
+ end
38
+
39
+ def call(env)
40
+ accept?(env) ? admit(env) : deny(env)
41
+ end
42
+
43
+ private
44
+
45
+ def admit(env)
46
+ # On each successful request, we create a fresh masked token
47
+ # which will be used in any forms rendered for this request.
48
+ s = session(env)
49
+ s[:csrf] ||= SecureRandom.base64(TOKEN_LENGTH)
50
+ env[:csrf_token] = mask_token(s[:csrf])
51
+ @app.call(env)
52
+ end
53
+
54
+ def safe?(env)
55
+ %w[GET HEAD OPTIONS TRACE].include? env["REQUEST_METHOD"]
56
+ end
57
+
58
+ def logger(env)
59
+ @logger ||= env["rack.logger"] || ::Logger.new(env["rack.errors"])
60
+ end
61
+
62
+ def deny(env)
63
+ logger(env).warn "attack prevented by #{self.class}"
64
+ [403, {Rack::CONTENT_TYPE => "text/plain"}, ["Forbidden"]]
65
+ end
66
+
67
+ def session(env)
68
+ env["rack.session"] || fail(<<~EOM)
69
+ Sidekiq::Web needs a valid Rack session for CSRF protection. If this is a Rails app,
70
+ make sure you mount Sidekiq::Web *inside* your application routes:
71
+
72
+
73
+ Rails.application.routes.draw do
74
+ mount Sidekiq::Web => "/sidekiq"
75
+ ....
76
+ end
77
+
78
+
79
+ If this is a Rails app in API mode, you need to enable sessions.
80
+
81
+ https://guides.rubyonrails.org/api_app.html#using-session-middlewares
82
+
83
+ If this is a bare Rack app, use a session middleware before Sidekiq::Web:
84
+
85
+ # first, use IRB to create a shared secret key for sessions and commit it
86
+ require 'securerandom'; File.open(".session.key", "w") {|f| f.write(SecureRandom.hex(32)) }
87
+
88
+ # now use the secret with a session cookie middleware
89
+ use Rack::Session::Cookie, secret: File.read(".session.key"), same_site: true, max_age: 86400
90
+ run Sidekiq::Web
91
+
92
+ EOM
93
+ end
94
+
95
+ def accept?(env)
96
+ return true if safe?(env)
97
+
98
+ giventoken = ::Rack::Request.new(env).params["authenticity_token"]
99
+ valid_token?(env, giventoken)
100
+ end
101
+
102
+ TOKEN_LENGTH = 32
103
+
104
+ # Checks that the token given to us as a parameter matches
105
+ # the token stored in the session.
106
+ def valid_token?(env, giventoken)
107
+ return false if giventoken.nil? || giventoken.empty?
108
+
109
+ begin
110
+ token = decode_token(giventoken)
111
+ rescue ArgumentError # client input is invalid
112
+ return false
113
+ end
114
+
115
+ sess = session(env)
116
+ localtoken = sess[:csrf]
117
+
118
+ # Checks that Rack::Session::Cookie actually contains the csrf token
119
+ return false if localtoken.nil?
120
+
121
+ # Rotate the session token after every use
122
+ sess[:csrf] = SecureRandom.base64(TOKEN_LENGTH)
123
+
124
+ # See if it's actually a masked token or not. We should be able
125
+ # to handle any unmasked tokens that we've issued without error.
126
+
127
+ if unmasked_token?(token)
128
+ compare_with_real_token token, localtoken
129
+ elsif masked_token?(token)
130
+ unmasked = unmask_token(token)
131
+ compare_with_real_token unmasked, localtoken
132
+ else
133
+ false # Token is malformed
134
+ end
135
+ end
136
+
137
+ # Creates a masked version of the authenticity token that varies
138
+ # on each request. The masking is used to mitigate SSL attacks
139
+ # like BREACH.
140
+ def mask_token(token)
141
+ token = decode_token(token)
142
+ one_time_pad = SecureRandom.random_bytes(token.length)
143
+ encrypted_token = xor_byte_strings(one_time_pad, token)
144
+ masked_token = one_time_pad + encrypted_token
145
+ encode_token(masked_token)
146
+ end
147
+
148
+ # Essentially the inverse of +mask_token+.
149
+ def unmask_token(masked_token)
150
+ # Split the token into the one-time pad and the encrypted
151
+ # value and decrypt it
152
+ token_length = masked_token.length / 2
153
+ one_time_pad = masked_token[0...token_length]
154
+ encrypted_token = masked_token[token_length..]
155
+ xor_byte_strings(one_time_pad, encrypted_token)
156
+ end
157
+
158
+ def unmasked_token?(token)
159
+ token.length == TOKEN_LENGTH
160
+ end
161
+
162
+ def masked_token?(token)
163
+ token.length == TOKEN_LENGTH * 2
164
+ end
165
+
166
+ def compare_with_real_token(token, local)
167
+ ::Rack::Utils.secure_compare(token.to_s, decode_token(local).to_s)
168
+ end
169
+
170
+ def encode_token(token)
171
+ [token].pack("m0").tr("+/", "-_")
172
+ end
173
+
174
+ def decode_token(token)
175
+ token.tr("-_", "+/").unpack1("m0")
176
+ end
177
+
178
+ def xor_byte_strings(s1, s2)
179
+ s1.bytes.zip(s2.bytes).map { |(c1, c2)| c1 ^ c2 }.pack("c*")
180
+ end
181
+ end
182
+ end
183
+ end